深度解析:Android在Mms设置页面更改短信中心号码流程

简介:

相关控件初始化方法:showSmscPref

  private void  showSmscPref () {
        int count = MSimTelephonyManager.getDefault().getPhoneCount();
        boolean airplaneModeOn = Settings.System.getInt(getContentResolver(),
                Settings.System.AIRPLANE_MODE_ON, 0) != 0;
        for (int i = 0; i < count; i++) {
            final Preference pref = new Preference(this);
            pref.setKey(String.valueOf(i));
            String title = (count <= 1) ?

getString(R.string.pref_one_smcs)
                    : getString(R.string.pref_more_smcs, i + 1);
            pref.setTitle(title);

            pref.setOnPreferenceClickListener(new OnPreferenceClickListener() {
                @Override
                public boolean onPreferenceClick(Preference preference) {
                    MyEditDialogFragment dialog = MyEditDialogFragment.newInstance(
                            MessagingPreferenceActivity.this,
                            preference.getTitle(),
                            preference.getSummary(),
                            Integer.valueOf(preference.getKey()));
                    dialog.show(getFragmentManager(), "dialog");
                    return true;
                }
            });


            mSmscPrefCate.addPreference(pref);
            mSmscPrefList.add(pref);
            updateSMSCPref(i, airplaneModeOn);
        }
        registerReceiver();

    }

这里使用了一个内部类MyEditDialogFragment,该类继承了DialogFragment。


public static class MyEditDialogFragment extends DialogFragment {
        private MessagingPreferenceActivity mActivity;

        public static MyEditDialogFragment newInstance(MessagingPreferenceActivity activity,
                CharSequence title, CharSequence smsc, int sub) {
            MyEditDialogFragment dialog = new MyEditDialogFragment();
            dialog.mActivity = activity;

            Bundle args = new Bundle();
            args.putCharSequence(TITLE, title);
            args.putCharSequence(SMSC, smsc);
            args.putInt(SUB, sub);
            dialog.setArguments(args);
            return dialog;
        }

        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            final int sub = getArguments().getInt(SUB);
            if (null == mActivity) {
                mActivity = (MessagingPreferenceActivity) getActivity();
                dismiss();
            }
            final EditText edit = new EditText(mActivity);
            edit.setPadding(15, 15, 15, 15);
            edit.setText(getArguments().getCharSequence(SMSC));

            Dialog alert = new AlertDialog.Builder(mActivity)
                    .setTitle(getArguments().getCharSequence(TITLE))
                    .setView(edit)
                    .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog, int whichButton) {
                            MyAlertDialogFragment newFragment = MyAlertDialogFragment.newInstance(
                                    mActivity, sub, edit.getText().toString());
                            newFragment.show(getFragmentManager(), "dialog");
                            dismiss();
                        }
                    })
                    .setNegativeButton(android.R.string.cancel, null)
                    .setCancelable(true)
                    .create();

            alert.getWindow().setSoftInputMode(
                    WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
            return alert;
        }
    }

上述代码调用了还有一个内部类:《TAG 1-2》

 public static class MyAlertDialogFragment extends DialogFragment {
        private MessagingPreferenceActivity mActivity;

        public static MyAlertDialogFragment newInstance(MessagingPreferenceActivity activity,
                                                        int sub, String smsc) {
            MyAlertDialogFragment dialog = new MyAlertDialogFragment();
            dialog.mActivity = activity;

            Bundle args = new Bundle();
            args.putInt(SUB, sub);
            args.putString(SMSC, smsc);
            dialog.setArguments(args);
            return dialog;
        }

        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            final int sub = getArguments().getInt(SUB);
            final String displayedSMSC = getArguments().getString(SMSC);

            // When framework re-instantiate this fragment by public empty
            // constructor and call onCreateDialog(Bundle savedInstanceState) ,
            // we should make sure mActivity not null.
            if (null == mActivity) {
                mActivity = (MessagingPreferenceActivity) getActivity();
            }

            return new AlertDialog.Builder(mActivity)
                    .setIcon(android.R.drawable.ic_dialog_alert).setMessage(
                            R.string.set_smsc_confirm_message)
                    .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog, int whichButton) {
                            Intent intent = new Intent();
                            intent.setComponent(new ComponentName("com.android.phonefeature",
                                    "com.android.phonefeature.smsc.SmscService"));
                            intent.setAction(COMMAND_SET_SMSC);
                            intent.putExtra(SUB, sub);
                            intent.putExtra(SMSC, displayedSMSC);
                            mActivity.startService(intent);
                        }

                    })
                    .setNegativeButton(android.R.string.cancel, null)
                    .setCancelable(true)
                    .create();
        }
    }
当用户点击确认(OK)后,会启动一个SmscService服务,而且把改动后的smsc number封装到intent中去,在SmscService服务中的onStartCommand将Intent中的数加入到消息队列中进行处理。
    public int onStartCommand(Intent intent, int flags, int startId) {
        int sub = intent.getIntExtra(SUB, 0);
        int count = MSimTelephonyManager.getDefault().getPhoneCount();
        Phone phone = count > 1 ? MSimPhoneFactory.getPhone(sub)
                : PhoneFactory.getDefaultPhone();
        if (phone == null) return START_STICKY;

        boolean enable = phone.getIccCard().hasIccCard();
        Intent i = new Intent();
        i.setAction(NOTIFY_PHONE_STATE);
        i.putExtra(SUB, sub);
        i.putExtra(ENABLE, enable);
        sendBroadcast(i);

        String action = intent.getAction();
        Message msg;

        if (COMMAND_GET_SMSC.equals(action)) {
            msg = mHandler.obtainMessage(MSG_GET_SMSC);
            msg.arg1 = sub;
            phone.getSmscAddress(msg);
        } else if (COMMAND_SET_SMSC.equals(action)) {
            msg = mHandler.obtainMessage(MSG_SET_SMSC);
            msg.arg1 = sub;

            String displayedSMSC = intent.getStringExtra(SMSC);
            Bundle bundle = new Bundle();
            bundle.putString(SMSC, displayedSMSC);
            msg.setData(bundle);

            String actualSMSC = adjustSMSC(displayedSMSC);
            phone.setSmscAddress(actualSMSC, msg);

        }
        return START_STICKY;
    }

上述代码中的phone。通过验证分析,得到phone为GSMPhone的实例,验证的代码为PhoneFactory类中的makeDefaultPhone方法进行验证得到(我这里验证的结果为GSM:

 public static void makeDefaultPhone(Context context) {<TAG 1-1>
        synchronized(Phone.class) {
            if (!sMadeDefaults) {
                sLooper = Looper.myLooper();
                sContext = context;

                if (sLooper == null) {
                    throw new RuntimeException(
                        "PhoneFactory.makeDefaultPhone must be called from Looper thread");
                }

                int retryCount = 0;
                for(;;) {
                    boolean hasException = false;
                    retryCount ++;

                    try {
                        // use UNIX domain socket to
                        // prevent subsequent initialization
                        new LocalServerSocket("com.android.internal.telephony");
                    } catch (java.io.IOException ex) {
                        hasException = true;
                    }

                    if ( !hasException ) {
                        break;
                    } else if (retryCount > SOCKET_OPEN_MAX_RETRY) {
                        throw new RuntimeException("PhoneFactory probably already running");
                    } else {
                        try {
                            Thread.sleep(SOCKET_OPEN_RETRY_MILLIS);
                        } catch (InterruptedException er) {
                        }
                    }
                }

                sPhoneNotifier = new DefaultPhoneNotifier();

                // Get preferred network mode
                int preferredNetworkMode = RILConstants.PREFERRED_NETWORK_MODE;
                if (TelephonyManager.getLteOnCdmaModeStatic() == PhoneConstants.LTE_ON_CDMA_TRUE) {
                    preferredNetworkMode = Phone.NT_MODE_GLOBAL;
                }
                int networkMode = Settings.Global.getInt(context.getContentResolver(),
                        Settings.Global.PREFERRED_NETWORK_MODE, preferredNetworkMode);
                Rlog.i(LOG_TAG, "Network Mode set to " + Integer.toString(networkMode));

                // As per certain operator requirement, the device is expected to be in global《TAG 1-2》
                // mode from boot up, by enabling the property persist.env.phone.global the
                // network mode is set to global during boot up.
                if (SystemProperties.getBoolean("persist.env.phone.global", false)) {
                    networkMode = Phone.NT_MODE_LTE_CMDA_EVDO_GSM_WCDMA;
                    Settings.Global.putInt(context.getContentResolver(),
                            Settings.Global.PREFERRED_NETWORK_MODE, networkMode);
                }

                // Get cdmaSubscription mode from Settings.Global
                int cdmaSubscription;
                cdmaSubscription = Settings.Global.getInt(context.getContentResolver(),
                                Settings.Global.CDMA_SUBSCRIPTION_MODE,
                                sPreferredCdmaSubscription);
                Rlog.i(LOG_TAG, "Cdma Subscription set to " + cdmaSubscription);

                //reads the system properties and makes commandsinterface
                sCommandsInterface = new RIL(context, networkMode, cdmaSubscription);

                // Instantiate UiccController so that all other classes can just call getInstance()
                UiccController.make(context, sCommandsInterface);

                int phoneType = TelephonyManager.getPhoneType(networkMode);
                if (phoneType == PhoneConstants.PHONE_TYPE_GSM) {
                    Rlog.i(LOG_TAG, "Creating GSMPhone");
                    sProxyPhone = new PhoneProxy(new GSMPhone(context,
                            sCommandsInterface, sPhoneNotifier));
                            android.util.Log.d("bill","GSM");

                } else if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
                    switch (TelephonyManager.getLteOnCdmaModeStatic()) {
                        case PhoneConstants.LTE_ON_CDMA_TRUE:
                            Rlog.i(LOG_TAG, "Creating CDMALTEPhone");
                            sProxyPhone = new PhoneProxy(new CDMALTEPhone(context,
                                sCommandsInterface, sPhoneNotifier));
                                android.util.Log.d("bill","CDMALTE");
                            break;
                        case PhoneConstants.LTE_ON_CDMA_FALSE:
                        default:
                            Rlog.i(LOG_TAG, "Creating CDMAPhone");
                            sProxyPhone = new PhoneProxy(new CDMAPhone(context,
                                    sCommandsInterface, sPhoneNotifier));
                                    android.util.Log.d("bill","CDMA");
                            break;
                    }
                }

                sMadeDefaults = true;
            }
        }
    }
可是GSMPhone中并没有setSmscAddress()方法,可是GSMPhone继承了BasePhone类,因此我们在BasePhone中找到了setSmscAddress()方法。


    @Override
    public void setSmscAddress(String address, Message result) {
        mCi.setSmscAddress(address, result);
    }

上述代码中的mCi为接口CommandsInterface的实例。mCi的所引用的实例为BasePhone构造函数中传递过来的,因此我们代码<TAG1-1>中找到了该实例为RIL的实例。因此我们在RIL类中找到了setSmscAddress方法;

    @Override
    public void setSmscAddress(String address, Message result) {
        RILRequest rr = RILRequest.obtain(RIL_REQUEST_SET_SMSC_ADDRESS, result);

        rr.mParcel.writeString(address);

        if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)
                + " : " + address);

        send(rr);

    }

    private void
    send(RILRequest rr) {

        Message msg;

        if (mSocket == null) {
            rr.onError(RADIO_NOT_AVAILABLE, null);
            rr.release();
            return;
        }

        msg = mSender.obtainMessage(EVENT_SEND, rr);

        acquireWakeLock();

        msg.sendToTarget();
    }

在上述代码中send()方法会发消息给RIL中RILSender进行处理,同一时候msg中封装了MSG_SET_SMSC,在RIL的RILSender中运行写卡操作,同一时候发消息给SmscService。

 //***** Handler implementation
        @Override public void
        handleMessage(Message msg) {
            RILRequest rr = (RILRequest)(msg.obj);
            RILRequest req = null;

            switch (msg.what) {
                case EVENT_SEND:
                    /**
                     * mRequestMessagePending++ already happened for every
                     * EVENT_SEND, thus we must make sure
                     * mRequestMessagePending-- happens once and only once
                     */
                    boolean alreadySubtracted = false;
                    try {
                        LocalSocket s;

                        s = mSocket;

                        if (s == null) {
                            rr.onError(RADIO_NOT_AVAILABLE, null);
                            rr.release();
                            if (mRequestMessagesPending > 0)
                                mRequestMessagesPending--;
                            alreadySubtracted = true;
                            return;
                        }

                        synchronized (mRequestList) {
                            mRequestList.add(rr);
                            mRequestMessagesWaiting++;
                        }

                        if (mRequestMessagesPending > 0)
                            mRequestMessagesPending--;
                        alreadySubtracted = true;

                        byte[] data;

                        data = rr.mParcel.marshall();
                        rr.mParcel.recycle();
                        rr.mParcel = null;

                        if (data.length > RIL_MAX_COMMAND_BYTES) {
                            throw new RuntimeException(
                                    "Parcel larger than max bytes allowed! "
                                                          + data.length);
                        }

                        // parcel length in big endian
                        dataLength[0] = dataLength[1] = 0;
                        dataLength[2] = (byte)((data.length >> 8) & 0xff);
                        dataLength[3] = (byte)((data.length) & 0xff);

                        //Rlog.v(RILJ_LOG_TAG, "writing packet: " + data.length + " bytes");

                        s.getOutputStream().write(dataLength);
                        s.getOutputStream().write(data);
                    } catch (IOException ex) {
                        Rlog.e(RILJ_LOG_TAG, "IOException", ex);
                        req = findAndRemoveRequestFromList(rr.mSerial);
                        // make sure this request has not already been handled,
                        // eg, if RILReceiver cleared the list.
                        if (req != null || !alreadySubtracted) {
                            rr.onError(RADIO_NOT_AVAILABLE, null);
                            rr.release();
                        }
                    } catch (RuntimeException exc) {
                        Rlog.e(RILJ_LOG_TAG, "Uncaught exception ", exc);
                        req = findAndRemoveRequestFromList(rr.mSerial);
                        // make sure this request has not already been handled,
                        // eg, if RILReceiver cleared the list.
                        if (req != null || !alreadySubtracted) {
                            rr.onError(GENERIC_FAILURE, null);
                            rr.release();
                        }
                    } finally {
                        // Note: We are "Done" only if there are no outstanding
                        // requests or replies. Thus this code path will only release
                        // the wake lock on errors.
                        releaseWakeLockIfDone();
                    }

                    if (!alreadySubtracted && mRequestMessagesPending > 0) {
                        mRequestMessagesPending--;
                    }

                    break;

这里进行推断写卡操作是否成功,并发送广播。


    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            AsyncResult ar = (AsyncResult) msg.obj;
            String smsc = null;
            switch (msg.what) {
                case MSG_SET_SMSC:
                    if (ar.exception != null) {
                        notifyChange(NOTIFY_SMSC_ERROR, null, 0);
                        return;
                    } else {
                        Bundle bundle = msg.getData();
                        smsc = bundle.getString(SMSC);
                        notifyChange(NOTIFY_SMSC_SUCCESS, null, 0);
                    }

                    break;

    private void notifyChange(String notify, String smsc, int sub) {
        Intent intent = new Intent(notify);
        intent.putExtra(SMSC, smsc);
        intent.putExtra(SUB, sub);
        sendBroadcast(intent);
    }

我们在MessagingPreferenceActivity的registerReceiver()方法中注冊广播接收器进行监听。

 private void registerReceiver() {
        if (mReceiver != null) return;
        mReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                String action = intent.getAction();
                if (Intent.ACTION_AIRPLANE_MODE_CHANGED.equals(action)) {
                    /*AddBy:yabin.huang BugID:SWBUG00029352 Date:20140521*/
                    updateSMSCPref(ALL_SUB, isAirplaneModeOn());
                    Message msg = new Message();
                    msg.what = AIR_PLANE_MODE_CHANGED;
                    msg.arg1 = (isAirplaneModeOn() ?

AIR_PLANE_MODE_ENABLE : AIR_PLANE_MODE_DISABLE);
                    mAirPlaneModeHandler.sendMessage(msg);
                } else if(TelephonyIntents.ACTION_SIM_STATE_CHANGED.equals(action)){
                    if(isSimReady())
                    updateSMSCPref(ALL_SUB, isAirplaneModeOn());
                } else if (NOTIFY_SMSC_ERROR.equals(action)) {
                    showToast(R.string.set_smsc_error);
                } else if (NOTIFY_SMSC_SUCCESS.equals(action)) {
                    showToast(R.string.set_smsc_success);
                    int sub = intent.getIntExtra(SUB, 0);
                    String summary = intent.getStringExtra(SMSC);
                    Log.d("bill","summary--"+summary);
                    mSmscPrefList.get(sub).setSummary(summary);

                } else if (NOTIFY_SMSC_UPDATE.equals(action)) {
                    int sub = intent.getIntExtra(SUB, 0);
                    if(TextUtils.isEmpty(mSmscPrefList.get(sub).getSummary())){
                        String summary = intent.getStringExtra(SMSC);
                        if(summary==null||summary.length()==0){
                            updateSMSCPref(ALL_SUB, isAirplaneModeOn());
                            mSmscPrefList.get(sub).setEnabled(false);
                            mSmscPrefList.get(sub).setSummary(null);
                        }else{
                            mSmscPrefList.get(sub).setEnabled(true);
                            mSmscPrefList.get(sub).setSummary(summary);
                        }
                    }else{
                        mSmscPrefList.get(sub).setEnabled(true);
                    }
                }
            }
        };

        IntentFilter filter = new IntentFilter();
        filter.addAction(TelephonyIntents.ACTION_SIM_STATE_CHANGED);
        filter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED);
        filter.addAction(NOTIFY_SMSC_ERROR);
        filter.addAction(NOTIFY_SMSC_SUCCESS);
        filter.addAction(NOTIFY_SMSC_UPDATE);
        registerReceiver(mReceiver, filter);
    }

至今。改变整个过程的短信中心号码被整理完毕。

版权声明:本文博客原创文章,博客,未经同意,不得转载。









本文转自mfrbuaa博客园博客,原文链接:http://www.cnblogs.com/mfrbuaa/p/4680325.html,如需转载请自行联系原作者


相关文章
|
1月前
|
移动开发 前端开发 Android开发
【02】建立各项目录和页面标准化产品-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
【02】建立各项目录和页面标准化产品-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
239 12
【02】建立各项目录和页面标准化产品-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
|
2月前
|
存储 消息中间件 人工智能
【05】AI辅助编程完整的安卓二次商业实战-消息页面媒体对象(Media Object)布局实战调整-按钮样式调整实践-优雅草伊凡
【05】AI辅助编程完整的安卓二次商业实战-消息页面媒体对象(Media Object)布局实战调整-按钮样式调整实践-优雅草伊凡
109 11
【05】AI辅助编程完整的安卓二次商业实战-消息页面媒体对象(Media Object)布局实战调整-按钮样式调整实践-优雅草伊凡
|
1月前
|
移动开发 Android开发
【03】建立隐私关于等相关页面和内容-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
【03】建立隐私关于等相关页面和内容-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
122 0
|
2月前
|
存储 域名解析 弹性计算
阿里云上云流程参考:云服务器+域名+备案+域名解析绑定,全流程图文详解
对于初次通过阿里云完成上云的企业和个人用户来说,很多用户不仅是需要选购云服务器,同时还需要注册域名以及完成备案和域名的解析相关流程,从而实现网站的上线。本文将以上云操作流程为核心,结合阿里云的活动政策与用户系统梳理云服务器选购、域名注册、备案申请及域名绑定四大关键环节,以供用户完成线上业务部署做出参考。
|
9月前
|
JavaScript 前端开发 Android开发
【03】仿站技术之python技术,看完学会再也不用去购买收费工具了-修改整体页面做好安卓下载发给客户-并且开始提交网站公安备案-作为APP下载落地页文娱产品一定要备案-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
【03】仿站技术之python技术,看完学会再也不用去购买收费工具了-修改整体页面做好安卓下载发给客户-并且开始提交网站公安备案-作为APP下载落地页文娱产品一定要备案-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
341 13
【03】仿站技术之python技术,看完学会再也不用去购买收费工具了-修改整体页面做好安卓下载发给客户-并且开始提交网站公安备案-作为APP下载落地页文娱产品一定要备案-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
|
8月前
|
安全 算法 小程序
【03】微信支付商户申请下户到配置完整流程-微信开放平台创建APP应用-填写上传基础资料-生成安卓证书-获取Apk签名-申请+配置完整流程-优雅草卓伊凡
【03】微信支付商户申请下户到配置完整流程-微信开放平台创建APP应用-填写上传基础资料-生成安卓证书-获取Apk签名-申请+配置完整流程-优雅草卓伊凡
541 28
【03】微信支付商户申请下户到配置完整流程-微信开放平台创建APP应用-填写上传基础资料-生成安卓证书-获取Apk签名-申请+配置完整流程-优雅草卓伊凡
|
7月前
|
NoSQL 应用服务中间件 PHP
布谷一对一直播源码android版环境配置流程及功能明细
部署需基于 CentOS 7.9 系统,硬盘不低于 40G,使用宝塔面板安装环境,包括 PHP 7.3(含 Redis、Fileinfo 扩展)、Nginx、MySQL 5.6、Redis 和最新 Composer。Swoole 扩展需按步骤配置。2021.08.05 后部署需将站点目录设为 public 并用 ThinkPHP 伪静态。开发环境建议 Windows 操作系统与最新 Android Studio,基础配置涉及 APP 名称修改、接口域名更换、包名调整及第三方登录分享(如 QQ、微信)的配置,同时需完成阿里云与腾讯云相关设置。
|
9月前
|
编解码 缓存 Prometheus
「ximagine」业余爱好者的非专业显示器测试流程规范,同时也是本账号输出内容的数据来源!如何测试显示器?荒岛整理总结出多种测试方法和注意事项,以及粗浅的原理解析!
本期内容为「ximagine」频道《显示器测试流程》的规范及标准,我们主要使用Calman、DisplayCAL、i1Profiler等软件及CA410、Spyder X、i1Pro 2等设备,是我们目前制作内容数据的重要来源,我们深知所做的仍是比较表面的活儿,和工程师、科研人员相比有着不小的差距,测试并不复杂,但是相当繁琐,收集整理测试无不花费大量时间精力,内容不完善或者有错误的地方,希望大佬指出我们好改进!
629 16
「ximagine」业余爱好者的非专业显示器测试流程规范,同时也是本账号输出内容的数据来源!如何测试显示器?荒岛整理总结出多种测试方法和注意事项,以及粗浅的原理解析!
|
9月前
|
前端开发 Java Shell
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
629 20
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex

推荐镜像

更多