diff --git a/app/build.gradle b/app/build.gradle index 463dbc4..7531e99 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -17,8 +17,8 @@ android { minSdkVersion 23 targetSdkVersion 29 - versionCode 36 - versionName "1.3.5" + versionCode 39 + versionName "1.3.8" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/src/main/java/com/xwad/os/activity/activation/ActivationActivity.java b/app/src/main/java/com/xwad/os/activity/activation/ActivationActivity.java index fe2cb2e..d711f92 100644 --- a/app/src/main/java/com/xwad/os/activity/activation/ActivationActivity.java +++ b/app/src/main/java/com/xwad/os/activity/activation/ActivationActivity.java @@ -135,7 +135,7 @@ public class ActivationActivity extends BaseMvvmActivity { // 此处执行轮询任务,例如网络请求 - mViewModel.getActivation(); + mViewModel.getEncryptedCode(); }, throwable -> { // 处理错误 throwable.printStackTrace(); diff --git a/app/src/main/java/com/xwad/os/activity/activation/ActivationViewModel.java b/app/src/main/java/com/xwad/os/activity/activation/ActivationViewModel.java index 8465e29..ac76cfb 100644 --- a/app/src/main/java/com/xwad/os/activity/activation/ActivationViewModel.java +++ b/app/src/main/java/com/xwad/os/activity/activation/ActivationViewModel.java @@ -8,6 +8,7 @@ import com.hjq.toast.Toaster; import com.tencent.mmkv.MMKV; import com.trello.rxlifecycle4.RxLifecycle; import com.trello.rxlifecycle4.android.ActivityEvent; +import com.xwad.os.BuildConfig; import com.xwad.os.base.mvvm.BaseViewModel; import com.xwad.os.bean.BaseResponse; import com.xwad.os.bean.CodeBean; @@ -153,16 +154,19 @@ public class ActivationViewModel extends BaseViewModel() { + .subscribe(new Observer>() { @Override public void onSubscribe(@NonNull Disposable d) { Log.e("codeActivation", "onSubscribe: "); } @Override - public void onNext(@NonNull BaseResponse baseResponse) { + public void onNext(@NonNull BaseResponse baseResponse) { Log.e("codeActivation", "onNext: " + baseResponse); if (baseResponse.code == 200) { + CodeBean codeBean = baseResponse.data; + ActivationUtil.getInstance().setActivationCode(codeBean.getCode()); + ActivationUtil.getInstance().setActivation(1); mCodeActivationData.setValue(true); } else { Toaster.show(baseResponse.msg); @@ -183,6 +187,7 @@ public class ActivationViewModel extends BaseViewModel mGetCodeData = new MutableLiveData<>(); + @Deprecated public void getActivation() { NetInterfaceManager.getInstance().getActivationCodeControl() .compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY)) @@ -196,7 +201,7 @@ public class ActivationViewModel extends BaseViewModel baseResponse) { if (baseResponse.code == 200) { CodeBean codeBean = baseResponse.data; - mMMKV.encode(CommonConfig.ACTIVATIONBEAN_CODE_KEY, codeBean.getCode()); + ActivationUtil.getInstance().setActivationCode(codeBean.getCode()); ActivationUtil.getInstance().setActivation(1); mGetCodeData.setValue(true); } else { @@ -216,6 +221,42 @@ public class ActivationViewModel extends BaseViewModel>() { + @Override + public void onSubscribe(@NonNull Disposable d) { + Log.e("getEncryptedCode", "onSubscribe: "); + } + + @Override + public void onNext(@NonNull BaseResponse baseResponse) { + if (BuildConfig.DEBUG) { + Log.e("getEncryptedCode", "onNext: " + baseResponse); + } + if (baseResponse.code == 200) { + CodeBean codeBean = baseResponse.data; + ActivationUtil.getInstance().setActivationCode(codeBean.getCode()); + ActivationUtil.getInstance().setActivation(1); + mGetCodeData.setValue(true); + } else { + ActivationUtil.getInstance().setActivation(0); + } + } + + @Override + public void onError(@NonNull Throwable e) { + Log.e("getEncryptedCode", "onError: " + e.getMessage()); + } + + @Override + public void onComplete() { + Log.e("getEncryptedCode", "onComplete: "); + } + }); + } + public MutableLiveData mCouponsLegalData = new MutableLiveData<>(); public MutableLiveData mCouponsData = new MutableLiveData<>(); diff --git a/app/src/main/java/com/xwad/os/activity/home/HomeActivity.java b/app/src/main/java/com/xwad/os/activity/home/HomeActivity.java index 87eb5dc..cbc2e00 100644 --- a/app/src/main/java/com/xwad/os/activity/home/HomeActivity.java +++ b/app/src/main/java/com/xwad/os/activity/home/HomeActivity.java @@ -264,7 +264,7 @@ public class HomeActivity extends BaseMvvmActivity() { @Override public void onChanged(UserInfo userInfo) { diff --git a/app/src/main/java/com/xwad/os/activity/home/HomeViewModel.java b/app/src/main/java/com/xwad/os/activity/home/HomeViewModel.java index 9b92fb4..7579cb0 100644 --- a/app/src/main/java/com/xwad/os/activity/home/HomeViewModel.java +++ b/app/src/main/java/com/xwad/os/activity/home/HomeViewModel.java @@ -114,7 +114,7 @@ public class HomeViewModel extends BaseViewModel appUpdateInfoBaseResponse) { Log.e("checkUpdate", "onNext: " + appUpdateInfoBaseResponse); if (appUpdateInfoBaseResponse.code == 200) { - AppInfo appUpdateInfo = appUpdateInfoBaseResponse.data; - mAppUpdateInfoData.setValue(appUpdateInfo); + AppInfo appInfo = appUpdateInfoBaseResponse.data; + mAppUpdateInfoData.setValue(appInfo); } else { mAppUpdateInfoData.setValue(null); } @@ -175,12 +175,12 @@ public class HomeViewModel extends BaseViewModel>() { @Override public void onSubscribe(@NonNull Disposable d) { - Log.e("checkUpdate", "onSubscribe: "); + Log.e("checkUpdateUiUiOS", "onSubscribe: "); } @Override public void onNext(@NonNull BaseResponse appUpdateInfoBaseResponse) { - Log.e("checkUpdate", "onNext: " + appUpdateInfoBaseResponse); + Log.e("checkUpdateUiUiOS", "onNext: " + appUpdateInfoBaseResponse); if (appUpdateInfoBaseResponse.code == 200) { AppUpdateInfo appUpdateInfo = appUpdateInfoBaseResponse.data; mAppUpdateInfoUiUiOSData.setValue(appUpdateInfo); @@ -191,13 +191,13 @@ public class HomeViewModel extends BaseViewModel mGetCodeData = new MutableLiveData<>(); + @Deprecated public void getActivation() { NetInterfaceManager.getInstance().getActivationCodeControl() .compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY)) @@ -293,7 +294,7 @@ public class HomeViewModel extends BaseViewModel baseResponse) { if (baseResponse.code == 200) { CodeBean codeBean = baseResponse.data; - mMMKV.encode(CommonConfig.ACTIVATIONBEAN_CODE_KEY, codeBean.getCode()); + ActivationUtil.getInstance().setActivationCode(codeBean.getCode()); mGetCodeData.setValue(true); ActivationUtil.getInstance().setActivation(1); } else { @@ -313,6 +314,42 @@ public class HomeViewModel extends BaseViewModel>() { + @Override + public void onSubscribe(@NonNull Disposable d) { + Log.e("getEncryptedCode", "onSubscribe: "); + } + + @Override + public void onNext(@NonNull BaseResponse baseResponse) { + if (BuildConfig.DEBUG) { + Log.e("getEncryptedCode", "onNext: " + baseResponse); + } + if (baseResponse.code == 200) { + CodeBean codeBean = baseResponse.data; + ActivationUtil.getInstance().setActivationCode(codeBean.getCode()); + ActivationUtil.getInstance().setActivation(1); + mGetCodeData.setValue(true); + } else { + ActivationUtil.getInstance().setActivation(0); + } + } + + @Override + public void onError(@NonNull Throwable e) { + Log.e("getEncryptedCode", "onError: " + e.getMessage()); + } + + @Override + public void onComplete() { + Log.e("getEncryptedCode", "onComplete: "); + } + }); + } + public MutableLiveData mUserInfoData = new MutableLiveData<>(); public void getUserInfo() { diff --git a/app/src/main/java/com/xwad/os/bean/UserExpireInfo.java b/app/src/main/java/com/xwad/os/bean/UserExpireInfo.java new file mode 100644 index 0000000..b7d7c4d --- /dev/null +++ b/app/src/main/java/com/xwad/os/bean/UserExpireInfo.java @@ -0,0 +1,26 @@ +package com.xwad.os.bean; + +import java.io.Serializable; + +public class UserExpireInfo implements Serializable { + private static final long serialVersionUID = 2733370887454572840L; + + String expire_at; + int year; + + public String getExpire_at() { + return expire_at; + } + + public void setExpire_at(String expire_at) { + this.expire_at = expire_at; + } + + public int getYear() { + return year; + } + + public void setYear(int year) { + this.year = year; + } +} diff --git a/app/src/main/java/com/xwad/os/fragment/complex/ComplexFragment.java b/app/src/main/java/com/xwad/os/fragment/complex/ComplexFragment.java index a088ef7..56f9e70 100644 --- a/app/src/main/java/com/xwad/os/fragment/complex/ComplexFragment.java +++ b/app/src/main/java/com/xwad/os/fragment/complex/ComplexFragment.java @@ -1,5 +1,6 @@ package com.xwad.os.fragment.complex; +import android.content.Context; import android.os.Bundle; import android.util.Log; import android.view.View; @@ -7,8 +8,11 @@ import android.widget.RelativeLayout; import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleEventObserver; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.viewpager2.adapter.FragmentStateAdapter; @@ -17,7 +21,9 @@ import androidx.viewpager2.widget.ViewPager2; import com.chad.library.adapter.base.BaseQuickAdapter; import com.chad.library.adapter.base.BaseViewHolder; import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; import com.google.gson.reflect.TypeToken; +import com.xwad.os.BuildConfig; import com.xwad.os.R; import com.xwad.os.base.mvvm.fragment.BaseMvvmFragment; import com.xwad.os.bean.jxw.TabBean; @@ -32,49 +38,47 @@ import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; +import java.lang.ref.WeakReference; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; /** - * A simple {@link Fragment} subclass. - * Use the {@link ComplexFragment#newInstance} factory method to - * create an instance of this fragment. + * 复合Fragment(包含垂直ViewPager2+侧边Tab列表) + * 核心功能:根据年级加载不同的学习阶段Fragment,支持年级切换刷新 */ public class ComplexFragment extends BaseMvvmFragment { - private static final String TAG = "ComplexFragment"; - - private FragmentActivity mContext; - - private MyAdapter adapter; - private TabTbxAdapter tabTbxAdapter; - private List tabBeanList = new ArrayList<>(); - private List mFragment = new ArrayList<>(); - - - // TODO: Rename parameter arguments, choose names that match - // the fragment initialization parameters, e.g. ARG_ITEM_NUMBER + // 1. 抽取常量,消除硬编码 + private static final String TAG = ComplexFragment.class.getSimpleName(); private static final String ARG_PARAM1 = "param1"; private static final String ARG_PARAM2 = "param2"; + private static final int OFFSCREEN_PAGE_LIMIT = 5; // 预加载页数(保留原有逻辑) + private static final String GRADE_PRIMARY_SCHOOL = "小学"; + private static final String ASSETS_TBX_JSON_PATH = "script/fragment_tbx.json"; + private static final int COLOR_WHITE = -1; + private static final int COLOR_TAB_NORMAL = 1627389951; - // TODO: Rename and change types of parameters + // 2. 优化上下文引用(避免内存泄漏) + private WeakReference mContextRef; + private StageTabAdapter mStageTabAdapter; // 语义化命名:tabTbxAdapter → mStageTabAdapter + private StageFragmentAdapter mStageFragmentAdapter; // 语义化命名:adapter → mStageFragmentAdapter + private List mTabBeanList = new ArrayList<>(); + private int mSelectedTabPosition = -1; // 语义化命名:defSel → mSelectedTabPosition + + // 页面参数(保留原有逻辑) private String mParam1; private String mParam2; public ComplexFragment() { - // Required empty public constructor - Log.e(TAG, "ComplexFragment: "); + super(); + if (BuildConfig.DEBUG) { + Log.d(TAG, "ComplexFragment: 构造函数"); + } } /** - * Use this factory method to create a new instance of - * this fragment using the provided parameters. - * - * @param param1 Parameter 1. - * @param param2 Parameter 2. - * @return A new instance of fragment ComplexFragment. + * 实例化Fragment(保留原有参数逻辑,增加注释) */ - // TODO: Rename and change types and number of parameters public static ComplexFragment newInstance(String param1, String param2) { ComplexFragment fragment = new ComplexFragment(); Bundle args = new Bundle(); @@ -85,16 +89,25 @@ public class ComplexFragment extends BaseMvvmFragment learnStageList = getLearnStageList(currentGrade); + if (learnStageList.isEmpty()) { + if (BuildConfig.DEBUG) { + Log.w(TAG, "initStageView: 学习阶段列表为空"); } + return; } - mViewDataBinding.rvTitle.setAdapter(tabTbxAdapter); - mViewDataBinding.subjectViewPager.setAdapter(adapter); - if (!tabBeanList.isEmpty()) { - tabTbxAdapter.setChoosePosition(0); - mViewDataBinding.subjectViewPager.setCurrentItem(0); - } + // 更新Tab列表 + updateTabList(learnStageList); + // 更新ViewPager2适配器 + updateViewPager2Adapter(learnStageList); + // 设置默认选中项 + setDefaultSelectedTab(); } - @Subscribe(threadMode = ThreadMode.MAIN) - public void onGkNoticeEvent(UpdateGradeEvent updateGradeEvent) { - initViews(); - if (isAdded()) { -// getNjId(); - } - } - - - private List getLearnStage() { - String grade = SPUtils.getGrade(); - Log.e(TAG, "getLearnStage: grade = " + grade); - String jsonString = AssertUtils.getFromAssets(mContext, "script/fragment_tbx.json"); - Type type = new TypeToken>() { - }.getType(); - List arrayList = new Gson().fromJson(jsonString, type); + /** + * 获取学习阶段列表(原getLearnStage,增强空安全+容错) + */ + private List getLearnStageList(String grade) { List resultList = new ArrayList<>(); - for (LearnStageBean learnStageBean : arrayList) { - if ("六年级".equals(grade)) { - String learnStage = learnStageBean.getLearnStage(); - if (learnStage.contains("," + grade)) { - resultList.add(learnStageBean); - } - } else if (learnStageBean.getLearnStage().contains(grade)) { - resultList.add(learnStageBean); + FragmentActivity context = mContextRef != null ? mContextRef.get() : null; + if (context == null || grade == null) { + return resultList; + } + + // 读取Assets文件(增加空判断) + String jsonString = AssertUtils.getFromAssets(context, ASSETS_TBX_JSON_PATH); + if (jsonString == null || jsonString.isEmpty()) { + Log.e(TAG, "getLearnStageList: 读取JSON文件失败"); + return resultList; + } + + // Gson解析(增加异常捕获) + Type type = new TypeToken>() {}.getType(); + try { + List allStageList = new Gson().fromJson(jsonString, type); + if (allStageList == null) { + return resultList; } + + // 过滤当前年级的学习阶段 + for (LearnStageBean stageBean : allStageList) { + String learnStage = stageBean.getLearnStage(); + if (learnStage == null) continue; + + if ("六年级".equals(grade)) { + if (learnStage.contains("," + grade)) { + resultList.add(stageBean); + } + } else if (learnStage.contains(grade)) { + resultList.add(stageBean); + } + } + } catch (JsonSyntaxException e) { + Log.e(TAG, "getLearnStageList: JSON解析失败", e); } return resultList; } + /** + * 更新Tab列表数据 + */ + private void updateTabList(List learnStageList) { + mTabBeanList.clear(); + FragmentActivity context = mContextRef != null ? mContextRef.get() : null; + if (context == null) return; - public class MyAdapter extends FragmentStateAdapter { - MyAdapter(FragmentActivity fragmentActivity) { + for (LearnStageBean stageBean : learnStageList) { + // 资源ID获取(增加空判断) + int normalResId = getDrawableResId(context, stageBean.getNormal()); + int pressResId = getDrawableResId(context, stageBean.getPress()); + TabBean tabBean = new TabBean(stageBean.getName(), normalResId, pressResId); + mTabBeanList.add(tabBean); + } + mStageTabAdapter.notifyDataSetChanged(); + } + + /** + * 更新ViewPager2适配器 + */ + private void updateViewPager2Adapter(List learnStageList) { + FragmentActivity context = mContextRef != null ? mContextRef.get() : null; + if (context == null) return; + + if (mStageFragmentAdapter == null) { + mStageFragmentAdapter = new StageFragmentAdapter(context, learnStageList); + mViewDataBinding.subjectViewPager.setAdapter(mStageFragmentAdapter); + } else { + mStageFragmentAdapter.updateData(learnStageList); + } + } + + /** + * 设置默认选中Tab(增加非空判断) + */ + private void setDefaultSelectedTab() { + if (mTabBeanList.isEmpty() || mViewDataBinding == null) return; + + mStageTabAdapter.setSelectedPosition(0); + if (mViewDataBinding.subjectViewPager.getCurrentItem() != 0) { + mViewDataBinding.subjectViewPager.setCurrentItem(0, false); + } + } + + /** + * 获取Drawable资源ID(封装重复逻辑,增加容错) + */ + private int getDrawableResId(Context context, String resName) { + if (context == null || resName == null || resName.isEmpty()) { + return 0; + } + return context.getResources().getIdentifier(resName, "drawable", context.getPackageName()); + } + + // ========== EventBus事件接收 ========== + @Subscribe(threadMode = ThreadMode.MAIN) + public void onGradeUpdateEvent(UpdateGradeEvent event) { + // 增强Fragment状态判断 + if (isAdded() && !isDetached() && mViewDataBinding != null) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "onGradeUpdateEvent: 年级更新,刷新视图"); + } + initStageView(); + } + } + + // ========== ViewPager2适配器(原MyAdapter,优化命名+容错) ========== + public class StageFragmentAdapter extends FragmentStateAdapter { + private List mLearnStageList; + + public StageFragmentAdapter(@NonNull FragmentActivity fragmentActivity, List learnStageList) { super(fragmentActivity); + this.mLearnStageList = learnStageList != null ? learnStageList : new ArrayList<>(); } @NonNull @Override public Fragment createFragment(int position) { - return mFragment.get(position); + if (position < 0 || position >= mLearnStageList.size()) { + Log.w(TAG, "createFragment: 位置越界,position=" + position); + return new Fragment(); // 兜底避免崩溃 + } + + LearnStageBean bean = mLearnStageList.get(position); + Bundle bundle = new Bundle(); + bundle.putString("subject", bean.getName()); + bundle.putString("label", bean.getLabel()); + bundle.putString("tbsp_tag", bean.getTbsp_tag()); + bundle.putBoolean("isBig", bean.getIsBig()); + + try { + Class fragmentClass = Class.forName(bean.getClassName()); + Fragment fragment = (Fragment) fragmentClass.getDeclaredConstructor().newInstance(); + fragment.setArguments(bundle); + return fragment; + } catch (Exception e) { + Log.e(TAG, "createFragment: 实例化Fragment失败,className=" + bean.getClassName(), e); + return new Fragment(); // 兜底 + } } @Override - public long getItemId(int i) { - return (mFragment.get(i)).hashCode(); + public long getItemId(int position) { + // 优先用label的hashCode(保证唯一性),无label则用position + LearnStageBean bean = mLearnStageList.get(position); + return bean.getLabel() != null ? bean.getLabel().hashCode() : position; + } + + @Override + public boolean containsItem(long itemId) { + for (LearnStageBean bean : mLearnStageList) { + if (bean.getLabel() != null && bean.getLabel().hashCode() == itemId) { + return true; + } + } + return false; } @Override public int getItemCount() { - return mFragment == null ? 0 : mFragment.size(); + return mLearnStageList.size(); + } + + public void updateData(List newList) { + this.mLearnStageList = newList != null ? newList : new ArrayList<>(); + notifyDataSetChanged(); } } - public class TabTbxAdapter extends BaseQuickAdapter { - private int defSel; + // ========== Tab列表适配器(原TabTbxAdapter,优化命名+封装) ========== + public class StageTabAdapter extends BaseQuickAdapter { - public void setChoosePosition(int i) { - int i2 = defSel; - if (i2 == i) { - return; - } - if (i2 != -1) { - getData().get(defSel).setSelect(false); - notifyItemChanged(defSel); - } - defSel = i; - if (defSel != -1) { - getData().get(defSel).setSelect(true); - notifyItemChanged(defSel); - } + public StageTabAdapter(List data) { + super(R.layout.item_tab_1, data != null ? data : new ArrayList<>()); } - public TabTbxAdapter(List list) { - super(R.layout.item_tab_1, list); - defSel = -1; + /** + * 设置选中的Tab位置(封装状态管理) + */ + public void setSelectedPosition(int position) { + if (position == mSelectedTabPosition || position < 0 || position >= getData().size()) { + return; + } + + // 取消原选中项 + if (mSelectedTabPosition != -1) { + getData().get(mSelectedTabPosition).setSelect(false); + notifyItemChanged(mSelectedTabPosition); + } + + // 设置新选中项 + mSelectedTabPosition = position; + getData().get(mSelectedTabPosition).setSelect(true); + notifyItemChanged(mSelectedTabPosition); } @Override - public void convert(final BaseViewHolder baseViewHolder, TabBean tabBean) { - RelativeLayout relativeLayout = baseViewHolder.getView(R.id.rl_root); - TextView textView = baseViewHolder.getView(R.id.iv_tab_title); + public void convert(@NonNull BaseViewHolder holder, TabBean tabBean) { + RelativeLayout rootLayout = holder.getView(R.id.rl_root); + TextView titleTv = holder.getView(R.id.iv_tab_title); + + // Tab选中/未选中样式(抽取常量,增强可读性) if (tabBean.isSelect()) { - relativeLayout.setBackgroundResource(R.drawable.icon_xk_tab_bg_pre); - textView.setTextSize(getResources().getDimension(R.dimen.x12)); - textView.setTextColor(-1); + rootLayout.setBackgroundResource(R.drawable.icon_xk_tab_bg_pre); + titleTv.setTextSize(getResources().getDimension(R.dimen.x12)); + titleTv.setTextColor(COLOR_WHITE); } else { - relativeLayout.setBackground(null); - textView.setTextSize(getResources().getDimension(R.dimen.x10)); - textView.setTextColor(1627389951); + rootLayout.setBackground(null); + titleTv.setTextSize(getResources().getDimension(R.dimen.x10)); + titleTv.setTextColor(COLOR_TAB_NORMAL); } - textView.setText(tabBean.getTab_Title()); - baseViewHolder.itemView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - setChoosePosition(baseViewHolder.getAdapterPosition()); - mViewDataBinding.subjectViewPager.setCurrentItem(baseViewHolder.getAdapterPosition(), false); + + titleTv.setText(tabBean.getTab_Title()); + + // Tab点击事件(解耦,避免直接依赖外部View) + holder.itemView.setOnClickListener(v -> { + int currentPos = holder.getAdapterPosition(); + setSelectedPosition(currentPos); + if (mViewDataBinding != null) { + mViewDataBinding.subjectViewPager.setCurrentItem(currentPos, false); } }); } } - -} +} \ No newline at end of file diff --git a/app/src/main/java/com/xwad/os/fragment/mine/MineFragment.java b/app/src/main/java/com/xwad/os/fragment/mine/MineFragment.java index 61b8f60..a4a2b6d 100644 --- a/app/src/main/java/com/xwad/os/fragment/mine/MineFragment.java +++ b/app/src/main/java/com/xwad/os/fragment/mine/MineFragment.java @@ -217,6 +217,9 @@ public class MineFragment extends BaseMvvmFragment baseResponse) { if (baseResponse.code == 200) { CodeBean codeBean = baseResponse.data; - mMMKV.encode(CommonConfig.ACTIVATIONBEAN_CODE_KEY, codeBean.getCode()); + ActivationUtil.getInstance().setActivationCode( codeBean.getCode()); ActivationUtil.getInstance().setActivation(1); ActivationUtil.getInstance().startJxwLauncher(); } else { @@ -111,6 +113,42 @@ public class AccountViewModel extends BaseViewModel>() { + @Override + public void onSubscribe(@NonNull Disposable d) { + Log.e("getEncryptedCode", "onSubscribe: "); + } + + @Override + public void onNext(@NonNull BaseResponse baseResponse) { + if (BuildConfig.DEBUG) { + Log.e("getEncryptedCode", "onNext: " + baseResponse); + } + if (baseResponse.code == 200) { + CodeBean codeBean = baseResponse.data; + ActivationUtil.getInstance().setActivationCode( codeBean.getCode()); + ActivationUtil.getInstance().setActivation(1); + ActivationUtil.getInstance().startJxwLauncher(); + } else { + ActivationUtil.getInstance().setActivation(0); + } + } + + @Override + public void onError(@NonNull Throwable e) { + Log.e("getEncryptedCode", "onError: " + e.getMessage()); + } + + @Override + public void onComplete() { + Log.e("getEncryptedCode", "onComplete: "); + } + }); + } + public void getAppInfo(String pkg) { NetInterfaceManager.getInstance().getAdminAppObservable(pkg) .compose(RxLifecycle.bindUntilEvent(getLifecycle(), FragmentEvent.DESTROY)) diff --git a/app/src/main/java/com/xwad/os/network/NetInterfaceManager.java b/app/src/main/java/com/xwad/os/network/NetInterfaceManager.java index 09df1fd..ea3763a 100644 --- a/app/src/main/java/com/xwad/os/network/NetInterfaceManager.java +++ b/app/src/main/java/com/xwad/os/network/NetInterfaceManager.java @@ -535,13 +535,14 @@ public class NetInterfaceManager { .observeOn(AndroidSchedulers.mainThread()); } - public Observable getCodeActivationControl(String code) { + public Observable> getCodeActivationControl(String code) { return mRetrofit.create(UserApi.class) .codeActivation(getToken(), DeviceSNManager.getDeviceSN(), code) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()); } + @Deprecated public Observable> getActivationCodeControl() { return mRetrofit.create(UserApi.class) .getActivationCode(getToken(), DeviceSNManager.getDeviceSN()) @@ -549,6 +550,13 @@ public class NetInterfaceManager { .observeOn(AndroidSchedulers.mainThread()); } + public Observable> getEncryptedActivationCodeControl() { + return mRetrofit.create(UserApi.class) + .getEncryptedActivationCode(getToken(), DeviceSNManager.getDeviceSN()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + } + public Observable> getPayQrcodeControl(String orderSn) { return mRetrofit.create(UserApi.class) .payQrcode(getToken(), DeviceSNManager.getDeviceSN(), orderSn) @@ -593,8 +601,8 @@ public class NetInterfaceManager { public Observable getOauthTokenObservable() { Map params = new HashMap<>(); - params.put("appId", JxwUtils.getAppId()); - params.put("signature", JxwUtils.getAppSecret()); + params.put("appId", JgyUtils.getAppId()); + params.put("signature", JgyUtils.getAppSecret()); params.put("timestamp", String.valueOf(System.currentTimeMillis())); params.put("nonce", "zylcpxlj"); return mJxwRetrofit.create(JxwApi.class) diff --git a/app/src/main/java/com/xwad/os/network/UrlAddress.java b/app/src/main/java/com/xwad/os/network/UrlAddress.java index ffa68da..d458584 100644 --- a/app/src/main/java/com/xwad/os/network/UrlAddress.java +++ b/app/src/main/java/com/xwad/os/network/UrlAddress.java @@ -20,6 +20,8 @@ public class UrlAddress { /*用户会员信息(用于判断是否有VIP)*/ public static final String USER_INFO = "user/info"; + /*获取设备过期时间*/ + public static final String USER_expire_info = "user/expire-info"; /*套餐可选列表*/ public static final String VIP_LIST = "user/vip-list"; /*购买VIP下订单*/ @@ -30,7 +32,11 @@ public class UrlAddress { /*激活码激活*/ public static final String activation_code = "activation/code"; /*获取激活码(激活成功后才有)*/ - public static final String get_activation_code = "activation/get-code"; + @Deprecated + public static final String GET_ACTIVATION_CODE_DEPRECATED = "activation/get-code"; + /*获取加密激活码*/ + public static final String GET_ACTIVATION_CODE = "activation/get-activation-code"; + /*发起支付*/ public static final String PAY_QRCODE = "pay/qrcode"; /*续费购买(已经是VIP过期或没过期)*/ diff --git a/app/src/main/java/com/xwad/os/network/api/UserApi.java b/app/src/main/java/com/xwad/os/network/api/UserApi.java index eb2bcdf..2d0ce57 100644 --- a/app/src/main/java/com/xwad/os/network/api/UserApi.java +++ b/app/src/main/java/com/xwad/os/network/api/UserApi.java @@ -4,6 +4,7 @@ import com.xwad.os.bean.BaseResponse; import com.xwad.os.bean.CodeBean; import com.xwad.os.bean.OrderInfo; import com.xwad.os.bean.PayInfo; +import com.xwad.os.bean.UserExpireInfo; import com.xwad.os.bean.UserInfo; import com.xwad.os.bean.VipInfo; import com.xwad.os.network.UrlAddress; @@ -24,6 +25,11 @@ public interface UserApi { @Header("token") String token ); + @GET(UrlAddress.USER_expire_info) + Observable> getUserExpireInfo( + @Header("token") String token + ); + @GET(UrlAddress.VIP_LIST) Observable>> getVipList( @Header("token") String token, @@ -55,18 +61,25 @@ public interface UserApi { @FormUrlEncoded @POST(UrlAddress.activation_code) - Observable codeActivation( + Observable> codeActivation( @Header("token") String token, @Field("sn") String sn, @Field("code") String code ); - @GET(UrlAddress.get_activation_code) + @Deprecated + @GET(UrlAddress.GET_ACTIVATION_CODE_DEPRECATED) Observable> getActivationCode( @Header("token") String token, @Query("sn") String sn ); + @GET(UrlAddress.GET_ACTIVATION_CODE) + Observable> getEncryptedActivationCode( + @Header("token") String token, + @Query("sn") String sn + ); + @FormUrlEncoded @POST(UrlAddress.PAY_QRCODE) Observable> payQrcode( diff --git a/app/src/main/java/com/xwad/os/utils/ActivationUtil.java b/app/src/main/java/com/xwad/os/utils/ActivationUtil.java index 4f381a7..098de7e 100644 --- a/app/src/main/java/com/xwad/os/utils/ActivationUtil.java +++ b/app/src/main/java/com/xwad/os/utils/ActivationUtil.java @@ -114,6 +114,11 @@ public class ActivationUtil { return mMMKV.decodeLong(CommonConfig.UIUI_EXPIRE_TIME_KEY, DEFAULT_EXPIRE_TIME); } + public void setActivationCode(String activationCode) { + String code = CryptoUtils.decrypt(activationCode); + mMMKV.encode(CommonConfig.ACTIVATIONBEAN_CODE_KEY, code); + } + public String getActivationCode() { return mMMKV.decodeString(CommonConfig.ACTIVATIONBEAN_CODE_KEY, ""); } diff --git a/app/src/main/java/com/xwad/os/utils/CryptoUtils.java b/app/src/main/java/com/xwad/os/utils/CryptoUtils.java new file mode 100644 index 0000000..28cefe5 --- /dev/null +++ b/app/src/main/java/com/xwad/os/utils/CryptoUtils.java @@ -0,0 +1,168 @@ +package com.xwad.os.utils; + +import android.util.Base64; +import android.util.Log; + +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.spec.AlgorithmParameterSpec; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +/** + * Android 对称加密解密工具类 + * 支持 AES/DES/TripleDES + ECB/CBC/CFB/OFB/CTR + PKCS5Padding 等 + */ +public class CryptoUtils { + /** + * 简化解密方法:仅需传入待解密字符串 + * + * @param data 待解密的字符串 + * @return 解密后的原文 + * @throws Exception 解密异常 + */ + public static String decrypt(String data) { + String KEY = JgyUtils.getAesKey(); + String IV = JgyUtils.getAesIv(); + + try { + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + + // 解码密钥 + byte[] keyBytes = KEY.getBytes(StandardCharsets.UTF_8); + SecretKeySpec secretKey = new SecretKeySpec(keyBytes, "AES"); + + // 初始化Cipher + // 解码偏移量IV + byte[] ivBytes = IV.getBytes(StandardCharsets.UTF_8); + AlgorithmParameterSpec ivSpec = new IvParameterSpec(ivBytes); + cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec); + + // 解码待解密数据 + byte[] dataBytes = Base64.decode(data, Base64.DEFAULT); + + // 执行解密并返回字符串 + byte[] decryptBytes = cipher.doFinal(dataBytes); + return new String(decryptBytes, StandardCharsets.UTF_8); + } catch (GeneralSecurityException e) { + e.printStackTrace(); + Log.e("CryptoUtils", "decrypt: " + e.getMessage()); + } + return ""; + } + + /** + * 对称解密核心方法 + * + * @param data 待解密的字符串 + * @param type 加密类型:AES / DES / TRIPLEDES + * @param mode 模式:ECB / CBC / CFB / OFB / CTR + * @param pad 填充方式:PKCS5Padding / ISO10126Padding / NoPadding + * @param key 密钥字符串 + * @param keyType 密钥编码:Utf8 / Base64 / Hex + * @param iv 偏移量(ECB模式无需) + * @param ivType 偏移量编码:Utf8 / Base64 / Hex + * @param isBase64 待解密数据编码:true=Base64 false=Hex + * @return 解密后的原始字符串 + * @throws Exception 解密异常(密钥长度/算法不支持/数据错误等) + */ + public static String decrypt( + String data, + String type, + String mode, + String pad, + String key, + String keyType, + String iv, + String ivType, + boolean isBase64 + ) throws Exception { + // 1. 拼接算法全称 + String algorithm = type + "/" + mode + "/" + pad; + Cipher cipher = Cipher.getInstance(algorithm); + + // 2. 解码密钥(修复:== 改为 equals() 字符串值比较) + byte[] keyBytes; + if ("Hex".equals(keyType)) { + keyBytes = hex2Bytes(key); + } else if ("Base64".equals(keyType)) { + keyBytes = Base64.decode(key, Base64.DEFAULT); + } else { + // 强制指定UTF-8,避免乱码 + keyBytes = key.getBytes(StandardCharsets.UTF_8); + } + SecretKeySpec secretKey = new SecretKeySpec(keyBytes, type); + + // 3. 初始化Cipher(ECB无IV,其他模式需要IV) + if ("ECB".equals(mode)) { + cipher.init(Cipher.DECRYPT_MODE, secretKey); + } else { + // 解码偏移量IV + byte[] ivBytes; + if ("Hex".equals(ivType)) { + ivBytes = hex2Bytes(iv); + } else if ("Base64".equals(ivType)) { + ivBytes = Base64.decode(iv, Base64.DEFAULT); + } else { + ivBytes = iv.getBytes(StandardCharsets.UTF_8); + } + AlgorithmParameterSpec ivSpec = new IvParameterSpec(ivBytes); + cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec); + } + + // 4. 解码待解密数据 + byte[] dataBytes; + if (isBase64) { + dataBytes = Base64.decode(data, Base64.DEFAULT); + } else { + dataBytes = hex2Bytes(data); + } + + // 5. 执行解密 + 转字符串 + byte[] decryptBytes = cipher.doFinal(dataBytes); + return new String(decryptBytes, StandardCharsets.UTF_8); + } + + /** + * 全Android版本兼容的 Hex字符串 → 字节数组 解码 + * 替代Apache Commons-Codec的Hex.decodeHex + */ + private static byte[] hex2Bytes(String hexStr) { + if (hexStr == null || hexStr.length() == 0) return new byte[0]; + byte[] bytes = new byte[hexStr.length() / 2]; + for (int i = 0; i < bytes.length; i++) { + int high = Character.digit(hexStr.charAt(2 * i), 16); + int low = Character.digit(hexStr.charAt(2 * i + 1), 16); + bytes[i] = (byte) (high << 4 | low); + } + return bytes; + } + + // ------------------- 你的业务调用示例(直接复制到Android Activity/ViewModel中使用) ------------------- + public static void testDecrypt() { + // 你的原始配置参数(完全不变) + String data = "88072p8780"; + String type = "AES"; + String mode = "CBC"; + String pad = "PKCS5Padding"; + String key = "fRW5b5FI4uG32HISvVZ40QFSkyvXYwP8"; + String keyType = "Utf8"; + String iv = "7COBgH2wUtJnPPSX"; + String ivType = "Utf8"; + boolean isBase64 = true; + + try { + // 执行解密 + String result = decrypt(data, type, mode, pad, key, keyType, iv, ivType, isBase64); + // 输出结果(Android中用Log.d,不要用System.out) + Log.d("Crypto", "解密结果:" + result); + } catch (Exception e) { + // 异常捕获(密钥错误/算法不支持/数据被篡改等) + e.printStackTrace(); + Log.e("Crypto", "解密失败:" + e.getMessage()); + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/xwad/os/utils/JgyUtils.java b/app/src/main/java/com/xwad/os/utils/JgyUtils.java index 2d7eb15..66b2969 100644 --- a/app/src/main/java/com/xwad/os/utils/JgyUtils.java +++ b/app/src/main/java/com/xwad/os/utils/JgyUtils.java @@ -70,7 +70,6 @@ import io.reactivex.rxjava3.disposables.Disposable; import static android.app.ActivityManager.RECENT_IGNORE_UNAVAILABLE; - public class JgyUtils { private static final String TAG = "JgyUtils"; @@ -90,6 +89,19 @@ public class JgyUtils { public static final String LENOVO_TAG = "lenovo"; public static final String NATURE_TAG = "Nature"; + static { + System.loadLibrary("xuewang"); // 加载 libjniutils.so + } + + // 声明为Native方法 + public static native String getAppId(); + + public static native String getAppSecret(); + + public static native String getAesKey(); + + public static native String getAesIv(); + private HashSet ownApp = new HashSet() {{ this.add("com.tt.ttutils"); diff --git a/app/src/main/java/com/xwad/os/utils/JxwUtils.java b/app/src/main/java/com/xwad/os/utils/JxwUtils.java index 20eea57..75ec3af 100644 --- a/app/src/main/java/com/xwad/os/utils/JxwUtils.java +++ b/app/src/main/java/com/xwad/os/utils/JxwUtils.java @@ -5,7 +5,6 @@ import android.util.Log; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.util.HashMap; import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; @@ -13,22 +12,13 @@ import java.util.TreeMap; public class JxwUtils { private static final String TAG = "JxwUtils"; - static { - System.loadLibrary("xuewang"); // 加载 libjniutils.so - } - - // 声明为Native方法 - public static native String getAppId(); - - public static native String getAppSecret(); - public static String getSignature(Map paramMap) { long timeStamp = System.currentTimeMillis(); //用于获取signature SortedMap items = new TreeMap<>(); items.put("nonce", "X6SH3YeRTs"); - items.put("appId", JxwUtils.getAppId()); - items.put("appsecret", JxwUtils.getAppSecret()); + items.put("appId", JgyUtils.getAppId()); + items.put("appsecret", JgyUtils.getAppSecret()); items.put("timestamp", String.valueOf(timeStamp)); // items.putAll(paramMap); diff --git a/app/src/main/jni/xuewang.cpp b/app/src/main/jni/xuewang.cpp index f5149e5..45288a4 100644 --- a/app/src/main/jni/xuewang.cpp +++ b/app/src/main/jni/xuewang.cpp @@ -13,14 +13,28 @@ extern "C" JNIEXPORT jstring JNICALL -Java_com_xwad_os_utils_JxwUtils_getAppId(JNIEnv *env, jclass thiz) { +Java_com_xwad_os_utils_JgyUtils_getAppId(JNIEnv *env, jclass thiz) { std::string key = "hlhdpb"; return env->NewStringUTF(key.c_str()); } extern "C" JNIEXPORT jstring JNICALL -Java_com_xwad_os_utils_JxwUtils_getAppSecret(JNIEnv *env, jclass thiz) { +Java_com_xwad_os_utils_JgyUtils_getAppSecret(JNIEnv *env, jclass thiz) { std::string key = "WDNUnYMNL33lqlGIIZB"; return env->NewStringUTF(key.c_str()); +} + +extern "C" +JNIEXPORT jstring JNICALL +Java_com_xwad_os_utils_JgyUtils_getAesKey(JNIEnv *env, jclass clazz) { + std::string key = "fRW5b5FI4uG32HISvVZ40QFSkyvXYwP8"; + return env->NewStringUTF(key.c_str()); +} + +extern "C" +JNIEXPORT jstring JNICALL +Java_com_xwad_os_utils_JgyUtils_getAesIv(JNIEnv *env, jclass clazz) { + std::string key = "7COBgH2wUtJnPPSX"; + return env->NewStringUTF(key.c_str()); } \ No newline at end of file diff --git a/app/src/main/res/drawable-nodpi/icon_read_klxyy.png b/app/src/main/res/drawable-nodpi/icon_read_klxyy.png new file mode 100644 index 0000000..2189bb6 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/icon_read_klxyy.png differ diff --git a/app/src/main/res/drawable-nodpi/icon_yy_xx_kywtbbdc.png b/app/src/main/res/drawable-nodpi/icon_yy_xx_kywtbbdc.png new file mode 100644 index 0000000..01ea4f7 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/icon_yy_xx_kywtbbdc.png differ diff --git a/app/src/main/res/layout/fragment_english.xml b/app/src/main/res/layout/fragment_english.xml index 4bfdfa5..c60a20f 100644 --- a/app/src/main/res/layout/fragment_english.xml +++ b/app/src/main/res/layout/fragment_english.xml @@ -496,9 +496,9 @@ + android:tag="@string/tag_args_new_bdc" /> + android:src="@drawable/icon_read_klxyy" + android:tag="@string/tag_args_new_klxyy" />