Android 7.1 针对不同的用户实现数据(上网开关)分离

背景

针对之前的文章《Android 利用多APN 实现APN分离》,新的需求到来啦。不但要求不同的用户用不同APN上网,而且还要各自的用户可以对数据分别开关(即通过systemUI的快捷方式开个数据的时候只针对当前用户),这样多用户的环境隔离更加彻底,在数据上网这块,相互不影响。

所谓的上网分离

之前分离APN,实现了手机有拨号产生两个IP地址,分别对应两个用户。但是对于标准安卓系统,数据开关是作为一个整体来看的,只要数据开关关闭,那么所以建好的数据链接都会被断掉,然后整个手机都无法上网,反之亦然。现在,我们要做的就是扩展这个控制开关,使之可以针对不同的用户生效,并且还要兼容标准SDK的接口,即装在另一个用户的app,如果需要控制数据上网,那么不应该影响另一个用户app数据的使用,这就需要对监听数据状态的应用根据用户进行分类,本用户数据状态的改变不应该发送给另一个用户,反正亦然。

主要思路

需要改动的点如下:

  1. SystemUI的分开显示(即通知栏上数据的状态图标要根据本用户的数据设置来显示)。
  2. ConnectivityService中数据状态的转换,即当本用户app查询数据情况的时候,要分别返回各自的网络状态。
  3. DcTracker中,对于建立和拆除数据连接的操作要根据当前用户来分别的保留另一个用户的数据连接,只能针对当前用户进行数据操作。
  4. 标准SDK接口PhoneListener监听数据状态时候,对于改变onDataConnectionStateChanged/onDataActivity消息的发送,要针对不同的用户发送。
  5. TelephonyManager中对于数据开关的查询和设置调用,要根据用户调用不同的接口。
  6. PhoneInterfaceManger中,对于数据开关的查询和设置要扩展新的对用户的接口。并且,对各个用户数据开关状态的维护也要在这个类里面做。
  7. 在Setting的database中,添加对于用户的数据状态的相关内容的存储。
  8. 我们还有自己的mdm数据管控接口,这块也要修改相关的代码(这个不是不是标准安卓的)。

关键的改动

不会根据上面每一个都列出来,这里只列出关键的改动,其他的都是对新接口的适配,所以就不详说啦。

framework的改动

  • ITelephony.aidl(frameworks/base/telephony/java/com/android/internal/telephony/ITelephony.aidl)中添加安装用户id来获得和设置数据开关的接口
    void setDataEnabledAsUser(int subId, boolean enable, int userId);
    boolean getDataEnabledAsUser(int subId, int userId);
  • Telephony Manger.java(frameworks/base/telephony/java/android/telephony/TelephonyManager.java)
    主要做的就是包装标准接口getDataEnabled/setDataEnabled的实现来调用新的getDataEnabledAsUser/setDataEnabledAsUser接口,具体实现的地方是在PhoneInterfaceManager中实现。这其实就是简单的aidl远程调用。
    /**  
     * @hide
     */
    public boolean getDataEnabled(int subId, int userId) {
        boolean retVal = false;
        Log.d(TAG, "getDataEnabled subId " + subId + " userId " + userId);
        try {
            ITelephony telephony = getITelephony();
            if (telephony != null)
                retVal = telephony.getDataEnabledAsUser(subId, userId);
            Log.d(TAG, "getDataEnabled retVal " + retVal);
        } catch (RemoteException e) { 
            Log.e(TAG, "Error calling ITelephony#getDataEnabled", e);
        } catch (NullPointerException e) { 
        }    
        return retVal;
    } 

    /**
     * @hide
     */
    public void setDataEnabledAsUser(int subId, boolean enable, int userId) {
        try {
            Log.d(TAG, "setDataEnabled: enabled=" + enable + " userId " + userId);
            ITelephony telephony = getITelephony();
            if (telephony != null)
                telephony.setDataEnabledAsUser(subId, enable, userId);
        } catch (RemoteException e) {
            Log.e(TAG, "Error calling ITelephony#setDataEnabled", e);
        }
    }

  • ConnectivityService(frameworks/base/services/core/java/com/android/server/ConnectivityService.java)中的getNetworkInfo函数实现的修改,如果发现是非主用户的app来查询networkInfo,这个时候,因为是我们扩展的非标准的连接正在生效,那么我们需要把非标准的连接info转换成标准的default类型的mobile连接,返回给应用,这样应用可以获得正确的网络状态。
    @Override
    public NetworkInfo getNetworkInfo(int networkType) {
        NetworkState state;
        //final NetworkState state = getFilteredNetworkState(networkType, uid, false);
        if (currentUserId == 10 && networkType == ConnectivityManager.TYPE_MOBILE) {
            state = getFilteredNetworkState(ConnectivityManager.TYPE_MOBILE_DEFAULT2, uid, false);
            state.networkInfo.setType(ConnectivityManager.TYPE_MOBILE);
            state.networkInfo.setTypeName(getNetworkTypeName(ConnectivityManager.TYPE_MOBILE));
            Log.d(TAG, "getNetworkInfo after modified networkInfo " + state.networkInfo);
        } else {
            state = getFilteredNetworkState(networkType, uid, false);
        }

        return state.networkInfo;
    }
  • TelephonyRegistry.java(frameworks/baseservices/core/java/com/android/server/TelephonyRegistry.java)对于PhoneListener回掉实现的更改。
   public void notifyDataConnectionForSubscriber(int subId, int state,
            boolean isDataConnectivityPossible, String reason, String apn, String apnType,
            LinkProperties linkProperties, NetworkCapabilities networkCapabilities,
            int networkType, boolean roaming) {
            .......
            int currentUserId = ActivityManager.getCurrentUser();
                    for (Record r : mRecords) {
                        if (r.matchPhoneStateListenerEvent(
                                PhoneStateListener.LISTEN_DATA_CONNECTION_STATE) &&
                                idMatch(r.subId, subId, phoneId)) {
                            try {
                                if (r.callingPackage.equals("com.android.systemui")
                                        && ((apnType.equals(PhoneConstants.APN_TYPE_DEFAULT2) && currentUserId == 10)
                                                || (apnType.equals(PhoneConstants.APN_TYPE_DEFAULT) && currentUserId == 0))) {
                                    log("Notify data connection for systemui");
                                    r.callback.onDataConnectionStateChanged(mDataConnectionState[phoneId],
                                            mDataConnectionNetworkType[phoneId]);
                                } else if (((r.callerUserId != currentUserId) || r.callingPackage.equals("com.android.systemui"))
                                        && (apnType.equals(PhoneConstants.APN_TYPE_DEFAULT) || apnType.equals(PhoneConstants.APN_TYPE_DEFAULT2))) {
                                    log("Notify data connection: backup state for other user");
                                    mDataConnectionStateForOtherUser = mDataConnectionState[phoneId];
                                    mDataConnectionNetworkTypeForOtherUser = mDataConnectionNetworkType[phoneId];
                                    mBackupSubId = subId;
                                    mBackupPhoneId = phoneId;
                                } else {
                                    log("Notify data connection state changed on sub: " +
                                            subId + " record: " + r + " currentUser " + ActivityManager.getCurrentUser() + " apnType " + apnType);
                                    r.callback.onDataConnectionStateChanged(mDataConnectionState[phoneId],
                                            mDataConnectionNetworkType[phoneId]);
                                }
                            } catch (RemoteException ex) {
                                mRemoveList.add(r.binder);
                            }
    .......
  • DcTracker.java(frameworks/opt/telephony/src/java/com/android/internal/telephony/dataconnection/DcTracker.java)中cleanUpAllConnections函数中,不拆除另一个用户的数据。
if ((cleanUpDefault2 && apnContext.getApnType().equals(PhoneConstants.APN_TYPE_DEFAULT))
         || (!cleanUpDefault2 && apnContext.getApnType().equals(PhoneConstants.APN_TYPE_DEFAULT2))) {
         log("cleanUpAllConnections cleanUpDefault2 " + cleanUpDefault2 + " type " + apnContext.getApnType() + " ignore!!!");
        continue;
}  
  • PhoneInterfaceManager.java(packages/services/Telephony/src/com/android/phone/PhoneInterfaceManager.java)
    实现ITelephony中定义的函数。
    public boolean getDataEnabledAsUser(int subId, int userId) {
        boolean enabled = !mDisabledUsers.contains(Integer.valueOf(userId));
        log("getDataEnabledAsUser ret " + enabled);
        return enabled;
    }  

    public void setDataEnabledAsUser(int subId, boolean enable, int userId) {
        final boolean oldValue = enable;

        if (userId >= 0) {
            Integer uobj = Integer.valueOf(userId);
            if (enable) {
                if (mDisabledUsers.contains(uobj)) {
                    mDisabledUsers.remove(uobj);
                    persistDataUsersState();
                }
            } else {
                if (!mDisabledUsers.contains(uobj)) {
                    mDisabledUsers.add(uobj);
                    persistDataUsersState();
                }
            }
            enable = !mDisabledUsers.containsAll(getAllUserIds());
        }

        if (enable) {
            persistDataState(DATA_ENABLED);
        } else {
            persistDataState(DATA_DISABLED);
        }
    }

可以看到,我们用了一个mDisabledUsers的集合来保存禁用数据的用户id,只有当两个用户多打开或者都关闭数据的时候,才会改变标准的数据设置。这样我们就不用扩展Setting数据库里面数据开关啦。但是我们还是要需要保存禁用数据的用户的,这样我们还是要扩展一个Setting数据库项"data_disabled_users",里面存的是用户id,用空格分开。这样做的好处就是方便扩展,如果以后有更多的用户要进行数据分离,那么我们就不需要再扩展Setting数据库了,如果一个用户对应一个数据库项的话,那么用户增多还会带来代码的修改。

总结

其实,这个需求是顺着APN分离来的,但是比APN分离要麻烦琐碎,因为要考虑标准接口的兼容,不同用户app获取状态的统一,所以改动和适配的地方更多一些。另外,对于如何保存分离的开关数据,也是考虑了好久才定下的方案。
基本上,对于数据连接这块的分离就完全实现啦。使用多用户的时候可以感觉在用两个不同的手机。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容