1. 程式人生 > >Android開發中,使用WIFI API時的兩個陷阱(第一篇,共二篇)

Android開發中,使用WIFI API時的兩個陷阱(第一篇,共二篇)

一、版本適配問題。在Android6.0上,APP無法更新系統儲存過的、不是由當前APP建立的WIFI配置。

1、現象
    在測試過程中,發現了一個bug。場景是:在Android6.0的機器上,連線一個系統儲存過的wifi,輸入了正確的密碼後,卻始終無法連線成功,即updateNetwork始終返回-1.

2、分析
    首先簡要說一下wifi的連線過程。我們使用系統api對當前要連線的wifi進行判斷:(1)如果系統未儲存過,則建立一個WifiConfiguration,呼叫WifiManager的addNetwork方法去建立新的wifi連線;(2)如果是系統儲存過的,就更新WifiConfiguration的引數(密碼等引數),呼叫WifiManager的updateNetwork方法去更新這個wifi。上面兩種方式,都會返回一個int值(the ID of the network),如果大於0,則表示操作成功;小於0表示操作失敗。

    第一步,檢視google官方的6.0 changes文件,看是否能找出很直觀的原因。Android 6.0 Changes連結

Wi-Fi and Networking Changes
This release introduces the following behavior changes to the Wi-Fi and networking APIs.

  • Your apps can now change the state of WifiConfiguration objects only if you created these objects. You are not permitted to modify or delete WifiConfiguration objects created by the user or by other apps.

google已經明確的告訴開發者,APP是不能修改或者刪除不是自己建立的wifi的。接下來,從原始碼層面深入分析一下原理。

    第二步,對比Android6.0和5.0以及7.0的原始碼,檢視版本之間的差異。

WifiManager裡,都會呼叫到WifiServiceImpl的addOrUpdateNetwork方法。

private int addOrUpdateNetwork(WifiConfiguration config) {
        try {
            return mService.addOrUpdateNetwork(config);
        } catch
(RemoteException e) { return -1; } }

然後,進入到WifiServiceImpl裡面,進行一系列的許可權驗證後,為方法引數config設定當前uid的資訊後,才開始連結

if (config.networkId == WifiConfiguration.INVALID_NETWORK_ID) {
    config.creatorUid = Binder.getCallingUid();
} else {
    config.lastUpdateUid = Binder.getCallingUid();
}

if (mWifiStateMachineChannel != null) {
    return mWifiStateMachine.syncAddOrUpdateNetwork(mWifiStateMachineChannel, config);
} else {
    Slog.e(TAG, "mWifiStateMachineChannel is not initialized");
    return -1;
}

繼續往狀態機WifiStateMachine裡走–

public int syncAddOrUpdateNetwork(AsyncChannel channel, WifiConfiguration config) {
    Message resultMsg = channel.sendMessageSynchronously(CMD_ADD_OR_UPDATE_NETWORK, config);
    int result = resultMsg.arg1;
    resultMsg.recycle();
    return result;
}

在內部類ConnectModeState的processMessage(Message message)方法裡,開始處理訊息CMD_ADD_OR_UPDATE_NETWORK,也就是這裡,開始出現了版本程式碼的差異。
下面是6.0程式碼比5.0新增的程式碼片段

case CMD_ADD_OR_UPDATE_NETWORK:
    config = (WifiConfiguration) message.obj;
    // difference begin(6.0新增程式碼開始位置)
    if (!recordUidIfAuthorized(config, message.sendingUid,
            /* onlyAnnotate */ false)) {
        logw("Not authorized to update network "
             + " config=" + config.SSID
             + " cnid=" + config.networkId
             + " uid=" + message.sendingUid);
        replyToMessage(message, message.what, FAILURE);
        break;
    }
    // difference end(6.0新增程式碼結束位置)
    //......

重點看一下recordUidIfAuthorized()方法–

/**
 * Save the UID correctly depending on if this is a new or existing network.
 * @return true if operation is authorized, false otherwise
 */
boolean recordUidIfAuthorized(WifiConfiguration config, int uid, boolean onlyAnnotate) {
    if (!mWifiConfigStore.isNetworkConfigured(config)) {
    config.creatorUid = uid;
    config.creatorName = mContext.getPackageManager().getNameForUid(uid);
    } else if (!mWifiConfigStore.canModifyNetwork(uid, config, onlyAnnotate)) {
    return false;
    }

    config.lastUpdateUid = uid;
    config.lastUpdateName = mContext.getPackageManager().getNameForUid(uid);

    return true;

}

1、首先通過WifiConfigStore物件判斷如果這個wifi還沒有被儲存過,則記錄creatorUid為當前的app id,這個比較好理解。
2、然後繼續判斷當前app有沒有許可權修改這個wifi,就是canModifyNetwork()方法。繼續跟進去,最終會執行到WifiConfigStore的canModifyNetwork()方法–

/**
     * Checks if uid has access to modify the configuration corresponding to networkId.
     *
     * Factors involved in modifiability of a config are as follows.
     *    If uid is a Device Owner app then it has full control over the device, including WiFi
     * configs.
     *    If the modification is only for administrative annotation (e.g. when connecting) or the
     * config is not lockdown eligible (currently that means any config not last updated by the DO)
     * then the creator of config or an app holding OVERRIDE_CONFIG_WIFI can modify the config.
     *    If the config is lockdown eligible and the modification is substantial (not annotation)
     * then the requirement to be able to modify the config by the uid is as follows:
     *    a) the uid has to hold OVERRIDE_CONFIG_WIFI and
     *    b) the lockdown feature should be disabled.
     */
    boolean canModifyNetwork(int uid, int networkId, boolean onlyAnnotate) {
        WifiConfiguration config = mConfiguredNetworks.get(networkId);

        if (config == null) {
            loge("canModifyNetwork: cannot find config networkId " + networkId);
            return false;
        }

        final DevicePolicyManagerInternal dpmi = LocalServices.getService(
                DevicePolicyManagerInternal.class);

        final boolean isUidDeviceOwner = dpmi != null && dpmi.isActiveAdminWithPolicy(uid,
                DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);

        if (isUidDeviceOwner) {
            // Device Owner has full control over the device, including WiFi Configs
            return true;
        }

        final boolean isCreator = (config.creatorUid == uid);

        if (onlyAnnotate) {
            return isCreator || checkConfigOverridePermission(uid);
        }

        // Check if device has DPM capability. If it has and dpmi is still null, then we
        // treat this case with suspicion and bail out.
        if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN)
                && dpmi == null) {
            return false;
        }

        // WiFi config lockdown related logic. At this point we know uid NOT to be a Device Owner.

        final boolean isConfigEligibleForLockdown = dpmi != null && dpmi.isActiveAdminWithPolicy(
                config.creatorUid, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
        if (!isConfigEligibleForLockdown) {
            return isCreator || checkConfigOverridePermission(uid);
        }

        final ContentResolver resolver = mContext.getContentResolver();
        final boolean isLockdownFeatureEnabled = Settings.Global.getInt(resolver,
                Settings.Global.WIFI_DEVICE_OWNER_CONFIGS_LOCKDOWN, 0) != 0;
        return !isLockdownFeatureEnabled && checkConfigOverridePermission(uid);
    }

這裡面會先判斷當前app是否是Device Owner,然後判斷是否有許可權OVERRIDE_WIFI_CONFIG,這個app都不符合,所以會返回false,分析到這裡得以驗證。

3、結語
    用google的原話:Your apps can now change the state of WifiConfiguration objects only if you created these objects. You are not permitted to modify or delete WifiConfiguration objects created by the user or by other apps.

4、備註:google在6.0上增加了這個邏輯,然後又在7.0去掉了。所以這個問題只存在於6.0的系統上。