diff --git a/app/build.gradle b/app/build.gradle index 3732692..3ef0653 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -69,8 +69,8 @@ android { official { flavorDimensions "default" - versionCode 94 - versionName "3.5.3" + versionCode 99 + versionName "3.5.8" applicationId "com.fuying.sn" buildConfigField "String", "ROOT_URL", '"https://as.fuyingy.com/android/"' @@ -502,11 +502,11 @@ dependencies { implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.legacy:legacy-support-v4:1.0.0' - implementation 'androidx.lifecycle:lifecycle-viewmodel:2.6.2' - implementation 'androidx.lifecycle:lifecycle-livedata:2.6.2' - implementation 'androidx.lifecycle:lifecycle-runtime:2.6.2' + implementation 'androidx.lifecycle:lifecycle-viewmodel:2.3.1' + implementation 'androidx.lifecycle:lifecycle-livedata:2.3.1' + implementation 'androidx.lifecycle:lifecycle-runtime:2.3.1' // 可选:RxJava3 Observable → LiveData 的桥接 - implementation 'androidx.lifecycle:lifecycle-reactivestreams:2.6.2' + implementation 'androidx.lifecycle:lifecycle-reactivestreams:2.3.1' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' @@ -560,9 +560,8 @@ dependencies { implementation 'com.amap.api:location:5.1.0' //工具类 implementation 'com.blankj:utilcodex:1.31.0' - //autosize会改变第三方view的大小 - //https://github.com/JessYanCoding/AndroidAutoSize - //implementation 'me.jessyan:autosize:1.2.1' + //指示器 + implementation 'com.github.hackware1993:MagicIndicator:1.7.0' implementation 'com.github.chrisbanes:PhotoView:2.0.0' //沉浸状态栏 implementation 'com.gitee.zackratos:UltimateBarX:0.8.0' @@ -597,7 +596,7 @@ tasks.register('deleteAppSourceWhiteList', Exec) { ignoreExitValue = true doLast { - println "ADB command executed: settings delete system app_source_white_list" + println "ADB command executed: adb shell settings delete system app_source_white_list" } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cd2e493..74d345e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -126,6 +126,9 @@ android:name=".activity.update.UpdateActivity" android:launchMode="singleTask" android:theme="@style/DialogCloseOnTouchOutside" /> + mFragments; @@ -78,7 +79,7 @@ public class HomeActivity extends BaseMvvmActivity(); -// mFragments.add(new UsageFragment()); + mFragments.add(new UsageFragment()); mFragments.add(new DeviceFragment()); mFragmentManager = getSupportFragmentManager(); mBaseFragmentPagerAdapter = new BaseFragmentPagerAdapter(mFragmentManager, mFragments); diff --git a/app/src/main/java/com/fuying/sn/activity/usage/UsageDetailActivity.java b/app/src/main/java/com/fuying/sn/activity/usage/UsageDetailActivity.java new file mode 100644 index 0000000..b803dc2 --- /dev/null +++ b/app/src/main/java/com/fuying/sn/activity/usage/UsageDetailActivity.java @@ -0,0 +1,196 @@ +package com.fuying.sn.activity.usage; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Color; +import android.os.Bundle; +import android.view.View; +import android.widget.TextView; + +import androidx.core.content.ContextCompat; +import androidx.lifecycle.Observer; +import androidx.recyclerview.widget.GridLayoutManager; + +import com.fuying.sn.R; +import com.fuying.sn.adapter.AppUseTimeAdapter; +import com.fuying.sn.base.mvvm.BaseMvvmActivity; +import com.fuying.sn.bean.BaseResponse; +import com.fuying.sn.data.model.AppUseTimeData; +import com.fuying.sn.data.model.AppUseTimeInfo; +import com.fuying.sn.databinding.ActivityUsageDetailBinding; +import com.fuying.sn.utils.ScreenUtils; +import com.fuying.sn.utils.ToastUtil; + +import net.lucode.hackware.magicindicator.MagicIndicator; +import net.lucode.hackware.magicindicator.buildins.commonnavigator.CommonNavigator; +import net.lucode.hackware.magicindicator.buildins.commonnavigator.abs.CommonNavigatorAdapter; +import net.lucode.hackware.magicindicator.buildins.commonnavigator.abs.IPagerIndicator; +import net.lucode.hackware.magicindicator.buildins.commonnavigator.abs.IPagerTitleView; +import net.lucode.hackware.magicindicator.buildins.commonnavigator.indicators.LinePagerIndicator; +import net.lucode.hackware.magicindicator.buildins.commonnavigator.titles.SimplePagerTitleView; + +import java.util.Collections; +import java.util.List; + +/** + * 应用使用时长详情页。 + * 对应 fragment_usage 中 cl_use_time 模块,进入时 mIsAll = true,展示全部应用数据。 + */ +public class UsageDetailActivity extends BaseMvvmActivity { + + private Activity mContext; + private MagicIndicator mMagicIndicator; + private int mSelectedTab = 0; + private AppUseTimeAdapter mAdapter; + + private int mQueryType = 1; + // 详情页固定展示全部数据 + private boolean mIsAll = true; + + @Override + public boolean setNightMode() { + return true; + } + + @Override + public int getLayoutId() { + return R.layout.activity_usage_detail; + } + + @Override + protected void initDataBinding() { + mContext = this; + mViewModel.setCtx(this); + mViewModel.setLifecycle(getLifecycleSubject()); + mViewModel.setVDBinding(mViewDataBinding); + mViewDataBinding.setClick(new BtnClick()); + } + + @Override + protected void initView() { + initTabs(); + initRecyclerView(); + selectTab(0); + } + + @Override + protected void initData() { + mViewModel.appUseTime.observe(this, this::onAppUseTime); + mViewModel.loading.observe(this, this::onLoading); + mViewModel.error.observe(this, this::onError); + loadAppUseTime(); + } + + private void loadAppUseTime() { + mViewModel.loadAppUseTime(mQueryType, mIsAll ? 1 : 0); + } + + private void onAppUseTime(BaseResponse resp) { + if (resp == null || resp.data == null || resp.data.getApp_list().isEmpty()) { + return; + } + + List list = resp.data.getApp_list(); + Collections.sort(list, (a, b) -> Long.compare(b.getUsage_time(), a.getUsage_time())); + + long totalSeconds = 0; + for (AppUseTimeInfo info : list) { + totalSeconds += info.getUsage_time(); + } + mViewDataBinding.tvTotalTime.setText(formatTotalTime(totalSeconds)); + + mAdapter.setData(list); + } + + private void initTabs() { + mMagicIndicator = mViewDataBinding.magicIndicator; + final String[] titles = { + getString(R.string.usage_today), + getString(R.string.usage_week), + getString(R.string.usage_month) + }; + CommonNavigator commonNavigator = new CommonNavigator(mContext); + commonNavigator.setAdjustMode(false); + commonNavigator.setAdapter(new CommonNavigatorAdapter() { + @Override + public int getCount() { + return titles.length; + } + + @Override + public IPagerTitleView getTitleView(Context context, final int index) { + SimplePagerTitleView titleView = new SimplePagerTitleView(context); + titleView.setText(titles[index]); + titleView.setTextSize(ScreenUtils.sp2px(mContext, 18f)); + titleView.setNormalColor(ContextCompat.getColor(mContext, R.color.gray)); + titleView.setSelectedColor(Color.BLACK); + titleView.setOnClickListener(v -> selectTab(index)); + return titleView; + } + + @Override + public IPagerIndicator getIndicator(Context context) { + return new LinePagerIndicator(context); + } + }); + mMagicIndicator.setNavigator(commonNavigator); + } + + private void initRecyclerView() { + mAdapter = new AppUseTimeAdapter(); + // 使用 rv_left 作为列表容器,设置 GridLayoutManager 2 列实现双列效果 + mViewDataBinding.rvLeft.setLayoutManager(new GridLayoutManager(mContext, 2)); + mViewDataBinding.rvLeft.setAdapter(mAdapter); + // 隐藏右侧 rv,统一用左侧 rv + GridLayoutManager 实现 + mViewDataBinding.rvRight.setVisibility(View.GONE); + } + + private void selectTab(int position) { + mSelectedTab = position; + CommonNavigator navigator = (CommonNavigator) mMagicIndicator.getNavigator(); + if (navigator != null) { + navigator.onPageSelected(position); + navigator.notifyDataSetChanged(); + } + + int newQueryType = position + 1; + if (mQueryType != newQueryType) { + mQueryType = newQueryType; + loadAppUseTime(); + } + } + + private void onLoading(Boolean visible) { + // 页面暂无 loading 视图,可按需要扩展 + } + + private void onError(Throwable e) { + ToastUtil.show(e != null && e.getMessage() != null ? e.getMessage() : getString(R.string.open_app_fail)); + } + + @Override + public void onBackPressed() { + finish(); + } + + public class BtnClick { + public void onBackClick(View view) { + finish(); + } + } + + private static String formatTotalTime(long totalSeconds) { + if (totalSeconds <= 0) { + return "0" + "分钟"; + } + long hours = totalSeconds / 3600; + long minutes = (totalSeconds % 3600) / 60; + if (hours > 0 && minutes > 0) { + return hours + "小时" + minutes + "分钟"; + } else if (hours > 0) { + return hours + "小时"; + } else { + return minutes + "分钟"; + } + } +} diff --git a/app/src/main/java/com/fuying/sn/activity/usage/UsageDetailViewModel.java b/app/src/main/java/com/fuying/sn/activity/usage/UsageDetailViewModel.java new file mode 100644 index 0000000..95dc1b8 --- /dev/null +++ b/app/src/main/java/com/fuying/sn/activity/usage/UsageDetailViewModel.java @@ -0,0 +1,72 @@ +package com.fuying.sn.activity.usage; + +import android.util.Log; + +import androidx.lifecycle.MutableLiveData; + +import com.fuying.sn.base.mvvm.BaseViewModel; +import com.fuying.sn.bean.BaseResponse; +import com.fuying.sn.data.model.AppUseTimeData; +import com.fuying.sn.data.repository.StatisticsRepository; +import com.fuying.sn.databinding.ActivityUsageDetailBinding; +import com.trello.rxlifecycle4.RxLifecycle; +import com.trello.rxlifecycle4.android.ActivityEvent; + +import io.reactivex.rxjava3.disposables.CompositeDisposable; +import io.reactivex.rxjava3.disposables.Disposable; + +/** + * UsageDetailActivity 对应的 ViewModel。 + * 仅负责"应用使用时长"这一模块的数据获取(复用 StatisticsRepository), + * 不持有任何图表/UI 绘制逻辑,结果通过 LiveData 暴露给 Activity。 + */ +public class UsageDetailViewModel extends BaseViewModel { + + private static final String TAG = "UsageDetailViewModel"; + private final CompositeDisposable mDisposables = new CompositeDisposable(); + private final StatisticsRepository mRepository = StatisticsRepository.getInstance(); + + public final MutableLiveData> appUseTime = new MutableLiveData<>(); + public final MutableLiveData loading = new MutableLiveData<>(); + public final MutableLiveData error = new MutableLiveData<>(); + + @Override + public ActivityUsageDetailBinding getVDBinding() { + return binding; + } + + /** + * 加载指定周期的应用使用记录。 + * + * @param queryType 查询类型:1 今天、2 本周、3 本月 + * @param isAll 0 返回使用时长最高的 6 条,1 返回全部有使用的记录 + */ + public void loadAppUseTime(int queryType, int isAll) { + loading.setValue(true); + Disposable disposable = mRepository.getAppUseTime(queryType, isAll) + .compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY)) + .subscribe( + resp -> { + Log.e(TAG, "loadAppUseTime: " + (resp == null ? "null" : resp.data)); + appUseTime.setValue(resp); + loading.setValue(false); + }, + err -> { + error.setValue(err); + loading.setValue(false); + } + ); + mDisposables.add(disposable); + } + + @Override + public void onDestroy() { + mDisposables.clear(); + } + + @Override + protected void onCleared() { + mDisposables.clear(); + super.onCleared(); + } +} diff --git a/app/src/main/java/com/fuying/sn/adapter/AppControlDetailAdapter.java b/app/src/main/java/com/fuying/sn/adapter/AppControlDetailAdapter.java new file mode 100644 index 0000000..d91de0a --- /dev/null +++ b/app/src/main/java/com/fuying/sn/adapter/AppControlDetailAdapter.java @@ -0,0 +1,101 @@ +package com.fuying.sn.adapter; + +import android.content.Context; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.fuying.sn.R; +import com.fuying.sn.bean.PartTime; +import com.fuying.sn.config.CommonConfig; +import com.fuying.sn.data.model.AppControlDetail; +import com.fuying.sn.utils.GlideLoadUtils; +import com.fuying.sn.utils.TimeUtils; +import com.tencent.mmkv.MMKV; + +import java.util.List; + +public class AppControlDetailAdapter extends RecyclerView.Adapter { + private static final String TAG = "DesktopItemAdapter"; + + private MMKV mMMKV = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE); + private Context mContext; + + private List mAppTimeControlInfos; + + public void setAppTimeControlInfos(List appTimeControlInfos) { + mAppTimeControlInfos = appTimeControlInfos; + notifyDataSetChanged(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + mContext = parent.getContext(); + return new ViewHolder(LayoutInflater.from(mContext).inflate(R.layout.item_app_time_detail, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + AppControlDetail appControlDetail = mAppTimeControlInfos.get(position); + holder.tv_name.setText(appControlDetail.getApp_name()); + GlideLoadUtils.getInstance().glideLoad(mContext, appControlDetail.getApp_img(), holder.iv_icon, R.mipmap.ic_launcher); + + if (appControlDetail.getIs_control() == 0) { + holder.tv_type.setText("禁用"); + holder.tv_type.setBackground(mContext.getResources().getDrawable(R.drawable.tv_time_disable_background)); + holder.tv_today_time.setText(""); + holder.tv_remaining_time.setVisibility(View.GONE); + } else { + holder.tv_today_time.setText(""); + holder.tv_remaining_time.setVisibility(View.GONE); + switch (appControlDetail.getControl_type()) { + case 0: + holder.tv_type.setText("不限制"); + holder.tv_type.setBackground(mContext.getResources().getDrawable(R.drawable.tv_time_unlimited_background)); + break; + case 1: + holder.tv_type.setText("限制时长"); + holder.tv_type.setBackground(mContext.getResources().getDrawable(R.drawable.tv_time_duration_background)); + holder.tv_today_time.setText("每天" + TimeUtils.formatTime(appControlDetail.getToday_time_total())); + holder.tv_remaining_time.setText("剩余" + TimeUtils.formatTime(appControlDetail.getRemaining_time())); + holder.tv_remaining_time.setVisibility(View.VISIBLE); + break; + case 2: + holder.tv_type.setText("限制时间段"); + holder.tv_type.setBackground(mContext.getResources().getDrawable(R.drawable.tv_time_period_background)); + holder.tv_today_time.setText(TimeUtils.getPartTimeString(appControlDetail)); + break; + default: + holder.tv_type.setText("禁用"); + holder.tv_type.setBackground(mContext.getResources().getDrawable(R.drawable.tv_time_disable_background)); + } + } + } + + @Override + public int getItemCount() { + return mAppTimeControlInfos == null ? 0 : mAppTimeControlInfos.size(); + } + + public class ViewHolder extends RecyclerView.ViewHolder { + + ImageView iv_icon; + TextView tv_name, tv_type, tv_today_time, tv_remaining_time; + + public ViewHolder(View itemView) { + super(itemView); + iv_icon = itemView.findViewById(R.id.iv_icon); + tv_name = itemView.findViewById(R.id.tv_name); + tv_type = itemView.findViewById(R.id.tv_type); + tv_today_time = itemView.findViewById(R.id.tv_today_time); + tv_remaining_time = itemView.findViewById(R.id.tv_remaining_time); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fuying/sn/adapter/AppUseTimeAdapter.java b/app/src/main/java/com/fuying/sn/adapter/AppUseTimeAdapter.java new file mode 100644 index 0000000..550eb9a --- /dev/null +++ b/app/src/main/java/com/fuying/sn/adapter/AppUseTimeAdapter.java @@ -0,0 +1,83 @@ +package com.fuying.sn.adapter; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.fuying.sn.R; +import com.fuying.sn.data.model.AppUseTimeInfo; +import com.fuying.sn.utils.GlideLoadUtils; + +import java.util.List; + +public class AppUseTimeAdapter extends RecyclerView.Adapter { + + private Context mContext; + private List mList; + + public void setData(List list) { + mList = list; + notifyDataSetChanged(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + mContext = parent.getContext(); + return new ViewHolder(LayoutInflater.from(mContext).inflate(R.layout.item_app_use_time, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + AppUseTimeInfo info = mList.get(position); + holder.tv_name.setText(info.getApp_name()); + holder.tv_time.setText(formatUsageTime(info.getUsage_time())); + GlideLoadUtils.getInstance().glideLoad(mContext, info.getApp_img(), holder.iv_icon, R.mipmap.ic_launcher); + } + + @Override + public int getItemCount() { + return mList == null ? 0 : mList.size(); + } + + /** + * 将秒数格式化为可读时长,精确到秒,如 "1小时20分30秒"、"20分30秒"、"30秒" + */ + private String formatUsageTime(long totalSeconds) { + if (totalSeconds <= 0) { + return "0秒"; + } + long hours = totalSeconds / 3600; + long minutes = (totalSeconds % 3600) / 60; + long seconds = totalSeconds % 60; + StringBuilder sb = new StringBuilder(); + if (hours > 0) { + sb.append(hours).append("小时"); + } + if (minutes > 0) { + sb.append(minutes).append("分"); + } + if (seconds > 0) { + sb.append(seconds).append("秒"); + } + return sb.toString(); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + ImageView iv_icon; + TextView tv_name, tv_time; + + ViewHolder(View itemView) { + super(itemView); + iv_icon = itemView.findViewById(R.id.iv_icon); + tv_name = itemView.findViewById(R.id.tv_name); + tv_time = itemView.findViewById(R.id.tv_time); + } + } +} diff --git a/app/src/main/java/com/fuying/sn/bean/AppTimeControlInfo.java b/app/src/main/java/com/fuying/sn/bean/AppTimeControlInfo.java index 30229a2..01d968b 100644 --- a/app/src/main/java/com/fuying/sn/bean/AppTimeControlInfo.java +++ b/app/src/main/java/com/fuying/sn/bean/AppTimeControlInfo.java @@ -17,6 +17,9 @@ public class AppTimeControlInfo implements Serializable { * 0不管控,1时长管控,2时间段管控 */ int control_type; + /** + * 0 禁用,1 未禁用 + */ int is_control; long work_time; long rest_time; diff --git a/app/src/main/java/com/fuying/sn/bean/DeviceInfo.java b/app/src/main/java/com/fuying/sn/bean/DeviceInfo.java new file mode 100644 index 0000000..ac674fd --- /dev/null +++ b/app/src/main/java/com/fuying/sn/bean/DeviceInfo.java @@ -0,0 +1,26 @@ +package com.fuying.sn.bean; + +import java.io.Serializable; + +public class DeviceInfo implements Serializable { + private static final long serialVersionUID = 1410550828670262801L; + + String parent_mobile; + String bind_time; + + public String getParent_mobile() { + return parent_mobile; + } + + public void setParent_mobile(String parent_mobile) { + this.parent_mobile = parent_mobile; + } + + public String getBind_time() { + return bind_time; + } + + public void setBind_time(String bind_time) { + this.bind_time = bind_time; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fuying/sn/config/CommonConfig.java b/app/src/main/java/com/fuying/sn/config/CommonConfig.java index 2cb3986..fdfef5e 100644 --- a/app/src/main/java/com/fuying/sn/config/CommonConfig.java +++ b/app/src/main/java/com/fuying/sn/config/CommonConfig.java @@ -32,6 +32,9 @@ public class CommonConfig { public static final String BROWSER_APP_KEY = "browser_app_key"; public static final String TYPEWRITING_APP_KEY = "typewriting_app_key"; + /*上次 loadData 请求成功的时间戳(毫秒)*/ + public static final String LAST_SYNC_TIME_KEY = "last_sync_time_key"; + public static final String isLogined = "isLogined"; public static final String AES_KEY = "0123456789ABCDEF"; public static final String HTTP_KEY = "YTM3YTAxNTJmMmZmNzkyM2E2YzIwZjlhZTc0NzNmMGI="; diff --git a/app/src/main/java/com/fuying/sn/data/local/LocalDataSource.java b/app/src/main/java/com/fuying/sn/data/local/LocalDataSource.java new file mode 100644 index 0000000..684440f --- /dev/null +++ b/app/src/main/java/com/fuying/sn/data/local/LocalDataSource.java @@ -0,0 +1,39 @@ +package com.fuying.sn.data.local; + +/** + * 本地数据源(伪代码 / 待实现)。 + * + * 职责:统一封装所有本地存储读写,供 Repository 调用, + * 使 Repository 不感知具体存储实现(内存缓存 / 磁盘缓存 / SP / MMKV / 数据库)。 + * + * 当前 StatisticsRepository 直接使用 NetInterfaceManager.withMultiLevelCache + * 完成缓存;后续可将缓存读写、SP/MMKV 操作、业务副作用收敛到本类, + * 让 Repository 只做"取网络还是取本地"的决策。 + * + * 以下均为示意骨架,尚未实现具体逻辑。 + */ +public class LocalDataSource { + + // ========== 1. 内存 + 磁盘缓存(承接 RxCacheManager / CacheHelper)========== + // public T getCache(String key) { /* TODO */ return null; } + // public void saveCache(String key, T data, long expireMillis) { /* TODO */ } + // public void removeCache(String key) { /* TODO */ } + + // ========== 2. SharedPreferences 存储(承接 SPUtils)========== + // public void putString(String key, String value) { /* TODO */ } + // public String getString(String key, String def) { /* TODO */ return def; } + // public void putInt(String key, int value) { /* TODO */ } + // public int getInt(String key, int def) { /* TODO */ return def; } + + // ========== 3. MMKV 存储(承接 MMKV)========== + // public void putMMKV(String key, String value) { /* TODO */ } + // public String getMMKV(String key, String def) { /* TODO */ return def; } + + // ========== 4. 业务副作用收敛(原散落在 NetInterfaceManager.onNext / Activity 中)========== + // public void saveLoginState(int code) { /* TODO 原 SPUtils.put(isLogined, ...) */ } + // public void markControlState(int state) { /* TODO 原 Settings.System.putInt(KEY_IS_CONTROL, ...) */ } + + // ========== 5. 设备/统计信息缓存示例 ========== + // public void saveSnInfo(BaseResponse resp) { /* TODO */ } + // public BaseResponse getSnInfoCache() { /* TODO */ return null; } +} diff --git a/app/src/main/java/com/fuying/sn/data/model/AppControlDetail.java b/app/src/main/java/com/fuying/sn/data/model/AppControlDetail.java new file mode 100644 index 0000000..871ed88 --- /dev/null +++ b/app/src/main/java/com/fuying/sn/data/model/AppControlDetail.java @@ -0,0 +1,85 @@ +package com.fuying.sn.data.model; + +import androidx.annotation.NonNull; + +import com.fuying.sn.bean.PartTime; +import com.google.gson.Gson; +import com.google.gson.JsonParser; + +import java.io.Serializable; +import java.util.List; + +public class AppControlDetail implements Serializable { + private static final long serialVersionUID = 4052368488850538033L; + + String app_name; + String app_img; + int control_type; + int is_control; + int today_time_total; + int remaining_time; + + List time_period; + + public String getApp_name() { + return app_name; + } + + public void setApp_name(String app_name) { + this.app_name = app_name; + } + + public String getApp_img() { + return app_img; + } + + public void setApp_img(String app_img) { + this.app_img = app_img; + } + + public int getControl_type() { + return control_type; + } + + public void setControl_type(int control_type) { + this.control_type = control_type; + } + + public int getIs_control() { + return is_control; + } + + public void setIs_control(int is_control) { + this.is_control = is_control; + } + + public int getToday_time_total() { + return today_time_total; + } + + public void setToday_time_total(int today_time_total) { + this.today_time_total = today_time_total; + } + + public int getRemaining_time() { + return remaining_time; + } + + public void setRemaining_time(int remaining_time) { + this.remaining_time = remaining_time; + } + + public List getTime_period() { + return time_period; + } + + public void setTime_period(List time_period) { + this.time_period = time_period; + } + + @NonNull + @Override + public String toString() { + return JsonParser.parseString(new Gson().toJson(this)).getAsJsonObject().toString(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fuying/sn/data/model/AppUseTimeData.java b/app/src/main/java/com/fuying/sn/data/model/AppUseTimeData.java new file mode 100644 index 0000000..05e5f95 --- /dev/null +++ b/app/src/main/java/com/fuying/sn/data/model/AppUseTimeData.java @@ -0,0 +1,39 @@ +package com.fuying.sn.data.model; + +import androidx.annotation.NonNull; + +import com.google.gson.Gson; +import com.google.gson.JsonParser; + +import java.io.Serializable; +import java.util.List; + +public class AppUseTimeData implements Serializable { + private static final long serialVersionUID = -6067838846112447777L; + + long total_time; + + List app_list; + + public long getTotal_time() { + return total_time; + } + + public void setTotal_time(long total_time) { + this.total_time = total_time; + } + + public List getApp_list() { + return app_list; + } + + public void setApp_list(List app_list) { + this.app_list = app_list; + } + + @NonNull + @Override + public String toString() { + return JsonParser.parseString(new Gson().toJson(this)).getAsJsonObject().toString(); + } +} diff --git a/app/src/main/java/com/fuying/sn/data/model/AppUseTimeInfo.java b/app/src/main/java/com/fuying/sn/data/model/AppUseTimeInfo.java new file mode 100644 index 0000000..9601ca1 --- /dev/null +++ b/app/src/main/java/com/fuying/sn/data/model/AppUseTimeInfo.java @@ -0,0 +1,56 @@ +package com.fuying.sn.data.model; + +import androidx.annotation.NonNull; + +import com.google.gson.Gson; +import com.google.gson.JsonParser; + +import java.io.Serializable; + +public class AppUseTimeInfo implements Serializable { + private static final long serialVersionUID = 7957352661798901963L; + + String app_package; + String app_name; + long usage_time; + String app_img; + + + public String getApp_package() { + return app_package; + } + + public void setApp_package(String app_package) { + this.app_package = app_package; + } + + public String getApp_name() { + return app_name; + } + + public void setApp_name(String app_name) { + this.app_name = app_name; + } + + public long getUsage_time() { + return usage_time; + } + + public void setUsage_time(long usage_time) { + this.usage_time = usage_time; + } + + public String getApp_img() { + return app_img; + } + + public void setApp_img(String app_img) { + this.app_img = app_img; + } + + @NonNull + @Override + public String toString() { + return JsonParser.parseString(new Gson().toJson(this)).getAsJsonObject().toString(); + } +} diff --git a/app/src/main/java/com/fuying/sn/data/model/ControlStatusCombined.java b/app/src/main/java/com/fuying/sn/data/model/ControlStatusCombined.java new file mode 100644 index 0000000..ce25cc9 --- /dev/null +++ b/app/src/main/java/com/fuying/sn/data/model/ControlStatusCombined.java @@ -0,0 +1,27 @@ +package com.fuying.sn.data.model; + +import com.fuying.sn.bean.BaseResponse; + +import java.io.Serializable; + +/** + * zip 合并后的组合结果:管控状态 + 剩余时长。 + */ +public class ControlStatusCombined implements Serializable { + private static final long serialVersionUID = 1L; + + public final BaseResponse controlStatus; + public final BaseResponse remainingTime; + + public ControlStatusCombined(BaseResponse controlStatus, + BaseResponse remainingTime) { + this.controlStatus = controlStatus; + this.remainingTime = remainingTime; + } + + /** 两个请求都成功(code==200)才为 true */ + public boolean isBothSuccess() { + return controlStatus != null && controlStatus.code == 200 + && remainingTime != null && remainingTime.code == 200; + } +} diff --git a/app/src/main/java/com/fuying/sn/data/model/ControlStatusInfo.java b/app/src/main/java/com/fuying/sn/data/model/ControlStatusInfo.java new file mode 100644 index 0000000..7cd55a9 --- /dev/null +++ b/app/src/main/java/com/fuying/sn/data/model/ControlStatusInfo.java @@ -0,0 +1,78 @@ +package com.fuying.sn.data.model; + +import androidx.annotation.NonNull; + +import com.fuying.sn.bean.PartTime; +import com.google.gson.Gson; +import com.google.gson.JsonParser; + +import java.io.Serializable; +import java.util.List; + +public class ControlStatusInfo implements Serializable { + private static final long serialVersionUID = -4241643521947293549L; + + String sn; + /** + * 0 默认 1 限制时长 2 时间段限制 + */ + int control_type; + int work_time; + int rest_time; + int is_week_end; + List time_period; + + public String getSn() { + return sn; + } + + public void setSn(String sn) { + this.sn = sn; + } + + public int getControl_type() { + return control_type; + } + + public void setControl_type(int control_type) { + this.control_type = control_type; + } + + public int getWork_time() { + return work_time; + } + + public void setWork_time(int work_time) { + this.work_time = work_time; + } + + public int getRest_time() { + return rest_time; + } + + public void setRest_time(int rest_time) { + this.rest_time = rest_time; + } + + public int getIs_week_end() { + return is_week_end; + } + + public void setIs_week_end(int is_week_end) { + this.is_week_end = is_week_end; + } + + public List getTime_period() { + return time_period; + } + + public void setTime_period(List time_period) { + this.time_period = time_period; + } + + @NonNull + @Override + public String toString() { + return JsonParser.parseString(new Gson().toJson(this)).getAsJsonObject().toString(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fuying/sn/data/model/RemainingTimeBean.java b/app/src/main/java/com/fuying/sn/data/model/RemainingTimeBean.java new file mode 100644 index 0000000..d7ed689 --- /dev/null +++ b/app/src/main/java/com/fuying/sn/data/model/RemainingTimeBean.java @@ -0,0 +1,37 @@ +package com.fuying.sn.data.model; + +import androidx.annotation.NonNull; + +import com.google.gson.Gson; +import com.google.gson.JsonParser; + +import java.io.Serializable; + +public class RemainingTimeBean implements Serializable { + private static final long serialVersionUID = 5470771600877982771L; + + String sn; + long remaining_time; + + public String getSn() { + return sn; + } + + public void setSn(String sn) { + this.sn = sn; + } + + public long getRemaining_time() { + return remaining_time; + } + + public void setRemaining_time(long remaining_time) { + this.remaining_time = remaining_time; + } + + @NonNull + @Override + public String toString() { + return JsonParser.parseString(new Gson().toJson(this)).getAsJsonObject().toString(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fuying/sn/data/repository/StatisticsRepository.java b/app/src/main/java/com/fuying/sn/data/repository/StatisticsRepository.java new file mode 100644 index 0000000..13f6650 --- /dev/null +++ b/app/src/main/java/com/fuying/sn/data/repository/StatisticsRepository.java @@ -0,0 +1,117 @@ +package com.fuying.sn.data.repository; + +import com.fuying.sn.bean.AppTimeControlInfo; +import com.fuying.sn.bean.BaseResponse; +import com.fuying.sn.bean.SystemSettingsSet; +import com.fuying.sn.data.model.AppControlDetail; +import com.fuying.sn.data.model.AppUseTimeData; +import com.fuying.sn.data.model.AppUseTimeInfo; +import com.fuying.sn.data.model.ControlStatusCombined; +import com.fuying.sn.data.model.ControlStatusInfo; +import com.fuying.sn.data.model.RemainingTimeBean; +import com.fuying.sn.network.NetInterfaceManager; +import com.fuying.sn.network.UrlAddress; +import com.fuying.sn.utils.Utils; +import com.google.gson.reflect.TypeToken; + +import java.lang.reflect.Type; +import java.util.List; + +import io.reactivex.rxjava3.core.Observable; + +/** + * 使用统计数据仓库(Repository)。 + *

+ * 职责:作为使用统计相关数据的唯一入口,向 ViewModel 屏蔽数据来源细节 + * (网络 / 缓存),并统一套用 NetInterfaceManager 提供的多级缓存策略。 + *

+ * 类型策略:每个方法的返回类型与 StatisticsApi 接口完全一致并带具体泛型, + * 这样 withMultiLevelCache 才能精确反序列化缓存,且调用方拿到强类型、无需强转。 + * 暂无具体 Bean 的接口暂用 BaseResponse 过渡,有 Bean 时直接替换即可。 + */ +public class StatisticsRepository { + + // 缓存过期时间:3秒钟(NetInterfaceManager 内部常量为 private,这里自定义) + private static final long CACHE_EXPIRE_ONE_MINUTE = 3 * 1000L; + + private static volatile StatisticsRepository INSTANCE; + + public static StatisticsRepository getInstance() { + if (INSTANCE == null) { + synchronized (StatisticsRepository.class) { + if (INSTANCE == null) { + INSTANCE = new StatisticsRepository(); + } + } + } + return INSTANCE; + } + + private StatisticsRepository() { + // 单例:依赖 NetInterfaceManager(已初始化的单例)提供 Api 与缓存能力 + } + + public Observable> getSetting() { + return NetInterfaceManager.withMultiLevelCache( + NetInterfaceManager.getInstance().getSettingsApi().getSetting(Utils.getSerial()), + UrlAddress.GET_SETTINGS, + new TypeToken>() { + }.getType(), + CACHE_EXPIRE_ONE_MINUTE); + } + + public Observable> getControlStatus() { + return NetInterfaceManager.withMultiLevelCache( + NetInterfaceManager.getInstance().getStatisticsApi().getControlStatus(Utils.getSerial()), + UrlAddress.CONTROL_STATUS, + new TypeToken>() { + }.getType(), + CACHE_EXPIRE_ONE_MINUTE); + } + + public Observable> getRemainingTime() { + return NetInterfaceManager.withMultiLevelCache( + NetInterfaceManager.getInstance().getStatisticsApi().getRemainingTime(Utils.getSerial()), + UrlAddress.REMAINING_TIME, + new TypeToken>() { + }.getType(), + CACHE_EXPIRE_ONE_MINUTE); + } + + /** + * 将管控状态(controlStatus)与剩余时长(remainingTime)两个请求 zip 合并, + * 两个都返回后一次性给出组合结果,便于上层根据两个值联动判断。 + * 任意一方网络失败都会触发 onError,上层按整体失败处理即可。 + */ + public Observable getControlStatusCombined() { + return Observable.zip( + getControlStatus(), + getRemainingTime(), + (statusResp, remainResp) -> new ControlStatusCombined(statusResp, remainResp) + ); + } + + /** + * 获取应用使用记录 + * + * @param query_type 查询类型 1 今天 2 本周 3 本月 + * @param is_all 0 最高时长的6条 ,1 全部有使用的记录 + */ + public Observable> getAppUseTime(int query_type, int is_all) { + return NetInterfaceManager.withMultiLevelCache( + NetInterfaceManager.getInstance().getStatisticsApi().getAppUseTime(Utils.getSerial(), query_type, is_all), + UrlAddress.APP_USE_TIME, + new TypeToken>() { + }.getType(), + CACHE_EXPIRE_ONE_MINUTE); + } + + public Observable>> getAppControlDetail() { + return NetInterfaceManager.withMultiLevelCache( + NetInterfaceManager.getInstance().getStatisticsApi().getAppControlDetail(Utils.getSerial()), + UrlAddress.APP_CONTROL_DETAIL, + new TypeToken>>() { + }.getType(), + CACHE_EXPIRE_ONE_MINUTE); + } +} diff --git a/app/src/main/java/com/fuying/sn/fragment/device/DeviceFragment.java b/app/src/main/java/com/fuying/sn/fragment/device/DeviceFragment.java index ddbc492..786defa 100644 --- a/app/src/main/java/com/fuying/sn/fragment/device/DeviceFragment.java +++ b/app/src/main/java/com/fuying/sn/fragment/device/DeviceFragment.java @@ -18,6 +18,7 @@ import com.fuying.sn.R; import com.fuying.sn.activity.update.UpdateActivity; import com.fuying.sn.base.mvvm.fragment.BaseMvvmFragment; import com.fuying.sn.bean.AppInfo; +import com.fuying.sn.bean.DeviceInfo; import com.fuying.sn.bean.SnInfo; import com.fuying.sn.config.CommonConfig; import com.fuying.sn.databinding.FragmentDeviceBinding; @@ -88,19 +89,36 @@ public class DeviceFragment extends BaseMvvmFragment() { + @Override + public void onChanged(DeviceInfo deviceInfo) { + if (deviceInfo != null) { + if (!TextUtils.isEmpty(deviceInfo.getBind_time())) { + mViewDataBinding.tvBindTime.setText(deviceInfo.getBind_time()); + } else { + mViewDataBinding.tvBindTime.setText("获取失败"); + } + if (!TextUtils.isEmpty(deviceInfo.getParent_mobile())) { + mViewDataBinding.tvPhoneNumber.setText(deviceInfo.getParent_mobile()); + } else { + mViewDataBinding.tvPhoneNumber.setText("获取失败"); + } + } else { + mViewDataBinding.tvBindTime.setText("未绑定"); + mViewDataBinding.tvPhoneNumber.setText("未绑定"); + } + } + }); + + mViewModel.getDeviceInfo(); mViewModel.mAppInfoMutableLiveData.observe(this, new Observer() { @Override public void onChanged(AppInfo appInfo) { diff --git a/app/src/main/java/com/fuying/sn/fragment/device/DeviceViewModel.java b/app/src/main/java/com/fuying/sn/fragment/device/DeviceViewModel.java index f55fc89..66510de 100644 --- a/app/src/main/java/com/fuying/sn/fragment/device/DeviceViewModel.java +++ b/app/src/main/java/com/fuying/sn/fragment/device/DeviceViewModel.java @@ -8,6 +8,7 @@ import com.fuying.sn.BuildConfig; import com.fuying.sn.base.mvvm.BaseViewModel; import com.fuying.sn.bean.AppInfo; import com.fuying.sn.bean.BaseResponse; +import com.fuying.sn.bean.DeviceInfo; import com.fuying.sn.bean.SnInfo; import com.fuying.sn.databinding.FragmentDeviceBinding; import com.fuying.sn.network.NetInterfaceManager; @@ -64,6 +65,38 @@ public class DeviceViewModel extends BaseViewModel mDeviceInfoMutableLiveData = new MutableLiveData<>(); + + public void getDeviceInfo() { + NetInterfaceManager.getInstance().getDeviceInfoObservable() + .subscribe(new Observer>() { + @Override + public void onSubscribe(@NonNull Disposable d) { + + } + + @Override + public void onNext(BaseResponse baseResponse) { + if (baseResponse.code == 200) { + DeviceInfo deviceInfo = baseResponse.data; + mDeviceInfoMutableLiveData.setValue(deviceInfo); + }else { + mDeviceInfoMutableLiveData.setValue(null); + } + } + + @Override + public void onError(@NonNull Throwable e) { + + } + + @Override + public void onComplete() { + + } + }); + } + public MutableLiveData mAppInfoMutableLiveData = new MutableLiveData<>(); public void checkUpdate(boolean response) { diff --git a/app/src/main/java/com/fuying/sn/fragment/usage/UsageFragment.java b/app/src/main/java/com/fuying/sn/fragment/usage/UsageFragment.java index 7a5fe2c..31b55fd 100644 --- a/app/src/main/java/com/fuying/sn/fragment/usage/UsageFragment.java +++ b/app/src/main/java/com/fuying/sn/fragment/usage/UsageFragment.java @@ -1,20 +1,95 @@ package com.fuying.sn.fragment.usage; import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.RectF; import android.os.Bundle; +import android.text.TextUtils; +import android.view.View; +import android.widget.TextView; + +import androidx.core.content.ContextCompat; +import androidx.lifecycle.Observer; +import androidx.recyclerview.widget.LinearLayoutManager; import com.fuying.sn.R; +import com.fuying.sn.activity.usage.UsageDetailActivity; +import com.fuying.sn.adapter.AppControlDetailAdapter; import com.fuying.sn.base.mvvm.fragment.BaseMvvmFragment; +import com.fuying.sn.bean.BaseResponse; +import com.fuying.sn.bean.SystemSettings; +import com.fuying.sn.bean.SystemSettingsSet; import com.fuying.sn.config.CommonConfig; +import com.fuying.sn.data.model.AppControlDetail; +import com.fuying.sn.data.model.AppUseTimeData; +import com.fuying.sn.data.model.AppUseTimeInfo; +import com.fuying.sn.data.model.ControlStatusCombined; +import com.fuying.sn.data.model.ControlStatusInfo; +import com.fuying.sn.data.model.RemainingTimeBean; import com.fuying.sn.databinding.FragmentUsageBinding; +import com.fuying.sn.manager.AmapManager; +import com.fuying.sn.utils.ScreenUtils; +import com.fuying.sn.utils.TimeUtils; +import com.fuying.sn.utils.ToastUtil; +import com.fuying.sn.utils.Utils; +import com.github.mikephil.charting.charts.HorizontalBarChart; +import com.github.mikephil.charting.components.XAxis; +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.data.BarData; +import com.github.mikephil.charting.data.BarDataSet; +import com.github.mikephil.charting.data.BarEntry; +import com.github.mikephil.charting.formatter.ValueFormatter; +import com.github.mikephil.charting.interfaces.datasets.IBarDataSet; +import com.jeremyliao.liveeventbus.LiveEventBus; import com.tencent.mmkv.MMKV; +import net.lucode.hackware.magicindicator.MagicIndicator; +import net.lucode.hackware.magicindicator.buildins.commonnavigator.CommonNavigator; +import net.lucode.hackware.magicindicator.buildins.commonnavigator.abs.CommonNavigatorAdapter; +import net.lucode.hackware.magicindicator.buildins.commonnavigator.abs.IPagerIndicator; +import net.lucode.hackware.magicindicator.buildins.commonnavigator.abs.IPagerTitleView; +import net.lucode.hackware.magicindicator.buildins.commonnavigator.model.PositionData; +import net.lucode.hackware.magicindicator.buildins.commonnavigator.titles.SimplePagerTitleView; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + public class UsageFragment extends BaseMvvmFragment { private static final String TAG = "UsageFragment"; private Activity mContext; - private MMKV mMMKV = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE); + private HorizontalBarChart mBarChart; + private TextView mTvTotalTime; + private MagicIndicator mMagicIndicator; + private int mSelectedTab = 0; + + private int mQueryType = 1; + private boolean mIsAll = false; + + private AppControlDetailAdapter mAppControlDetailAdapter; + + // 一次 loadData 周期内登记的请求集合,用于判断整体是否全部请求完成。 + // 取代原先硬编码的 TOTAL_REQUESTS,因为实际请求数量取决于 onSettingData 的结果 + // (限制场景会额外发出 controlStatus / remainingTime / appControlDetail 三个请求)。 + private static final String REQ_SETTING = "setting"; + private static final String REQ_USE_TIME = "useTime"; + private static final String REQ_CONTROL_STATUS = "controlStatus"; + private static final String REQ_REMAINING_TIME = "remainingTime"; + private static final String REQ_APP_CONTROL_DETAIL = "appControlDetail"; + + private final Set mPendingRequests = new HashSet<>(); + // 本次 loadData 周期登记的总请求数(含根据 onSettingData 动态补充的请求), + // 用于判断整体是否全部成功 + private int mTotalRequests = 0; + private int mSuccessCount = 0; @Override protected int getLayoutId() { @@ -32,20 +107,553 @@ public class UsageFragment extends BaseMvvmFragment() { + @Override + public void onChanged(String s) { + if (!TextUtils.isEmpty(s)) { + mViewDataBinding.tvAddress.setText(s); + } + } + }); + mViewModel.settingData.observe(this, this::onSettingData); + mViewModel.controlCombined.observe(getViewLifecycleOwner(), this::onControlCombined); + mViewModel.appUseTime.observe(getViewLifecycleOwner(), this::onAppUseTime); + mViewModel.mAppInfosLiveData.observe(getViewLifecycleOwner(), this::onAppControlDetail); + mViewModel.loading.observe(getViewLifecycleOwner(), this::onLoading); + mViewModel.error.observe(getViewLifecycleOwner(), this::onError); + + loadData(); + } + + + private void loadData() { + // 重置请求登记,统计本次 loadData 是否全部请求成功。 + // 先登记一定会发出的两个请求:设置信息与使用时长。 + mPendingRequests.clear(); + mSuccessCount = 0; + mPendingRequests.add(REQ_SETTING); + mPendingRequests.add(REQ_USE_TIME); + mTotalRequests = mPendingRequests.size(); + mViewModel.loadSettingData(); + loadAppUseTime(); + AmapManager.getInstance().startLocation(); + } + + /** + * 登记一个整体请求。仅 loadData 周期内发出的请求需要登记, + * 由 onSettingData 根据数据动态补充登记额外请求。 + */ + private void registerRequest(String tag) { + if (mPendingRequests.add(tag)) { + mTotalRequests++; + } + } + + /** + * 统计本次 loadData 内各请求的完成情况。 + * 当全部已登记的请求都完成且都成功时,保存当前时间戳到 MMKV 并刷新同步时间显示。 + * 注意:本方法仅用于 loadData 周期登记的请求;切换 Tab / fetchData 触发的单独 + * loadAppUseTime 不会登记,因此不会计入整体同步时间。 + */ + private void onRequestFinished(String tag, boolean success) { + if (!mPendingRequests.remove(tag)) { + // 该请求未被登记(如非 loadData 触发的单独请求),不计入整体同步时间 + return; + } + if (success) { + mSuccessCount++; + } + if (mPendingRequests.isEmpty()) { + // 所有已登记的请求都已完成,全部成功才算本次同步成功 + if (mSuccessCount >= mTotalRequests) { + long now = System.currentTimeMillis(); + mMMKV.encode(CommonConfig.LAST_SYNC_TIME_KEY, now); + updateLastSyncTime(); + } + mSuccessCount = 0; + } + } + + /** + * 读取 MMKV 中缓存的同步时间戳并展示到 tv_last_sync_time。 + */ + private void updateLastSyncTime() { + long ts = mMMKV.decodeLong(CommonConfig.LAST_SYNC_TIME_KEY, 0); + if (ts > 0) { + mViewDataBinding.tvLastSyncTime.setText("最新数据同步时间:" + TimeUtils.transferLongToDate(ts)); + } else { + mViewDataBinding.tvLastSyncTime.setText("最新数据同步时间:暂无"); + } + } + + private void onSettingData(BaseResponse systemSettingsSetBaseResponse) { + onRequestFinished(REQ_SETTING, systemSettingsSetBaseResponse.code == 200); + if (systemSettingsSetBaseResponse.code == 200) { + SystemSettings systemSettings = systemSettingsSetBaseResponse.data.getUser(); + int is_control = systemSettings.getIs_control(); + if (is_control == 1) { + mViewDataBinding.tvTodayTime.setText(""); + mViewDataBinding.tvRemainingTime.setText(""); + mViewDataBinding.tvNoData.setVisibility(View.GONE); + mViewDataBinding.recyclerView.setVisibility(View.VISIBLE); + + // 限制场景下动态补充登记额外请求,使其计入整体同步时间统计 + registerRequest(REQ_CONTROL_STATUS); + registerRequest(REQ_REMAINING_TIME); + registerRequest(REQ_APP_CONTROL_DETAIL); + mViewModel.loadControlCombined(); + mViewModel.loadAppControlDetail(); + } else { + mViewDataBinding.tvControlType.setText("不限制"); + mViewDataBinding.tvControlType.setBackground(mContext.getResources().getDrawable(R.drawable.tv_time_unlimited_background)); + mViewDataBinding.tvTodayTime.setText("当前pad无管控限制"); + mViewDataBinding.tvRemainingTime.setText(""); + mViewDataBinding.tvNoData.setVisibility(View.VISIBLE); + mViewDataBinding.recyclerView.setVisibility(View.GONE); + } + } + } + + /** + * controlStatus 与 remainingTime 经 zip 合并后的统一回调。 + * 此时两个结果都已就绪,可安全根据 control_type 与 remaining_time 组合判断。 + */ + private void onControlCombined(ControlStatusCombined combined) { + boolean statusOk = combined.controlStatus != null && combined.controlStatus.code == 200; + boolean remainOk = combined.remainingTime != null && combined.remainingTime.code == 200; + onRequestFinished(REQ_CONTROL_STATUS, statusOk); + onRequestFinished(REQ_REMAINING_TIME, remainOk); + + if (!statusOk) { + // 管控状态获取失败:整体置为“不支持” + mViewDataBinding.tvControlType.setText("不支持"); + mViewDataBinding.tvControlType.setBackground(mContext.getResources().getDrawable(R.drawable.tv_time_disable_background)); + mViewDataBinding.tvTodayTime.setText("无可用时间"); + mViewDataBinding.tvRemainingTime.setText(""); + return; + } + + ControlStatusInfo info = combined.controlStatus.data; + // 根据 control_type 与 remaining_time 组合判断 + int type = info.getControl_type(); + long remain = remainOk && combined.remainingTime.data != null + ? combined.remainingTime.data.getRemaining_time() : -1; + + if (type == 2) { + mViewDataBinding.tvTodayTime.setText(TimeUtils.getPartTimeStringLine(info.getTime_period())); + } else { + // 每日可用时长文案(与星期相关) + if (TimeUtils.inWeekDay()) { + mViewDataBinding.tvTodayTime.setText("每天" + TimeUtils.formatTime(info.getRest_time())); + } else { + mViewDataBinding.tvTodayTime.setText("每天" + TimeUtils.formatTime(info.getWork_time())); + } + } + + switch (type) { + case 0: + mViewDataBinding.tvControlType.setText("不限制"); + mViewDataBinding.tvControlType.setBackground(mContext.getResources().getDrawable(R.drawable.tv_time_unlimited_background)); + mViewDataBinding.tvRemainingTime.setText(""); + + mViewDataBinding.tvTodayTime.setText("不限制整机时长"); + break; + case 1: // 限制时长:根据剩余时间组合判断 + mViewDataBinding.tvControlType.setText("限制时长"); + mViewDataBinding.tvControlType.setBackground(mContext.getResources().getDrawable(R.drawable.tv_time_duration_background)); + if (remainOk && remain > 0) { + mViewDataBinding.tvRemainingTime.setText("剩余" + TimeUtils.formatTime(remain)); + } else if (remainOk && remain == 0) { + mViewDataBinding.tvRemainingTime.setText("已用尽"); + } else { + mViewDataBinding.tvRemainingTime.setText(""); + } + break; + case 2: // 限制时间段:按需求不展示剩余时长 + mViewDataBinding.tvControlType.setText("限制时间段"); + mViewDataBinding.tvControlType.setBackground(mContext.getResources().getDrawable(R.drawable.tv_time_period_background)); + mViewDataBinding.tvRemainingTime.setText(""); + break; + default: + mViewDataBinding.tvControlType.setText("不限制"); + mViewDataBinding.tvControlType.setBackground(mContext.getResources().getDrawable(R.drawable.tv_time_unlimited_background)); + mViewDataBinding.tvRemainingTime.setText(""); + } + } + + private void onAppControlDetail(BaseResponse> response) { + onRequestFinished(REQ_APP_CONTROL_DETAIL, response.code == 200); + if (response.code == 200) { + List appTimeControlInfos = response.data; + mAppControlDetailAdapter.setAppTimeControlInfos(appTimeControlInfos); + } else { + mAppControlDetailAdapter.setAppTimeControlInfos(null); + } + } + + private void initChart() { + // 关闭图表右下角的描述文字(标题说明) + mBarChart.getDescription().setEnabled(false); + // 没有数据显示的提示文字 + mBarChart.setNoDataText("没有数据"); + // 设置每条柱子的数值显示 + mBarChart.setDrawValueAboveBar(true); + // 不绘制图表背景网格的填充底色 + mBarChart.setDrawGridBackground(false); + // 不绘制图表边框 + mBarChart.setDrawBorders(false); + // 禁用图表整体的自由缩放(拖拽缩放) + mBarChart.setScaleEnabled(false); + //分别在x轴和y轴上进行缩放 + mBarChart.setPinchZoom(false); + //禁用双击缩放 + mBarChart.setDoubleTapToZoomEnabled(false); + // 禁用图例(避免显示多余的系列说明) + mBarChart.getLegend().setEnabled(false); + // 让柱子自动适配 X 轴范围,使两端不留空隙 + mBarChart.setFitBars(true); + // 使用 setViewPortOffsets 固定绘图区域偏移,防止起始轴(0刻度线)随标签长度变化而抖动 + // 参数依次为:左、上、右、下(单位 px)。左侧预留足够空间显示应用名,底部预留空间显示 Y 轴刻度。 + mBarChart.setViewPortOffsets(ScreenUtils.dp2px(80f), ScreenUtils.dp2px(8f), ScreenUtils.dp2px(12f), ScreenUtils.dp2px(28f)); + // 设置图表四周额外留白(左/上/右/下,dp 转 px),避免标签被边缘裁剪 + mBarChart.setExtraOffsets(ScreenUtils.dp2px(4f), ScreenUtils.dp2px(8f), ScreenUtils.dp2px(4f), ScreenUtils.dp2px(8f)); + + // 获取 X 轴(分类轴)对象,用于后续配置 + XAxis xAxis = mBarChart.getXAxis(); + // 横向柱状图的分类轴在左侧垂直显示,避免与底部数值刻度重叠 + xAxis.setPosition(XAxis.XAxisPosition.BOTTOM); + // 不绘制 X 轴线 + xAxis.setDrawAxisLine(false); + // 不绘制 X 轴方向的网格线 + xAxis.setDrawGridLines(false); + // 设置 X 轴刻度最小间隔为 1,避免标签重复/重叠 + xAxis.setGranularity(1f); + // 设置 X 轴分类标签文字大小 + xAxis.setTextSize(ScreenUtils.sp2px(mContext, 10f)); + // 设置 X 轴分类标签文字颜色为灰色(#666666) + xAxis.setTextColor(Color.parseColor("#666666")); + + // 获取左侧 Y 轴(数值轴)对象,用于后续配置 + YAxis leftAxis = mBarChart.getAxisLeft(); + // 锁定数值轴最小值恒为 0,确保 0 刻度始终贴着绘图区左边缘 + leftAxis.setAxisMinimum(0f); + // 底部数值轴:绘制轴线、网格刻度和标签,作为柱状图底部的刻度尺 + leftAxis.setDrawAxisLine(true); + // 设置左侧 Y 轴轴线颜色为浅灰(#CCCCCC) + leftAxis.setAxisLineColor(Color.parseColor("#CCCCCC")); + // 设置左侧 Y 轴轴线宽度为 0.5(细线) + leftAxis.setAxisLineWidth(0.5f); + // 绘制左侧 Y 轴方向的网格线 + leftAxis.setDrawGridLines(true); + // 设置网格线颜色为浅灰(#EEEEEE) + leftAxis.setGridColor(Color.parseColor("#EEEEEE")); + // 缩小刻度线宽度,使刻度更细 + leftAxis.setGridLineWidth(0.5f); + // 缩小刻度文字,让底部刻度更紧凑 + leftAxis.setTextSize(ScreenUtils.sp2px(mContext, 10f)); + // 设置左侧 Y 轴刻度文字颜色为浅灰(#999999) + leftAxis.setTextColor(Color.parseColor("#999999")); + // 设置数值格式化器,将刻度值按"小时"格式显示 + leftAxis.setValueFormatter(new HourAxisValueFormatter()); + + // 禁用右侧 Y 轴(右侧不显示数值轴) + mBarChart.getAxisRight().setEnabled(false); + } + + private void initTabs() { + mMagicIndicator = mViewDataBinding.magicIndicator; + final String[] titles = { + getString(R.string.usage_today), + getString(R.string.usage_week), + getString(R.string.usage_month) + }; + CommonNavigator commonNavigator = new CommonNavigator(mContext); + commonNavigator.setAdjustMode(false); + commonNavigator.setAdapter(new CommonNavigatorAdapter() { + @Override + public int getCount() { + return titles.length; + } + + @Override + public IPagerTitleView getTitleView(Context context, final int index) { + SimplePagerTitleView titleView = new SimplePagerTitleView(context); + titleView.setText(titles[index]); + titleView.setTextSize(ScreenUtils.sp2px(mContext, 12)); + titleView.setNormalColor(ContextCompat.getColor(mContext, R.color.gray)); + titleView.setSelectedColor(Color.WHITE); + titleView.setOnClickListener(v -> selectTab(index)); + return titleView; + } + + @Override + public IPagerIndicator getIndicator(Context context) { + return new RoundedRectIndicator(context, + ContextCompat.getColor(mContext, R.color.default_blue), + ScreenUtils.dp2px(16f), + ScreenUtils.dp2px(12f), + ScreenUtils.dp2px(4f)); + } + }); + mMagicIndicator.setNavigator(commonNavigator); + } + + private void selectTab(int position) { + mSelectedTab = position; + CommonNavigator navigator = (CommonNavigator) mMagicIndicator.getNavigator(); + if (navigator != null) { + navigator.onPageSelected(position); + navigator.notifyDataSetChanged(); + } + + int newQueryType = position + 1; + if (mQueryType != newQueryType) { + mQueryType = newQueryType; + loadAppUseTime(); + } + } + + + private void loadAppUseTime() { + // Tab 切换 / fetchData 触发的单独请求,不计入 loadData 整体同步时间 + mViewModel.loadAppUseTime(mQueryType, mIsAll ? 1 : 0); + } + + private void onAppUseTime(BaseResponse resp) { + onRequestFinished(REQ_USE_TIME, resp != null && resp.code == 200 + && resp.data != null && !resp.data.getApp_list().isEmpty()); + if (resp == null || resp.data == null || resp.data.getApp_list().isEmpty()) { + // 数据为空时清空图表已有内容 + mBarChart.clear(); + // 强制刷新图表(触发重绘,使清空生效) + mBarChart.invalidate(); + mTvTotalTime.setText(formatTotalTime(0)); + return; + } + + List list = resp.data.getApp_list(); + Collections.sort(list, (a, b) -> Long.compare(b.getUsage_time(), a.getUsage_time())); + + List displayList = mIsAll ? list : list.subList(0, Math.min(6, list.size())); + // 横向柱状图默认从底部向上递增,升序排列让最长的柱子显示在最上方 + Collections.sort(displayList, (a, b) -> Long.compare(a.getUsage_time(), b.getUsage_time())); + + long totalSeconds = 0; + for (AppUseTimeInfo info : list) { + totalSeconds += info.getUsage_time(); + } + mTvTotalTime.setText(formatTotalTime(totalSeconds)); + + List labels = new ArrayList<>(); + List entries = new ArrayList<>(); + float maxHours = 0f; + for (int i = 0; i < displayList.size(); i++) { + AppUseTimeInfo info = displayList.get(i); + labels.add(info.getApp_name()); + float hours = secondsToHours(info.getUsage_time()); + entries.add(new BarEntry(i, hours)); + if (hours > maxHours) { + maxHours = hours; + } + } + + // 创建柱状图数据集(第二个参数为图例名称,留空) + BarDataSet dataSet = new BarDataSet(entries, ""); + // 不在柱子顶部绘制具体数值文本 + dataSet.setDrawValues(false); + // 设置柱子填充颜色为绿色(#1DBF73) + dataSet.setColor(Color.parseColor("#1DBF73")); + // 设置选中高亮时的颜色(同样为绿色) + dataSet.setHighLightColor(Color.parseColor("#1DBF73")); + + List dataSets = new ArrayList<>(); + dataSets.add(dataSet); + // 用数据集构建柱状图数据对象 + BarData data = new BarData(dataSets); + // 设置柱子宽度为 0.5(占单个刻度区间的一半,留出间隙) + data.setBarWidth(0.5f); + // 将构建好的数据设置给图表 + mBarChart.setData(data); + + // 获取 X 轴,配置分类标签显示 + XAxis xAxis = mBarChart.getXAxis(); + // 使用自定义格式化器,将索引映射为应用名称标签 + xAxis.setValueFormatter(new AppNameValueFormatter(labels)); + // 按标签数量均分 X 轴刻度(不强制等距,避免标签被压缩) + xAxis.setLabelCount(labels.size(), false); + + // 获取左侧 Y 轴,配置数值范围与刻度 + YAxis leftAxis = mBarChart.getAxisLeft(); + // 计算 Y 轴上限:取最大小时数向上取整,且至少显示到 1 + float axisMax = Math.max((float) Math.ceil(maxHours), 1f); + leftAxis.setAxisMaximum(axisMax); + // 减少刻度数量,使底部刻度尺更稀疏、更小 + int labelCount = Math.min(4, Math.max((int) axisMax + 1, 2)); + leftAxis.setLabelCount(labelCount, true); + + // 通知图表数据已更新,重新绘制 + mBarChart.invalidate(); + } + + private void onLoading(Boolean visible) { + // 页面暂无 loading 视图,可按需要扩展 + } + + private void onError(Throwable e) { + ToastUtil.show(e != null && e.getMessage() != null ? e.getMessage() : getString(R.string.open_app_fail)); } @Override public void fetchData() { - + loadAppUseTime(); } public class BtnClick { + public void onViewAllClick(View view) { + startActivity(new Intent(getActivity(), UsageDetailActivity.class)); + } + public void onRefreshClick(View view) { + loadData(); + } + } + + /** + * 服务端返回的 usage_time 单位为秒,这里统一换算成小时。 + */ + private static float secondsToHours(long seconds) { + return seconds / 3600f; + } + + private static String formatTotalTime(long totalSeconds) { + if (totalSeconds <= 0) { + return "0" + "分钟"; + } + long hours = totalSeconds / 3600; + long minutes = (totalSeconds % 3600) / 60; + if (hours > 0 && minutes > 0) { + return hours + "小时" + minutes + "分钟"; + } else if (hours > 0) { + return hours + "小时"; + } else { + return minutes + "分钟"; + } + } + + private static class HourAxisValueFormatter extends ValueFormatter { + @Override + public String getFormattedValue(float value) { + return (int) value + "小时"; + } + } + + private static class AppNameValueFormatter extends ValueFormatter { + private final List labels; + + AppNameValueFormatter(List labels) { + this.labels = labels; + } + + @Override + public String getFormattedValue(float value) { + int index = (int) value; + if (index < 0 || index >= labels.size()) { + return ""; + } + String name = labels.get(index); + return name.length() > 8 ? name.substring(0, 8) + "..." : name; + } + } + + /** + * 圆角矩形指示器:在选中标题(或滑动过程中)背后绘制一个带圆角的矩形高亮背景。 + */ + private static class RoundedRectIndicator extends View implements IPagerIndicator { + private final Paint mPaint; + private final RectF mRect = new RectF(); + private final int mRoundRadius; + private final int mPaddingHorizontal; + private final int mPaddingVertical; + private List mPositionDataList; + private int mSelectedPosition = 0; + + RoundedRectIndicator(Context context, int color, int roundRadius, + int paddingHorizontal, int paddingVertical) { + super(context); + mRoundRadius = roundRadius; + mPaddingHorizontal = paddingHorizontal; + mPaddingVertical = paddingVertical; + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mPaint.setStyle(Paint.Style.FILL); + mPaint.setColor(color); + } + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + mSelectedPosition = position; + updateRect(position, positionOffset); + } + + @Override + public void onPageSelected(int position) { + mSelectedPosition = position; + updateRect(position, 0f); + } + + @Override + public void onPageScrollStateChanged(int state) { + } + + @Override + public void onPositionDataProvide(List dataList) { + mPositionDataList = dataList; + // 布局完成后位置数据就绪,重新计算当前选中态,保证首屏也能正确绘制 + updateRect(mSelectedPosition, 0f); + } + + private void updateRect(int position, float offset) { + if (mPositionDataList == null || mPositionDataList.isEmpty()) { + return; + } + position = Math.max(0, Math.min(position, mPositionDataList.size() - 1)); + PositionData current = mPositionDataList.get(position); + PositionData next = mPositionDataList.get(Math.min(position + 1, mPositionDataList.size() - 1)); + int left = (int) (current.mContentLeft + (next.mContentLeft - current.mContentLeft) * offset) - mPaddingHorizontal; + int right = (int) (current.mContentRight + (next.mContentRight - current.mContentRight) * offset) + mPaddingHorizontal; + int top = (int) (current.mContentTop + (next.mContentTop - current.mContentTop) * offset) - mPaddingVertical; + int bottom = (int) (current.mContentBottom + (next.mContentBottom - current.mContentBottom) * offset) + mPaddingVertical; + mRect.set(left, top, right, bottom); + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + canvas.drawRoundRect(mRect, mRoundRadius, mRoundRadius, mPaint); + } } } diff --git a/app/src/main/java/com/fuying/sn/fragment/usage/UsageViewModel.java b/app/src/main/java/com/fuying/sn/fragment/usage/UsageViewModel.java index 5c8a28a..4281bc4 100644 --- a/app/src/main/java/com/fuying/sn/fragment/usage/UsageViewModel.java +++ b/app/src/main/java/com/fuying/sn/fragment/usage/UsageViewModel.java @@ -1,18 +1,192 @@ package com.fuying.sn.fragment.usage; +import android.util.Log; + +import androidx.lifecycle.MutableLiveData; + import com.fuying.sn.base.mvvm.BaseViewModel; +import com.fuying.sn.bean.BaseResponse; +import com.fuying.sn.bean.SystemSettings; +import com.fuying.sn.bean.SystemSettingsSet; +import com.fuying.sn.data.model.AppControlDetail; +import com.fuying.sn.data.model.AppUseTimeData; +import com.fuying.sn.data.model.ControlStatusCombined; +import com.fuying.sn.data.model.ControlStatusInfo; +import com.fuying.sn.data.model.RemainingTimeBean; +import com.fuying.sn.data.repository.StatisticsRepository; import com.fuying.sn.databinding.FragmentUsageBinding; +import com.hjq.toast.Toaster; +import com.trello.rxlifecycle4.RxLifecycle; import com.trello.rxlifecycle4.android.FragmentEvent; +import java.util.List; + +import io.reactivex.rxjava3.disposables.CompositeDisposable; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.functions.Consumer; + +/** + * UsageFragment 对应的 ViewModel。 + *

+ * 职责:持有 UI 状态(LiveData)、调用 StatisticsRepository 获取数据, + * 不持有网络/IO 细节,不直接依赖 NetInterfaceManager。 + * 用 CompositeDisposable 管理 RxJava 订阅,在 onCleared / onDestroy 时释放。 + */ public class UsageViewModel extends BaseViewModel { + private final CompositeDisposable mDisposables = new CompositeDisposable(); + private final StatisticsRepository mRepository = StatisticsRepository.getInstance(); + + // ===== UI 状态(对外暴露 LiveData,类型与 Repository 返回类型一致)===== + public final MutableLiveData> settingData = new MutableLiveData<>(); + public final MutableLiveData> controlStatus = new MutableLiveData<>(); + public final MutableLiveData> remainingTime = new MutableLiveData<>(); + /** zip 合并后的管控状态 + 剩余时长组合结果 */ + public final MutableLiveData controlCombined = new MutableLiveData<>(); + public final MutableLiveData> appUseTime = new MutableLiveData<>(); + public final MutableLiveData>> mAppInfosLiveData = new MutableLiveData<>(); + public final MutableLiveData loading = new MutableLiveData<>(); + public final MutableLiveData error = new MutableLiveData<>(); + @Override public FragmentUsageBinding getVDBinding() { return binding; } + public void loadSettingData() { + Disposable disposable = mRepository.getSetting() + .compose(RxLifecycle.bindUntilEvent(getLifecycle(), FragmentEvent.DESTROY_VIEW)) + .subscribe(new Consumer>() { + @Override + public void accept(BaseResponse resp) throws Throwable { + Log.e("UsageViewModel", "loadSettingData: " + resp.data); + settingData.setValue(resp); + loading.setValue(false); + Toaster.show("刷新成功"); + } + }, new Consumer() { + @Override + public void accept(Throwable err) throws Throwable { + error.setValue(err); + loading.setValue(false); + Toaster.show("网络错误"); + } + }); + mDisposables.add(disposable); + } + + + public void loadControlStatus() { + loading.setValue(true); + + Disposable disposable = mRepository.getControlStatus() + .compose(RxLifecycle.bindUntilEvent(getLifecycle(), FragmentEvent.DESTROY_VIEW)) + .subscribe( + resp -> { + Log.e("UsageViewModel", "loadControlStatus: " + resp.data); + controlStatus.setValue(resp); + loading.setValue(false); + }, + err -> { + error.setValue(err); + loading.setValue(false); + } + ); + mDisposables.add(disposable); + } + + public void loadRemainingTime() { + Disposable disposable = mRepository.getRemainingTime() + .compose(RxLifecycle.bindUntilEvent(getLifecycle(), FragmentEvent.DESTROY_VIEW)) + .subscribe( + resp -> { + Log.e("UsageViewModel", "loadRemainingTime: " + resp.data); + remainingTime.setValue(resp); + loading.setValue(false); + }, + err -> { + error.setValue(err); + loading.setValue(false); + } + ); + mDisposables.add(disposable); + } + + /** + * 同时请求管控状态与剩余时长,zip 合并后一次性回调。 + * Fragment 只需观察 controlCombined,即可根据两个值联动判断。 + */ + public void loadControlCombined() { + loading.setValue(true); + Disposable disposable = mRepository.getControlStatusCombined() + .compose(RxLifecycle.bindUntilEvent(getLifecycle(), FragmentEvent.DESTROY_VIEW)) + .subscribe( + combined -> { + Log.e("UsageViewModel", "loadControlCombined: " + combined); + // 保留原有独立 LiveData,兼容仍单独观察的地方 + controlStatus.setValue(combined.controlStatus); + remainingTime.setValue(combined.remainingTime); + controlCombined.setValue(combined); + loading.setValue(false); + }, + err -> { + error.setValue(err); + loading.setValue(false); + } + ); + mDisposables.add(disposable); + } + + /** + * 加载指定周期的应用使用记录。 + * + * @param queryType 查询类型:1 今天、2 本周、3 本月 + * @param isAll 0 返回使用时长最高的 6 条,1 返回全部有使用的记录 + */ + public void loadAppUseTime(int queryType, int isAll) { + loading.setValue(true); + + Disposable disposable = mRepository.getAppUseTime(queryType, isAll) + .compose(RxLifecycle.bindUntilEvent(getLifecycle(), FragmentEvent.DESTROY_VIEW)) + .subscribe( + resp -> { + appUseTime.setValue(resp); + loading.setValue(false); + }, + err -> { + error.setValue(err); + loading.setValue(false); + } + ); + mDisposables.add(disposable); + } + + public void loadAppControlDetail() { + Disposable disposable = mRepository.getAppControlDetail() + .compose(RxLifecycle.bindUntilEvent(getLifecycle(), FragmentEvent.DESTROY_VIEW)) + .subscribe( + resp -> { + mAppInfosLiveData.setValue(resp); +// loading.setValue(false); + }, + err -> { +// error.setValue(err); +// loading.setValue(false); + } + ); + mDisposables.add(disposable); + } + @Override public void onDestroy() { + // BaseViewModel 抽象方法:由页面销毁时调用 + mDisposables.clear(); + } + @Override + protected void onCleared() { + // ViewModel 系统回调:确保订阅一定被释放(比 onDestroy 更可靠) + mDisposables.clear(); + super.onCleared(); } } diff --git a/app/src/main/java/com/fuying/sn/manager/time/TimeManager.java b/app/src/main/java/com/fuying/sn/manager/time/TimeManager.java index 274104d..b4260cb 100644 --- a/app/src/main/java/com/fuying/sn/manager/time/TimeManager.java +++ b/app/src/main/java/com/fuying/sn/manager/time/TimeManager.java @@ -707,6 +707,7 @@ public class TimeManager { } public void killBackgroundProcesses(String processName) { + Log.e(TAG, "killBackgroundProcesses: " + processName); ActivityManager activityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); activityManager.forceStopPackage(processName); } diff --git a/app/src/main/java/com/fuying/sn/network/NetInterfaceManager.java b/app/src/main/java/com/fuying/sn/network/NetInterfaceManager.java index bc7ee38..0999c91 100644 --- a/app/src/main/java/com/fuying/sn/network/NetInterfaceManager.java +++ b/app/src/main/java/com/fuying/sn/network/NetInterfaceManager.java @@ -32,6 +32,7 @@ import com.fuying.sn.bean.BaseResponse; import com.fuying.sn.bean.BatchID; import com.fuying.sn.bean.BlacklistBean; import com.fuying.sn.bean.CloudLessonSetting; +import com.fuying.sn.bean.DeviceInfo; import com.fuying.sn.bean.Label; import com.fuying.sn.bean.MyAppUsageBean; import com.fuying.sn.bean.OverallAppBean; @@ -67,6 +68,7 @@ import com.fuying.sn.network.api.SnControlApi; import com.fuying.sn.network.api.SnInfoApi; import com.fuying.sn.network.api.SnLogApi; import com.fuying.sn.network.api.newly.ControlApi; +import com.fuying.sn.network.api.newly.DeviceInfoApi; import com.fuying.sn.network.api.newly.StatisticsApi; import com.fuying.sn.network.cache.RxCacheManager; import com.fuying.sn.network.interceptor.RepeatRequestInterceptor; @@ -405,237 +407,161 @@ public class NetInterfaceManager { return mRetrofit.create(StatisticsApi.class); } + public DeviceInfoApi getDeviceInfoApi() { + return mRetrofit.create(DeviceInfoApi.class); + } + /* * * Observable * * */ + public Observable schedule(Observable observable, BehaviorSubject lifecycle) { + return observable + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .compose(RxLifecycle.bindUntilEvent(lifecycle, ActivityEvent.DESTROY)); + } - public Observable> getSnInfoControl() { - return getSnApi() - .getsninfo(Utils.getSerial()) + public Observable schedule(Observable observable) { + return observable .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()); } + public Observable> getSnInfoControl() { + return schedule(getSnApi().getsninfo(Utils.getSerial())); + } + public Observable> getSettingControl() { - return getSettingsApi() - .getSetting(Utils.getSerial()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); + return schedule(getSettingsApi().getSetting(Utils.getSerial())); } public Observable>> getForceInstallControl() { - return getAppApi() - .getForceInstall(Utils.getSerial()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); + return schedule(getAppApi().getForceInstall(Utils.getSerial())); } public Observable> getBrowserControl() { - return getSettingsApi() - .getBrowser(Utils.getSerial()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); + return schedule(getSettingsApi().getBrowser(Utils.getSerial())); } public Observable> getLabelControl() { - return getSettingsApi() - .getLabel(Utils.getSerial()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); + return schedule(getSettingsApi().getLabel(Utils.getSerial())); } public Observable>> getAppStartControl() { - return getSnControlApi() - .getAppStatu(Utils.getSerial()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); + return schedule(getSnControlApi().getAppStatu(Utils.getSerial())); } public Observable>> getQueryAllAppControl() { - return getAppApi() - .getAllApp(Utils.getSerial()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); + return schedule(getAppApi().getAllApp(Utils.getSerial())); } public Observable getTimeControlObservable() { - return getSnControlApi() - .getTimeControl(Utils.getSerial()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); + return schedule(getSnControlApi().getTimeControl(Utils.getSerial())); } public Observable> getUserAvatarInfoControl() { - return getSnApi() - .getUserAvatarInfo(Utils.getSerial()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); + return schedule(getSnApi().getUserAvatarInfo(Utils.getSerial())); } public Observable getRunningAppObservable(String json) { - return getAppApi() - .sendAppInfo(Utils.getSerial(), json) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); + return schedule(getAppApi().sendAppInfo(Utils.getSerial(), json)); } public Observable> getQRCodeApiControl() { - return getFileApi() - .getQRCodeApi() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); + return schedule(getFileApi().getQRCodeApi()); } public Observable getGetGuideControl() { - return getFileApi() - .getGuidePic(2) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); + return schedule(getFileApi().getGuidePic(2)); } public Observable getAppInsideControl() { - return getSettingsApi() - .getAppInside(Utils.getSerial()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); + return schedule(getSettingsApi().getAppInside(Utils.getSerial())); } public Observable> getBatchApiControl() { - return getSnApi() - .getBatch(Utils.getSerial()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); + return schedule(getSnApi().getBatch(Utils.getSerial())); } public Observable> getCloudLessonSettingApiControl() { - return getSettingsApi() - .getCloudLessonSetting(Utils.getSerial()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); + return schedule(getSettingsApi().getCloudLessonSetting(Utils.getSerial())); } public Observable> getUpdateObservable(String packageName) { - return getAppApi() - .getAppUpdate(packageName, "0", JgyUtils.getInstance().checkAppPlatform()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); + return schedule(getAppApi().getAppUpdate(packageName, "0", JgyUtils.getInstance().checkAppPlatform())); } public Observable>> getOverallAppObservable() { int platformCode = JgyUtils.getInstance().checkAppPlatform(); - return getAppApi() - .getOverallApp(platformCode) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); + return schedule(getAppApi().getOverallApp(platformCode)); } public Observable>> getAppTimeControlObservable() { - return getSettingsApi() - .getAppTimeControl(Utils.getSerial()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); + return schedule(getSettingsApi().getAppTimeControl(Utils.getSerial())); } @Deprecated public Observable> getMachineTimeControlObservable() { - return getSettingsApi() - .getSnTimeControl(Utils.getSerial()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); + return schedule(getSettingsApi().getSnTimeControl(Utils.getSerial())); } public Observable getAppUsageRecordObservable(String pkg, String pkgName, Long open_time, Long close_time) { - return getCommonApi() - .sendAppUsageRecord(Utils.getSerial(), pkg, pkgName, open_time, close_time) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); + return schedule(getCommonApi().sendAppUsageRecord(Utils.getSerial(), pkg, pkgName, open_time, close_time)); } public Observable>> getMyAppListObservable() { - return getAppApi() - .getMyAppList(Utils.getSerial()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); + return schedule(getAppApi().getMyAppList(Utils.getSerial())); } public Observable>> getAppJumpObservable() { - return getSnControlApi() - .getSNJump(Utils.getSerial()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); + return schedule(getSnControlApi().getSNJump(Utils.getSerial())); } public Observable> getBlacklistObservable() { - return getCommonApi() - .getBlacklist() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); + return schedule(getCommonApi().getBlacklist()); } public Observable>> getAdminAppObservable() { - return getCommonApi() - .getAdminApp(Utils.getSerial()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); + return schedule(getCommonApi().getAdminApp(Utils.getSerial())); } public Observable> getLockScreenPwdObservable() { - return getSnControlApi() - .getLockScreenPwd(Utils.getSerial()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); + return schedule(getSnControlApi().getLockScreenPwd(Utils.getSerial())); } public Observable getUpdateLockScreenObservable() { - return getSnControlApi() - .updateLockScreen(Utils.getSerial()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); + return schedule(getSnControlApi().updateLockScreen(Utils.getSerial())); } public Observable>> getAppAndWhiteApiObservable() { - return getCommonApi() - .getAppAndWhite(Utils.getSerial()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); + return schedule(getCommonApi().getAppAndWhite(Utils.getSerial())); } public Observable> getSnBindObservable() { - return getSnApi() - .getSnBind(Utils.getSerial()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); + return schedule(getSnApi().getSnBind(Utils.getSerial())); } public Observable> getSnRunLogControl() { - return getSnLogApi() - .getSnRunLog(Utils.getSerial()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); + return schedule(getSnLogApi().getSnRunLog(Utils.getSerial())); } public Observable> getSnTag() { - return getSnApi() - .getTag(Utils.getSerial()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); + return schedule(getSnApi().getTag(Utils.getSerial())); } public Observable> getSnTimeControlObservable() { - return getControlApi() - .getSnTimeControl(Utils.getSerial()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); + return schedule(getControlApi().getSnTimeControl(Utils.getSerial())); } public Observable>> getAppTimeControlInfoObservable() { - return getControlApi() - .getAppTimeControl(Utils.getSerial()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); + return schedule(getControlApi().getAppTimeControl(Utils.getSerial())); + } + + public Observable> getDeviceInfoObservable() { + return schedule(getDeviceInfoApi().getDeviceInfo(Utils.getSerial())); } /* @@ -2441,23 +2367,18 @@ public class NetInterfaceManager { } private Observable getScreenshot(String filePath) { - Observable screenshotObservable = Observable.create(new ObservableOnSubscribe() { - @Override - public void subscribe(ObservableEmitter e) { - int code = CmdUtil.execute("screencap -p " + filePath).code; - e.onNext(code); - e.onComplete(); - } - }).subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); - return screenshotObservable; + return schedule(Observable.create(new ObservableOnSubscribe() { + @Override + public void subscribe(ObservableEmitter e) { + int code = CmdUtil.execute("screencap -p " + filePath).code; + e.onNext(code); + e.onComplete(); + } + })); } private Observable getSendFile(String path, MultipartBody.Part body) { - return getSnInfoApi() - .getControlScreenshot(Utils.getSerial(), body) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); + return schedule(getSnInfoApi().getControlScreenshot(Utils.getSerial(), body)); } public void screenshot() { @@ -3554,7 +3475,7 @@ public class NetInterfaceManager { Map remainingTimeMap = list.stream().collect(Collectors.toMap(AppTimeControlInfo::getApp_package, AppTimeControlInfo::getToday_time, (v1, v2) -> v1)); TimeManager.getInstance().setAppTimeControlMap(appTimeControlMap); TimeManager.getInstance().setRemainingTimeMap(remainingTimeMap); - }else if (response.code == 400) { + } else if (response.code == 400) { Log.e("getSnTimeControl", "onNext: Too frequent"); } else { diff --git a/app/src/main/java/com/fuying/sn/network/api/newly/DeviceInfoApi.java b/app/src/main/java/com/fuying/sn/network/api/newly/DeviceInfoApi.java index b0989f5..7d0786d 100644 --- a/app/src/main/java/com/fuying/sn/network/api/newly/DeviceInfoApi.java +++ b/app/src/main/java/com/fuying/sn/network/api/newly/DeviceInfoApi.java @@ -1,6 +1,7 @@ package com.fuying.sn.network.api.newly; import com.fuying.sn.bean.BaseResponse; +import com.fuying.sn.bean.DeviceInfo; import com.fuying.sn.network.UrlAddress; import io.reactivex.rxjava3.core.Observable; @@ -9,7 +10,7 @@ import retrofit2.http.Query; public interface DeviceInfoApi { @GET(UrlAddress.DEVICE_INFO) - Observable getDeviceInfo( + Observable> getDeviceInfo( @Query("sn") String sn ); diff --git a/app/src/main/java/com/fuying/sn/network/api/newly/StatisticsApi.java b/app/src/main/java/com/fuying/sn/network/api/newly/StatisticsApi.java index bb6eb49..89397c2 100644 --- a/app/src/main/java/com/fuying/sn/network/api/newly/StatisticsApi.java +++ b/app/src/main/java/com/fuying/sn/network/api/newly/StatisticsApi.java @@ -1,32 +1,40 @@ package com.fuying.sn.network.api.newly; +import com.fuying.sn.bean.AppTimeControlInfo; import com.fuying.sn.bean.BaseResponse; +import com.fuying.sn.data.model.AppControlDetail; +import com.fuying.sn.data.model.AppUseTimeData; +import com.fuying.sn.data.model.AppUseTimeInfo; +import com.fuying.sn.data.model.ControlStatusInfo; +import com.fuying.sn.data.model.RemainingTimeBean; import com.fuying.sn.network.UrlAddress; +import java.util.List; + import io.reactivex.rxjava3.core.Observable; import retrofit2.http.GET; import retrofit2.http.Query; public interface StatisticsApi { @GET(UrlAddress.CONTROL_STATUS) - Observable getControlStatus( + Observable> getControlStatus( @Query("sn") String sn ); @GET(UrlAddress.REMAINING_TIME) - Observable getRemainingTime( + Observable> getRemainingTime( @Query("sn") String sn ); @GET(UrlAddress.APP_USE_TIME) - Observable getAppUseTime( + Observable> getAppUseTime( @Query("sn") String sn, @Query("query_type") int query_type, @Query("is_all") int is_all ); @GET(UrlAddress.APP_CONTROL_DETAIL) - Observable getAppControlDetail( + Observable>> getAppControlDetail( @Query("sn") String sn ); } diff --git a/app/src/main/java/com/fuying/sn/network/interceptor/RepeatRequestInterceptor.java b/app/src/main/java/com/fuying/sn/network/interceptor/RepeatRequestInterceptor.java index 52bf837..9a9c257 100644 --- a/app/src/main/java/com/fuying/sn/network/interceptor/RepeatRequestInterceptor.java +++ b/app/src/main/java/com/fuying/sn/network/interceptor/RepeatRequestInterceptor.java @@ -72,7 +72,7 @@ public class RepeatRequestInterceptor implements Interceptor { private void log(String action, String requestKey, Request request) { if (BuildConfig.DEBUG) { - Log.e("REPEAT-REQUEST", action + requestKey + " Method @" + request.method() + " --- " + " URL = " + request.url().encodedPath() + "\t" + bodyToString(request)); + Log.e("REPEAT-REQUEST", action + requestKey + " Method @" + request.method() + " --- " + " URL = " + request.url() + "\t" + bodyToString(request)); } else { Log.e("REPEAT-REQUEST", action + requestKey + " Method @" + request.method()); } diff --git a/app/src/main/java/com/fuying/sn/utils/ApkUtils.java b/app/src/main/java/com/fuying/sn/utils/ApkUtils.java index 0d29ad1..a87afd0 100644 --- a/app/src/main/java/com/fuying/sn/utils/ApkUtils.java +++ b/app/src/main/java/com/fuying/sn/utils/ApkUtils.java @@ -14,6 +14,7 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Binder; import android.os.Build; @@ -1048,4 +1049,26 @@ public class ApkUtils { return "unknown"; } + /** + * 通过包名获取应用图标 + * + * @param context 上下文 + * @param packageName 包名 + * @return 应用图标 Drawable,未找到时返回 null + */ + public static Drawable getAppIcon(Context context, String packageName) { + if (context == null || TextUtils.isEmpty(packageName)) { + return null; + } + PackageManager pm = context.getPackageManager(); + try { + ApplicationInfo applicationInfo = pm.getApplicationInfo(packageName, 0); + return applicationInfo.loadIcon(pm); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + Log.e(TAG, "getAppIcon: " + packageName + " : " + e.getMessage()); + } + return context.getDrawable(R.mipmap.ic_launcher); + } + } diff --git a/app/src/main/java/com/fuying/sn/utils/GlideLoadUtils.java b/app/src/main/java/com/fuying/sn/utils/GlideLoadUtils.java new file mode 100644 index 0000000..5ef5b10 --- /dev/null +++ b/app/src/main/java/com/fuying/sn/utils/GlideLoadUtils.java @@ -0,0 +1,152 @@ +package com.fuying.sn.utils; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.util.Log; +import android.widget.ImageView; + +import androidx.fragment.app.Fragment; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.resource.bitmap.RoundedCorners; +import com.bumptech.glide.request.RequestOptions; + +import java.io.File; + +/** + * Glide 加载 简单判空封装 防止异步加载数据时调用Glide 抛出异常 + * Created by Li_Xavier on 2017/6/20 0020. + */ +public class GlideLoadUtils { + private String TAG = "ImageLoader"; + + /** + * 借助内部类 实现线程安全的单例模式 + * 属于懒汉式单例,因为Java机制规定,内部类SingletonHolder只有在getInstance() + * 方法第一次调用的时候才会被加载(实现了lazy),而且其加载过程是线程安全的。 + * 内部类加载的时候实例化一次instance。 + */ + public GlideLoadUtils() { + } + + private static class GlideLoadUtilsHolder { + private final static GlideLoadUtils INSTANCE = new GlideLoadUtils(); + } + + public static GlideLoadUtils getInstance() { + return GlideLoadUtilsHolder.INSTANCE; + } + + /** + * Glide 加载 简单判空封装 防止异步加载数据时调用Glide 抛出异常 + * + * @param context + * @param url 加载图片的url地址 String + * @param imageView 加载图片的ImageView 控件 + * @param defaultImage 图片展示错误的本地图片 id + */ + public void glideLoad(Context context, String url, ImageView imageView, int defaultImage) { + if (context != null) { + Glide.with(context).load(url).centerCrop().error(defaultImage).placeholder(imageView.getDrawable()).dontAnimate().into(imageView); + } else { + Log.i(TAG, "Picture loading failed,context is null"); + } + } + + public void glideLoadRound(Context context, String url, ImageView imageView, int defaultImage) { + if (context != null) { + Glide.with(context).load(url).centerCrop().error(defaultImage) + .placeholder(imageView.getDrawable()).dontAnimate() + .apply(RequestOptions.bitmapTransform(new RoundedCorners(ScreenUtils.dp2px(context, 16f)))) + .into(imageView); + } else { + Log.i(TAG, "Picture loading failed,context is null"); + } + } + + + public void glideLoad(Context context, int resId, ImageView imageView, int defaultImage) { + if (context != null) { + Glide.with(context).load(resId).centerCrop().placeholder(imageView.getDrawable()).error(defaultImage).into(imageView); + } else { + Log.i(TAG, "Picture loading failed,context is null"); + } + } + + public void glideLoad(Context context, String url, ImageView imageView) { + if (context != null) { + Glide.with(context).load(url).centerCrop().dontAnimate().placeholder(imageView.getDrawable()).into(imageView); + } else { + Log.i(TAG, "Picture loading failed,context is null"); + } + } + + public void glideLoad(Context context, Bitmap bitmap, ImageView imageView) { + if (context != null) { + Glide.with(context).load(bitmap).centerCrop().dontAnimate().placeholder(imageView.getDrawable()).into(imageView); + } else { + Log.i(TAG, "Picture loading failed,context is null"); + } + } + + public void glideLoad(Context context, Drawable drawable, ImageView imageView) { + if (context != null) { + Glide.with(context).load(drawable).centerCrop().dontAnimate().placeholder(imageView.getDrawable()).into(imageView); + } else { + Log.i(TAG, "Picture loading failed,context is null"); + } + } + + public void glideLoad(Context context, Drawable drawable, ImageView imageView,int resId) { + if (context != null) { + Glide.with(context).load(drawable).centerCrop().dontAnimate().placeholder(imageView.getDrawable()).error(resId).into(imageView); + } else { + Log.i(TAG, "Picture loading failed,context is null"); + } + } + + public void glideLoad(Context context, File file, ImageView imageView) { + if (context != null) { + Glide.with(context).load(file).centerCrop().into(imageView); + } else { + Log.i(TAG, "Picture loading failed,context is null"); + } + } + + public void glideLoad(Context context, File file, ImageView imageView, int defaultRes) { + if (context != null) { + Glide.with(context).load(file).centerCrop().error(defaultRes).into(imageView); + } else { + Log.i(TAG, "Picture loading failed,context is null"); + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public void glideLoad(Activity activity, String url, ImageView imageView, int default_image) { + if (!activity.isDestroyed()) { + Glide.with(activity).load(url).centerCrop().error(default_image).placeholder(imageView.getDrawable()).into(imageView); + } else { + Log.i(TAG, "Picture loading failed,activity is Destroyed"); + } + } + + public void glideLoad(Fragment fragment, String url, ImageView imageView, int default_image) { + if (fragment != null && fragment.getActivity() != null) { + Glide.with(fragment).load(url).centerCrop().error(default_image).into(imageView); + } else { + Log.i(TAG, "Picture loading failed,fragment is null"); + } + } + + public void glideLoad(android.app.Fragment fragment, String url, ImageView imageView, int default_image) { + if (fragment != null && fragment.getActivity() != null) { + Glide.with(fragment).load(url).centerCrop().error(default_image).into(imageView); + } else { + Log.i(TAG, "Picture loading failed,android.app.Fragment is null"); + } + } +} diff --git a/app/src/main/java/com/fuying/sn/utils/JgyUtils.java b/app/src/main/java/com/fuying/sn/utils/JgyUtils.java index 5a6a600..23432bd 100644 --- a/app/src/main/java/com/fuying/sn/utils/JgyUtils.java +++ b/app/src/main/java/com/fuying/sn/utils/JgyUtils.java @@ -856,7 +856,7 @@ public class JgyUtils { } } ComponentName preActivity = new ComponentName(defPackageName, defClassName); - context.getPackageManager().addPreferredActivity(filter, bestMatch, set, preActivity); + context.getPackageManager().replacePreferredActivity(filter, bestMatch, set, preActivity); } } catch (java.lang.SecurityException e) { e.printStackTrace(); @@ -865,29 +865,48 @@ public class JgyUtils { } public void setRoleHolderAsUser(Context context, String packageName) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - String roleName = "android.app.role.HOME"; - boolean add = true; - int flags = 0; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + Log.e(TAG, "setRoleHolderAsUser: RoleManager requires Android 10 or higher."); + return; + } + if (JgyUtils.getInstance().checkAppPlatform() == JgyUtils.MTKPlatform) { + Log.e(TAG, "setRoleHolderAsUser: MTK platform, skip"); + return; + } + RoleManager roleManager = context.getSystemService(RoleManager.class); + if (roleManager == null) { + Log.e(TAG, "setRoleHolderAsUser: roleManager is null"); + return; + } + String roleName = RoleManager.ROLE_HOME; + + try { UserHandle user = Process.myUserHandle(); - Log.i("settingssssssstemf", (add ? "Adding" : "Removing") + " package as role holder, role: " - + roleName + ", package: " + packageName); - if (JgyUtils.getInstance().checkAppPlatform() != JgyUtils.MTKPlatform) { - RoleManager roleManager = context.getSystemService(RoleManager.class); - Executor executor = context.getMainExecutor(); - Consumer callback = successful -> { - if (successful) { - Log.d("settingssssssstemf", "Package " + (add ? "added" : "removed") - + " as role holder, role: " + roleName + ", package: " + packageName); - } else { - Log.d("settingssssssstemf", "Failed to " + (add ? "add" : "remove") - + " package as role holder, role: " + roleName + ", package: " - + packageName); - } - }; - roleManager.addRoleHolderAsUser(roleName, packageName, flags, user, executor, callback); - Log.i("settingssssssstemf", "addRoleHolderAsUser done"); - } + Executor executor = context.getMainExecutor(); + + // 定义回调 + Consumer callback = successful -> { + if (successful) { + Log.i(TAG, "setRoleHolderAsUser: Successfully set " + packageName + " as " + roleName); + } else { + Log.e(TAG, "setRoleHolderAsUser: Failed to set " + packageName + " as " + roleName + ". Check system logs."); + } + }; + + // 2. 使用反射调用隐藏方法 addRoleHolderAsUser + // 方法签名: addRoleHolderAsUser(String, String, int, UserHandle, Executor, Consumer) + Method method = RoleManager.class.getMethod("addRoleHolderAsUser", + String.class, String.class, int.class, UserHandle.class, Executor.class, Consumer.class); + + Log.d(TAG, "setRoleHolderAsUser: Invoking addRoleHolderAsUser for package: " + packageName); + method.invoke(roleManager, roleName, packageName, 0, user, executor, callback); + +// roleManager.addRoleHolderAsUser(roleName, packageName, 0, user, executor, callback); + + } catch (NoSuchMethodException e) { + Log.e(TAG, "setRoleHolderAsUser: Method not found. Is this a non-standard ROM?", e); + } catch (Exception e) { + Log.e(TAG, "setRoleHolderAsUser: Error invoking addRoleHolderAsUser", e); } } @@ -959,42 +978,26 @@ public class JgyUtils { setRoleHolderAsUser(mContext, pkg); Log.e(TAG, "setDefaultDesktop: setRoleHolderAsUser"); } else { - //爱华设置,暂时屏蔽 -// setDefaultLauncher(mContext, pkg, className); + setDefaultLauncher(mContext, pkg, className); Log.e(TAG, "setDefaultDesktop: setDefaultLauncher"); } -// String oldDesktop = (String) SPUtils.get(mContext, "default_launcher", ""); -// if (Objects.equals(oldDesktop, pkg)) { -// Log.e(TAG, "setDefaultDesktop: " + "数据一致"); -// return; -// } - Intent intent = new Intent("setDefaultLauncher"); - intent.putExtra("package", pkg); - intent.putExtra("className", className); - if (JgyUtils.getInstance().checkAppPlatform() == MTKPlatform) { - Log.e(TAG, "setDefaultDesktop: MTK"); - //爱华定制 - intent.setComponent(new ComponentName("com.android.settings", "com.android.settings.AoleReceiver")); - // TODO: 2022/7/6 有问题 - setDefaultLauncher(mContext, "com.android.transfer", "com.android.transfer.MainActivity"); - SystemProperties.set("persist.sys.launcher.pkgname", pkg); - SystemProperties.set("persist.sys.launcher.classname", className); - } - intent.setPackage("com.android.settings"); - mContext.sendBroadcast(intent); -// ApkUtils.openPackage(mContext, pkg); - Log.e(TAG, "setDefaultDesktop: " + pkg + ":" + className); - Log.e(TAG, "setDefaultDesktop: " + "persist.sys.launcher.pkgname = " + SystemProperties.get("persist.sys.launcher.pkgname")); - Log.e(TAG, "setDefaultDesktop: " + "persist.sys.launcher.classname = " + SystemProperties.get("persist.sys.launcher.classname")); - } -// public void setDefaultDesktop(String pkg, String className) { // Intent intent = new Intent("setDefaultLauncher"); // intent.putExtra("package", pkg); // intent.putExtra("className", className); +// if (JgyUtils.getInstance().checkAppPlatform() == MTKPlatform) { +// Log.e(TAG, "setDefaultDesktop: MTK"); +// //爱华定制 +// intent.setComponent(new ComponentName("com.android.settings", "com.android.settings.AoleReceiver")); +// // TODO: 2022/7/6 有问题 +// setDefaultLauncher(mContext, "com.android.transfer", "com.android.transfer.MainActivity"); +// SystemProperties.set("persist.sys.launcher.pkgname", pkg); +// SystemProperties.set("persist.sys.launcher.classname", className); +// } +// // TODO: 2026/8/11 发送setDefaultLauncher会强制到桌面,暂时去掉 // intent.setPackage("com.android.settings"); // mContext.sendBroadcast(intent); -// } + } public ArrayList getForbidList() { ArrayList disableApp = new ArrayList<>(); @@ -1189,6 +1192,7 @@ public class JgyUtils { mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, currentVolume, 0); } + @Deprecated public void setDefaultLauncher(String pkg) { Intent intent = new Intent("setDefaultLauncher"); intent.putExtra("package", "com.uiui.os"); @@ -1350,26 +1354,26 @@ public class JgyUtils { return; } Observable.create(new ObservableOnSubscribe() { - @Override - public void subscribe(ObservableEmitter e) throws Exception { - Log.e("RemoveTask", "subscribe: " + packageName); - List list = Aria.download(context).getTaskList(); - for (DownloadEntity entity : list) { - long id = entity.getId(); - String extendField = Aria.download(this).load(id).getExtendField(); - try { - JsonObject jsonObject = JsonParser.parseString(extendField).getAsJsonObject(); - if (packageName.equals(jsonObject.get("app_package").getAsString())) { - Log.e("RemoveTask", "subscribe: " + "删除文件:" + entity.getFilePath()); - Aria.download(this).load(id).cancel(true); + @Override + public void subscribe(ObservableEmitter e) throws Exception { + Log.e("RemoveTask", "subscribe: " + packageName); + List list = Aria.download(context).getTaskList(); + for (DownloadEntity entity : list) { + long id = entity.getId(); + String extendField = Aria.download(this).load(id).getExtendField(); + try { + JsonObject jsonObject = JsonParser.parseString(extendField).getAsJsonObject(); + if (packageName.equals(jsonObject.get("app_package").getAsString())) { + Log.e("RemoveTask", "subscribe: " + "删除文件:" + entity.getFilePath()); + Aria.download(this).load(id).cancel(true); + } + } catch (Exception exception) { + Log.e("RemoveTask", "subscribe: " + exception.getMessage()); + } } - } catch (Exception exception) { - Log.e("RemoveTask", "subscribe: " + exception.getMessage()); + e.onComplete(); } - } - e.onComplete(); - } - }).subscribeOn(Schedulers.io()) + }).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Observer() { @Override diff --git a/app/src/main/java/com/fuying/sn/utils/ScreenUtils.java b/app/src/main/java/com/fuying/sn/utils/ScreenUtils.java new file mode 100644 index 0000000..76fd212 --- /dev/null +++ b/app/src/main/java/com/fuying/sn/utils/ScreenUtils.java @@ -0,0 +1,33 @@ +package com.fuying.sn.utils; + +import android.content.Context; +import android.content.res.Resources; + +public class ScreenUtils { + public static int dp2px(final float dpValue) { + final float scale = Resources.getSystem().getDisplayMetrics().density; + return (int) (dpValue * scale + 0.5f); + } + + public static int dp2px(Context context, final float dpValue) { + final float scale = context.getResources().getDisplayMetrics().density; + return (int) (dpValue * scale + 0.5f); + } + + public static int px2dp(Context context, final float pxValue) { + final float scale = context.getResources().getDisplayMetrics().density; + return (int) (pxValue / scale + 0.5f); + } + + + public static int sp2px(Context context, final float spValue) { + final float scale = context.getResources().getDisplayMetrics().scaledDensity; + return (int) (spValue * scale + 0.5f); + } + + public static int px2sp(Context context, final float pxValue) { + final float fontScale = context.getResources().getDisplayMetrics().scaledDensity; + return (int) (pxValue / fontScale + 0.5f); + } + +} diff --git a/app/src/main/java/com/fuying/sn/utils/TimeUtils.java b/app/src/main/java/com/fuying/sn/utils/TimeUtils.java index 68a4e4a..e764e1f 100644 --- a/app/src/main/java/com/fuying/sn/utils/TimeUtils.java +++ b/app/src/main/java/com/fuying/sn/utils/TimeUtils.java @@ -10,6 +10,8 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; +import com.fuying.sn.bean.PartTime; +import com.fuying.sn.data.model.AppControlDetail; import com.fuying.sn.service.ManagerService; import java.net.DatagramPacket; @@ -17,12 +19,15 @@ import java.net.DatagramSocket; import java.net.InetAddress; import java.text.DateFormat; import java.text.ParseException; +import java.util.List; +import java.util.concurrent.TimeUnit; import java.text.SimpleDateFormat; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; +import java.time.Duration; import java.util.Arrays; import java.util.Calendar; import java.util.Date; @@ -240,6 +245,56 @@ public class TimeUtils { private String[] weekDays = {"星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"}; + public static String formatTime(long timeSecond) { + if (timeSecond <= 0) { + return "0分钟"; + } + Duration duration = Duration.ofSeconds(timeSecond); + long hours = duration.toHours(); + long minutes = duration.toMinutes() - hours * 60; + if (hours > 0) { + if (minutes > 0) { + return hours + "小时" + minutes + "分钟"; + } + return hours + "小时"; + } + return minutes + "分钟"; + } + + public static String getPartTimeString(AppControlDetail appControlDetail) { + if (appControlDetail == null) return ""; + List time_period = appControlDetail.getTime_period(); + return getPartTimeString(time_period); + } + + public static String getPartTimeString(List time_period) { + if (time_period == null || time_period.isEmpty()) return ""; + StringBuilder sbWork = new StringBuilder(); + for (int i = 0; i < time_period.size(); i++) { + PartTime partTime = time_period.get(i); + sbWork.append(partTime.getStart_time()).append("~").append(partTime.getEnd_time()); + if (i < time_period.size() - 1) { + sbWork.append("\n"); + } + } + Log.e(TAG, "getPartTimeString: " + sbWork); + return sbWork.toString(); + } + + public static String getPartTimeStringLine(List time_period) { + if (time_period == null || time_period.isEmpty()) return ""; + StringBuilder sbWork = new StringBuilder(); + for (int i = 0; i < time_period.size(); i++) { + PartTime partTime = time_period.get(i); + sbWork.append(partTime.getStart_time()).append("~").append(partTime.getEnd_time()); + if (i < time_period.size() - 1) { + sbWork.append("\t"); + } + } + Log.e(TAG, "getPartTimeString: " + sbWork); + return sbWork.toString(); + } + /** * 获取星期几 * diff --git a/app/src/main/java/com/fuying/sn/utils/Utils.java b/app/src/main/java/com/fuying/sn/utils/Utils.java index b400fda..3efae32 100644 --- a/app/src/main/java/com/fuying/sn/utils/Utils.java +++ b/app/src/main/java/com/fuying/sn/utils/Utils.java @@ -557,32 +557,6 @@ public class Utils { return ""; } - /** - * 根据手机的分辨率从 dp 的单位 转成为 px(像素) - */ - public static int dip2px(Context context, float dpValue) { - final float scale = context.getResources().getDisplayMetrics().density; - return (int) (dpValue * scale + 0.5f); - } - - /** - * 根据手机的分辨率从 px(像素) 的单位 转成为 dp - */ - public static int px2dip(Context context, float pxValue) { - final float scale = context.getResources().getDisplayMetrics().density; - return (int) (pxValue / scale + 0.5f); - } - - public static float dp2px(Resources resources, float dp) { - final float scale = resources.getDisplayMetrics().density; - return dp * scale + 0.5f; - } - - public static float sp2px(Resources resources, float sp) { - final float scale = resources.getDisplayMetrics().scaledDensity; - return sp * scale; - } - public static String getTime() { long time = System.currentTimeMillis();//long now = android.os.SystemClock.uptimeMillis(); SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd"); diff --git a/app/src/main/res/drawable/bt_refresh_normnl.xml b/app/src/main/res/drawable/bt_refresh_normnl.xml new file mode 100644 index 0000000..ae96689 --- /dev/null +++ b/app/src/main/res/drawable/bt_refresh_normnl.xml @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_map_location.xml b/app/src/main/res/drawable/ic_map_location.xml new file mode 100644 index 0000000..b340877 --- /dev/null +++ b/app/src/main/res/drawable/ic_map_location.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/tv_time_disable_background.xml b/app/src/main/res/drawable/tv_time_disable_background.xml new file mode 100644 index 0000000..2ffe90b --- /dev/null +++ b/app/src/main/res/drawable/tv_time_disable_background.xml @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/tv_time_duration_background.xml b/app/src/main/res/drawable/tv_time_duration_background.xml new file mode 100644 index 0000000..4007fc1 --- /dev/null +++ b/app/src/main/res/drawable/tv_time_duration_background.xml @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/tv_time_period_background.xml b/app/src/main/res/drawable/tv_time_period_background.xml new file mode 100644 index 0000000..dd79910 --- /dev/null +++ b/app/src/main/res/drawable/tv_time_period_background.xml @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/tv_time_unlimited_background.xml b/app/src/main/res/drawable/tv_time_unlimited_background.xml new file mode 100644 index 0000000..3bb8ea5 --- /dev/null +++ b/app/src/main/res/drawable/tv_time_unlimited_background.xml @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_home.xml b/app/src/main/res/layout-land/activity_home.xml similarity index 100% rename from app/src/main/res/layout/activity_home.xml rename to app/src/main/res/layout-land/activity_home.xml diff --git a/app/src/main/res/layout-port/activity_home.xml b/app/src/main/res/layout-port/activity_home.xml new file mode 100644 index 0000000..9437779 --- /dev/null +++ b/app/src/main/res/layout-port/activity_home.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_usage_detail.xml b/app/src/main/res/layout/activity_usage_detail.xml new file mode 100644 index 0000000..9c99018 --- /dev/null +++ b/app/src/main/res/layout/activity_usage_detail.xml @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_device.xml b/app/src/main/res/layout/fragment_device.xml index ba9abaa..d0b8cda 100644 --- a/app/src/main/res/layout/fragment_device.xml +++ b/app/src/main/res/layout/fragment_device.xml @@ -592,10 +592,10 @@ + android:layout_weight="1"> - + android:layout_height="match_parent" + android:layout_margin="@dimen/dp_8" + android:orientation="horizontal"> - + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/dp_8" + android:layout_marginTop="@dimen/dp_4" + android:layout_marginEnd="@dimen/dp_8" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/textView5"> - - - - - - - - - - - - + android:layout_marginStart="@dimen/dp_8" + android:layout_marginTop="@dimen/dp_4" + android:layout_marginEnd="@dimen/dp_8" + android:layout_height="wrap_content" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/tv_sn" + app:layout_constraintTop_toBottomOf="@+id/constraintLayout4"> - + app:layout_constraintTop_toTopOf="parent" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + - + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - \ No newline at end of file + diff --git a/app/src/main/res/layout/item_app_time_detail.xml b/app/src/main/res/layout/item_app_time_detail.xml new file mode 100644 index 0000000..fe6b0d7 --- /dev/null +++ b/app/src/main/res/layout/item_app_time_detail.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_app_use_time.xml b/app/src/main/res/layout/item_app_use_time.xml new file mode 100644 index 0000000..932560b --- /dev/null +++ b/app/src/main/res/layout/item_app_use_time.xml @@ -0,0 +1,47 @@ + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index da4446e..948e54d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -20,4 +20,14 @@ + 使用时长统计 + 查看全部> + 使用时长 + 今日 + 本周 + 本月 + 小时 + 分钟 + 暂无数据 +