1. 程式人生 > >多個應用連線wifi熱點的切換問題--WifiConfiguration的新增--Android M

多個應用連線wifi熱點的切換問題--WifiConfiguration的新增--Android M


一、版本適配問題。在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的系統上。

-----------------------------------------------------------------------------------------------------------------------------

一下內容為補充和總結:

補充分析以下方法中的WifiConfigStore.isNetworkConfigured()

/**
 * 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;

}

看看isNetworkConfigured(),返回是否已經配置過,怎麼才算配置過呢:

boolean isNetworkConfigured(WifiConfiguration config) {
        // Check if either we have a network Id or a WifiConfiguration
        // matching the one we are trying to add.
//此時還沒有為該config生成networkId,如果是該config是從WifiManager中拿過來的,那麼networkid不為-1,那麼就看已配置的config列表中有沒有該config
        if(config.networkId != INVALID_NETWORK_ID) {
            return (mConfiguredNetworks.get(config.networkId) != null);
        }
//如果該config的networkid為-1,那麼看已配置config列表中有沒有和該config的ssid和加密方式同時相同的config,看configKey方法,知道這個string是ssid和加密方式的組合
        return (mConfiguredNetworks.getByConfigKey(config.configKey()) != null);
    }

再補充分析一下WifiConfigStore#canModifyNetwork(int uid, int networkId, boolean onlyAnnotate):

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);//是否該程序對應的應用首次新增這個config的,如果是就可以修改

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

        final boolean isCreator = (config.creatorUid == uid);//是否該程序對應的應用首次新增這個config的,如果是就可以修改
//如果是重大修改,即onlyAnnotate為false,那麼有OVERRIDE_WIFI_PERMISSION也不一定可以修改,再往下看
        if (onlyAnnotate) {//是否只對config做一些類似註釋的小改動(非重大改動),如果是隻要是該應用新增的,或者有android.permission.OVERRIDE_WIFI_CONFIG許可權
            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.
//跟Android M的新feature Wifi config lockdown有關,後面會說說這個特性
        final boolean isConfigEligibleForLockdown = dpmi != null && dpmi.isActiveAdminWithPolicy(
                config.creatorUid, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
        if (!isConfigEligibleForLockdown) {
            return isCreator || checkConfigOverridePermission(uid);
        }
//這裡也是跟前面說的特性有關,這個特性有關,在settingsprovider中可以配置一個欄位,為1,則啟動lockdown,這樣就算有OVERRIDE_WIFI_CONFIG許可權也無法修改
        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);
    }

所以對於連線Wifi,最好是通過WifiManger#getConfiguredNetworks()得到一個List<WifiConfiguration>列表。然後逐個對比,看有沒有對應ssid的config,有的話直接使用該config.networkId。但是新增到WifiService中的WifiConfiguration,並不一定是正確的,就是說這些config並不一定連線過,就是連線失敗的也會儲存在那裡,除非你在他連線失敗後主動把他移除了,所以用該方法去連線也有風險。

許可權:<uses-permission android:name="android.permission.OVERRIDE_WIFI_CONFIG" />

WiFi configuration lockdown 是 Android M, android for work 新增加的功能,是指企業指定的wifi configuration不允許一般使用者去修改。

如果有MTK online的賬號可以訪問這個地址:https://onlinesso.mediatek.com/Pages/eCourse.aspx?001=003&002=003002&003=003002001&itemId=846&csId=%257B433b9ec7-cc31-43c3-938c-6dfd42cf3b57%257D%2540%257Bad907af8-9a88-484a-b020-ea10437dadf8%257D