feat(statistics): add usage statistics and device info features
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +126,9 @@
|
||||
android:name=".activity.update.UpdateActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/DialogCloseOnTouchOutside" />
|
||||
<activity
|
||||
android:name=".activity.usage.UsageDetailActivity"
|
||||
android:launchMode="singleTask" />
|
||||
|
||||
<service
|
||||
android:name=".service.ControlPanelService"
|
||||
|
||||
@@ -24,6 +24,7 @@ import com.fuying.sn.databinding.ActivityHomeBinding;
|
||||
import com.fuying.sn.dialog.UserDebugDialog;
|
||||
import com.fuying.sn.fragment.BaseFragmentPagerAdapter;
|
||||
import com.fuying.sn.fragment.device.DeviceFragment;
|
||||
import com.fuying.sn.fragment.usage.UsageFragment;
|
||||
import com.fuying.sn.manager.ControlManager;
|
||||
import com.fuying.sn.manager.DeviceManager;
|
||||
import com.fuying.sn.manager.time.TimeManager;
|
||||
@@ -38,7 +39,7 @@ public class HomeActivity extends BaseMvvmActivity<HomeViewModel, ActivityHomeBi
|
||||
private static final String TAG = "MainActivity";
|
||||
|
||||
private String[] mAllTitle = new String[]{
|
||||
// "使用统计",
|
||||
"使用统计",
|
||||
"设备信息",};
|
||||
|
||||
private List<Fragment> mFragments;
|
||||
@@ -78,7 +79,7 @@ public class HomeActivity extends BaseMvvmActivity<HomeViewModel, ActivityHomeBi
|
||||
@Override
|
||||
public void initView() {
|
||||
mFragments = new ArrayList<>();
|
||||
// mFragments.add(new UsageFragment());
|
||||
mFragments.add(new UsageFragment());
|
||||
mFragments.add(new DeviceFragment());
|
||||
mFragmentManager = getSupportFragmentManager();
|
||||
mBaseFragmentPagerAdapter = new BaseFragmentPagerAdapter(mFragmentManager, mFragments);
|
||||
|
||||
@@ -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<UsageDetailViewModel, ActivityUsageDetailBinding> {
|
||||
|
||||
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<AppUseTimeData> resp) {
|
||||
if (resp == null || resp.data == null || resp.data.getApp_list().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<AppUseTimeInfo> 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 + "分钟";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ActivityUsageDetailBinding, ActivityEvent> {
|
||||
|
||||
private static final String TAG = "UsageDetailViewModel";
|
||||
private final CompositeDisposable mDisposables = new CompositeDisposable();
|
||||
private final StatisticsRepository mRepository = StatisticsRepository.getInstance();
|
||||
|
||||
public final MutableLiveData<BaseResponse<AppUseTimeData>> appUseTime = new MutableLiveData<>();
|
||||
public final MutableLiveData<Boolean> loading = new MutableLiveData<>();
|
||||
public final MutableLiveData<Throwable> 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();
|
||||
}
|
||||
}
|
||||
@@ -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<AppControlDetailAdapter.ViewHolder> {
|
||||
private static final String TAG = "DesktopItemAdapter";
|
||||
|
||||
private MMKV mMMKV = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE);
|
||||
private Context mContext;
|
||||
|
||||
private List<AppControlDetail> mAppTimeControlInfos;
|
||||
|
||||
public void setAppTimeControlInfos(List<AppControlDetail> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<AppUseTimeAdapter.ViewHolder> {
|
||||
|
||||
private Context mContext;
|
||||
private List<AppUseTimeInfo> mList;
|
||||
|
||||
public void setData(List<AppUseTimeInfo> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
26
app/src/main/java/com/fuying/sn/bean/DeviceInfo.java
Normal file
26
app/src/main/java/com/fuying/sn/bean/DeviceInfo.java
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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=";
|
||||
|
||||
@@ -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> T getCache(String key) { /* TODO */ return null; }
|
||||
// public <T> 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<SnInfo> resp) { /* TODO */ }
|
||||
// public BaseResponse<SnInfo> getSnInfoCache() { /* TODO */ return null; }
|
||||
}
|
||||
@@ -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<PartTime> 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<PartTime> getTime_period() {
|
||||
return time_period;
|
||||
}
|
||||
|
||||
public void setTime_period(List<PartTime> time_period) {
|
||||
this.time_period = time_period;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return JsonParser.parseString(new Gson().toJson(this)).getAsJsonObject().toString();
|
||||
}
|
||||
}
|
||||
@@ -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<AppUseTimeInfo> app_list;
|
||||
|
||||
public long getTotal_time() {
|
||||
return total_time;
|
||||
}
|
||||
|
||||
public void setTotal_time(long total_time) {
|
||||
this.total_time = total_time;
|
||||
}
|
||||
|
||||
public List<AppUseTimeInfo> getApp_list() {
|
||||
return app_list;
|
||||
}
|
||||
|
||||
public void setApp_list(List<AppUseTimeInfo> app_list) {
|
||||
this.app_list = app_list;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return JsonParser.parseString(new Gson().toJson(this)).getAsJsonObject().toString();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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<ControlStatusInfo> controlStatus;
|
||||
public final BaseResponse<RemainingTimeBean> remainingTime;
|
||||
|
||||
public ControlStatusCombined(BaseResponse<ControlStatusInfo> controlStatus,
|
||||
BaseResponse<RemainingTimeBean> remainingTime) {
|
||||
this.controlStatus = controlStatus;
|
||||
this.remainingTime = remainingTime;
|
||||
}
|
||||
|
||||
/** 两个请求都成功(code==200)才为 true */
|
||||
public boolean isBothSuccess() {
|
||||
return controlStatus != null && controlStatus.code == 200
|
||||
&& remainingTime != null && remainingTime.code == 200;
|
||||
}
|
||||
}
|
||||
@@ -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<PartTime> 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<PartTime> getTime_period() {
|
||||
return time_period;
|
||||
}
|
||||
|
||||
public void setTime_period(List<PartTime> time_period) {
|
||||
this.time_period = time_period;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return JsonParser.parseString(new Gson().toJson(this)).getAsJsonObject().toString();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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)。
|
||||
* <p>
|
||||
* 职责:作为使用统计相关数据的唯一入口,向 ViewModel 屏蔽数据来源细节
|
||||
* (网络 / 缓存),并统一套用 NetInterfaceManager 提供的多级缓存策略。
|
||||
* <p>
|
||||
* 类型策略:每个方法的返回类型与 StatisticsApi 接口完全一致并带具体泛型,
|
||||
* 这样 withMultiLevelCache 才能精确反序列化缓存,且调用方拿到强类型、无需强转。
|
||||
* 暂无具体 Bean 的接口暂用 BaseResponse<Object> 过渡,有 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<BaseResponse<SystemSettingsSet>> getSetting() {
|
||||
return NetInterfaceManager.withMultiLevelCache(
|
||||
NetInterfaceManager.getInstance().getSettingsApi().getSetting(Utils.getSerial()),
|
||||
UrlAddress.GET_SETTINGS,
|
||||
new TypeToken<BaseResponse<SystemSettingsSet>>() {
|
||||
}.getType(),
|
||||
CACHE_EXPIRE_ONE_MINUTE);
|
||||
}
|
||||
|
||||
public Observable<BaseResponse<ControlStatusInfo>> getControlStatus() {
|
||||
return NetInterfaceManager.withMultiLevelCache(
|
||||
NetInterfaceManager.getInstance().getStatisticsApi().getControlStatus(Utils.getSerial()),
|
||||
UrlAddress.CONTROL_STATUS,
|
||||
new TypeToken<BaseResponse<ControlStatusInfo>>() {
|
||||
}.getType(),
|
||||
CACHE_EXPIRE_ONE_MINUTE);
|
||||
}
|
||||
|
||||
public Observable<BaseResponse<RemainingTimeBean>> getRemainingTime() {
|
||||
return NetInterfaceManager.withMultiLevelCache(
|
||||
NetInterfaceManager.getInstance().getStatisticsApi().getRemainingTime(Utils.getSerial()),
|
||||
UrlAddress.REMAINING_TIME,
|
||||
new TypeToken<BaseResponse<RemainingTimeBean>>() {
|
||||
}.getType(),
|
||||
CACHE_EXPIRE_ONE_MINUTE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将管控状态(controlStatus)与剩余时长(remainingTime)两个请求 zip 合并,
|
||||
* 两个都返回后一次性给出组合结果,便于上层根据两个值联动判断。
|
||||
* 任意一方网络失败都会触发 onError,上层按整体失败处理即可。
|
||||
*/
|
||||
public Observable<ControlStatusCombined> 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<BaseResponse<AppUseTimeData>> 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<BaseResponse<AppUseTimeData>>() {
|
||||
}.getType(),
|
||||
CACHE_EXPIRE_ONE_MINUTE);
|
||||
}
|
||||
|
||||
public Observable<BaseResponse<List<AppControlDetail>>> getAppControlDetail() {
|
||||
return NetInterfaceManager.withMultiLevelCache(
|
||||
NetInterfaceManager.getInstance().getStatisticsApi().getAppControlDetail(Utils.getSerial()),
|
||||
UrlAddress.APP_CONTROL_DETAIL,
|
||||
new TypeToken<BaseResponse<List<AppControlDetail>>>() {
|
||||
}.getType(),
|
||||
CACHE_EXPIRE_ONE_MINUTE);
|
||||
}
|
||||
}
|
||||
@@ -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<DeviceViewModel, FragmentDe
|
||||
@Override
|
||||
public void onChanged(SnInfo snInfo) {
|
||||
if (snInfo != null) {
|
||||
if (!TextUtils.isEmpty(snInfo.getMobile())) {
|
||||
mViewDataBinding.tvPhoneNumber.setText(snInfo.getMobile());
|
||||
}
|
||||
mViewDataBinding.tvBindTime.setText(TimeUtils.getDate(0));
|
||||
|
||||
} else {
|
||||
mViewDataBinding.tvPhoneNumber.setText("未绑定");
|
||||
mViewDataBinding.tvBindTime.setText("未绑定");
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
mViewModel.getSnInfo();
|
||||
|
||||
mViewModel.mDeviceInfoMutableLiveData.observe(this, new Observer<DeviceInfo>() {
|
||||
@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<AppInfo>() {
|
||||
@Override
|
||||
public void onChanged(AppInfo appInfo) {
|
||||
|
||||
@@ -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<FragmentDeviceBinding, Fragme
|
||||
});
|
||||
}
|
||||
|
||||
public MutableLiveData<DeviceInfo> mDeviceInfoMutableLiveData = new MutableLiveData<>();
|
||||
|
||||
public void getDeviceInfo() {
|
||||
NetInterfaceManager.getInstance().getDeviceInfoObservable()
|
||||
.subscribe(new Observer<BaseResponse<DeviceInfo>>() {
|
||||
@Override
|
||||
public void onSubscribe(@NonNull Disposable d) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(BaseResponse<DeviceInfo> 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<AppInfo> mAppInfoMutableLiveData = new MutableLiveData<>();
|
||||
|
||||
public void checkUpdate(boolean response) {
|
||||
|
||||
@@ -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<UsageViewModel, FragmentUsageBinding> {
|
||||
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<String> 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<UsageViewModel, FragmentUsag
|
||||
|
||||
@Override
|
||||
protected void initView(Bundle bundle) {
|
||||
mViewDataBinding.tvSn.setText("SN码:" + Utils.getSerial());
|
||||
mAppControlDetailAdapter = new AppControlDetailAdapter();
|
||||
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getCtx());
|
||||
linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
|
||||
mViewDataBinding.recyclerView.setAdapter(mAppControlDetailAdapter);
|
||||
mViewDataBinding.recyclerView.setLayoutManager(linearLayoutManager);
|
||||
|
||||
mBarChart = mViewDataBinding.barChart;
|
||||
mTvTotalTime = mViewDataBinding.tvTotalTime;
|
||||
initChart();
|
||||
initTabs();
|
||||
// 进入页面时先展示上一次缓存的同步时间
|
||||
updateLastSyncTime();
|
||||
selectTab(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initData(Bundle savedInstanceState) {
|
||||
String address = mMMKV.decodeString(CommonConfig.MAP_ADDRESS_KEY, "正在定位");
|
||||
if (!TextUtils.isEmpty(address)) {
|
||||
mViewDataBinding.tvAddress.setText(address);
|
||||
}
|
||||
LiveEventBus.get(CommonConfig.MAP_ADDRESS_KEY, String.class)
|
||||
.observe(this, new Observer<String>() {
|
||||
@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<SystemSettingsSet> 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<List<AppControlDetail>> response) {
|
||||
onRequestFinished(REQ_APP_CONTROL_DETAIL, response.code == 200);
|
||||
if (response.code == 200) {
|
||||
List<AppControlDetail> 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<AppUseTimeData> 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<AppUseTimeInfo> list = resp.data.getApp_list();
|
||||
Collections.sort(list, (a, b) -> Long.compare(b.getUsage_time(), a.getUsage_time()));
|
||||
|
||||
List<AppUseTimeInfo> 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<String> labels = new ArrayList<>();
|
||||
List<BarEntry> 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<IBarDataSet> 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<String> labels;
|
||||
|
||||
AppNameValueFormatter(List<String> 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<PositionData> 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<PositionData> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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。
|
||||
* <p>
|
||||
* 职责:持有 UI 状态(LiveData)、调用 StatisticsRepository 获取数据,
|
||||
* 不持有网络/IO 细节,不直接依赖 NetInterfaceManager。
|
||||
* 用 CompositeDisposable 管理 RxJava 订阅,在 onCleared / onDestroy 时释放。
|
||||
*/
|
||||
public class UsageViewModel extends BaseViewModel<FragmentUsageBinding, FragmentEvent> {
|
||||
|
||||
private final CompositeDisposable mDisposables = new CompositeDisposable();
|
||||
private final StatisticsRepository mRepository = StatisticsRepository.getInstance();
|
||||
|
||||
// ===== UI 状态(对外暴露 LiveData,类型与 Repository 返回类型一致)=====
|
||||
public final MutableLiveData<BaseResponse<SystemSettingsSet>> settingData = new MutableLiveData<>();
|
||||
public final MutableLiveData<BaseResponse<ControlStatusInfo>> controlStatus = new MutableLiveData<>();
|
||||
public final MutableLiveData<BaseResponse<RemainingTimeBean>> remainingTime = new MutableLiveData<>();
|
||||
/** zip 合并后的管控状态 + 剩余时长组合结果 */
|
||||
public final MutableLiveData<ControlStatusCombined> controlCombined = new MutableLiveData<>();
|
||||
public final MutableLiveData<BaseResponse<AppUseTimeData>> appUseTime = new MutableLiveData<>();
|
||||
public final MutableLiveData<BaseResponse<List<AppControlDetail>>> mAppInfosLiveData = new MutableLiveData<>();
|
||||
public final MutableLiveData<Boolean> loading = new MutableLiveData<>();
|
||||
public final MutableLiveData<Throwable> 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<BaseResponse<SystemSettingsSet>>() {
|
||||
@Override
|
||||
public void accept(BaseResponse<SystemSettingsSet> resp) throws Throwable {
|
||||
Log.e("UsageViewModel", "loadSettingData: " + resp.data);
|
||||
settingData.setValue(resp);
|
||||
loading.setValue(false);
|
||||
Toaster.show("刷新成功");
|
||||
}
|
||||
}, new Consumer<Throwable>() {
|
||||
@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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 <T> Observable<T> schedule(Observable<T> observable, BehaviorSubject<ActivityEvent> lifecycle) {
|
||||
return observable
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.compose(RxLifecycle.bindUntilEvent(lifecycle, ActivityEvent.DESTROY));
|
||||
}
|
||||
|
||||
public Observable<BaseResponse<SnInfo>> getSnInfoControl() {
|
||||
return getSnApi()
|
||||
.getsninfo(Utils.getSerial())
|
||||
public <T> Observable<T> schedule(Observable<T> observable) {
|
||||
return observable
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
public Observable<BaseResponse<SnInfo>> getSnInfoControl() {
|
||||
return schedule(getSnApi().getsninfo(Utils.getSerial()));
|
||||
}
|
||||
|
||||
public Observable<BaseResponse<SystemSettingsSet>> getSettingControl() {
|
||||
return getSettingsApi()
|
||||
.getSetting(Utils.getSerial())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
return schedule(getSettingsApi().getSetting(Utils.getSerial()));
|
||||
}
|
||||
|
||||
public Observable<BaseResponse<List<AppInfo>>> getForceInstallControl() {
|
||||
return getAppApi()
|
||||
.getForceInstall(Utils.getSerial())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
return schedule(getAppApi().getForceInstall(Utils.getSerial()));
|
||||
}
|
||||
|
||||
public Observable<BaseResponse<BrowserApiData>> getBrowserControl() {
|
||||
return getSettingsApi()
|
||||
.getBrowser(Utils.getSerial())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
return schedule(getSettingsApi().getBrowser(Utils.getSerial()));
|
||||
}
|
||||
|
||||
public Observable<BaseResponse<Label>> getLabelControl() {
|
||||
return getSettingsApi()
|
||||
.getLabel(Utils.getSerial())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
return schedule(getSettingsApi().getLabel(Utils.getSerial()));
|
||||
}
|
||||
|
||||
public Observable<BaseResponse<List<AppStart>>> getAppStartControl() {
|
||||
return getSnControlApi()
|
||||
.getAppStatu(Utils.getSerial())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
return schedule(getSnControlApi().getAppStatu(Utils.getSerial()));
|
||||
}
|
||||
|
||||
public Observable<BaseResponse<List<AppInfo>>> getQueryAllAppControl() {
|
||||
return getAppApi()
|
||||
.getAllApp(Utils.getSerial())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
return schedule(getAppApi().getAllApp(Utils.getSerial()));
|
||||
}
|
||||
|
||||
public Observable<BaseResponse> getTimeControlObservable() {
|
||||
return getSnControlApi()
|
||||
.getTimeControl(Utils.getSerial())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
return schedule(getSnControlApi().getTimeControl(Utils.getSerial()));
|
||||
}
|
||||
|
||||
public Observable<BaseResponse<UserAvatarInfo>> getUserAvatarInfoControl() {
|
||||
return getSnApi()
|
||||
.getUserAvatarInfo(Utils.getSerial())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
return schedule(getSnApi().getUserAvatarInfo(Utils.getSerial()));
|
||||
}
|
||||
|
||||
public Observable<BaseResponse> getRunningAppObservable(String json) {
|
||||
return getAppApi()
|
||||
.sendAppInfo(Utils.getSerial(), json)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
return schedule(getAppApi().sendAppInfo(Utils.getSerial(), json));
|
||||
}
|
||||
|
||||
public Observable<BaseResponse<AppletQRCode>> getQRCodeApiControl() {
|
||||
return getFileApi()
|
||||
.getQRCodeApi()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
return schedule(getFileApi().getQRCodeApi());
|
||||
}
|
||||
|
||||
public Observable<BaseResponse> getGetGuideControl() {
|
||||
return getFileApi()
|
||||
.getGuidePic(2)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
return schedule(getFileApi().getGuidePic(2));
|
||||
}
|
||||
|
||||
public Observable<BaseResponse> getAppInsideControl() {
|
||||
return getSettingsApi()
|
||||
.getAppInside(Utils.getSerial())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
return schedule(getSettingsApi().getAppInside(Utils.getSerial()));
|
||||
}
|
||||
|
||||
public Observable<BaseResponse<BatchID>> getBatchApiControl() {
|
||||
return getSnApi()
|
||||
.getBatch(Utils.getSerial())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
return schedule(getSnApi().getBatch(Utils.getSerial()));
|
||||
}
|
||||
|
||||
public Observable<BaseResponse<CloudLessonSetting>> getCloudLessonSettingApiControl() {
|
||||
return getSettingsApi()
|
||||
.getCloudLessonSetting(Utils.getSerial())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
return schedule(getSettingsApi().getCloudLessonSetting(Utils.getSerial()));
|
||||
}
|
||||
|
||||
public Observable<BaseResponse<AppInfo>> 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<BaseResponse<List<OverallAppBean>>> getOverallAppObservable() {
|
||||
int platformCode = JgyUtils.getInstance().checkAppPlatform();
|
||||
return getAppApi()
|
||||
.getOverallApp(platformCode)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
return schedule(getAppApi().getOverallApp(platformCode));
|
||||
}
|
||||
|
||||
public Observable<BaseResponse<List<AppTimeControl>>> getAppTimeControlObservable() {
|
||||
return getSettingsApi()
|
||||
.getAppTimeControl(Utils.getSerial())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
return schedule(getSettingsApi().getAppTimeControl(Utils.getSerial()));
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Observable<BaseResponse<MachineControl>> getMachineTimeControlObservable() {
|
||||
return getSettingsApi()
|
||||
.getSnTimeControl(Utils.getSerial())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
return schedule(getSettingsApi().getSnTimeControl(Utils.getSerial()));
|
||||
}
|
||||
|
||||
public Observable<BaseResponse> 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<BaseResponse<List<MyAppUsageBean>>> getMyAppListObservable() {
|
||||
return getAppApi()
|
||||
.getMyAppList(Utils.getSerial())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
return schedule(getAppApi().getMyAppList(Utils.getSerial()));
|
||||
}
|
||||
|
||||
public Observable<BaseResponse<List<AppJump>>> getAppJumpObservable() {
|
||||
return getSnControlApi()
|
||||
.getSNJump(Utils.getSerial())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
return schedule(getSnControlApi().getSNJump(Utils.getSerial()));
|
||||
}
|
||||
|
||||
public Observable<BaseResponse<BlacklistBean>> getBlacklistObservable() {
|
||||
return getCommonApi()
|
||||
.getBlacklist()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
return schedule(getCommonApi().getBlacklist());
|
||||
}
|
||||
|
||||
public Observable<BaseResponse<List<AppInfo>>> getAdminAppObservable() {
|
||||
return getCommonApi()
|
||||
.getAdminApp(Utils.getSerial())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
return schedule(getCommonApi().getAdminApp(Utils.getSerial()));
|
||||
}
|
||||
|
||||
public Observable<BaseResponse<ScreenPassword>> getLockScreenPwdObservable() {
|
||||
return getSnControlApi()
|
||||
.getLockScreenPwd(Utils.getSerial())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
return schedule(getSnControlApi().getLockScreenPwd(Utils.getSerial()));
|
||||
}
|
||||
|
||||
public Observable<BaseResponse> getUpdateLockScreenObservable() {
|
||||
return getSnControlApi()
|
||||
.updateLockScreen(Utils.getSerial())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
return schedule(getSnControlApi().updateLockScreen(Utils.getSerial()));
|
||||
}
|
||||
|
||||
public Observable<BaseResponse<List<AppAndWhiteBean>>> getAppAndWhiteApiObservable() {
|
||||
return getCommonApi()
|
||||
.getAppAndWhite(Utils.getSerial())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
return schedule(getCommonApi().getAppAndWhite(Utils.getSerial()));
|
||||
}
|
||||
|
||||
public Observable<BaseResponse<SnBindState>> getSnBindObservable() {
|
||||
return getSnApi()
|
||||
.getSnBind(Utils.getSerial())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
return schedule(getSnApi().getSnBind(Utils.getSerial()));
|
||||
}
|
||||
|
||||
public Observable<BaseResponse<SnRunLog>> getSnRunLogControl() {
|
||||
return getSnLogApi()
|
||||
.getSnRunLog(Utils.getSerial())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
return schedule(getSnLogApi().getSnRunLog(Utils.getSerial()));
|
||||
}
|
||||
|
||||
public Observable<BaseResponse<SnTag>> getSnTag() {
|
||||
return getSnApi()
|
||||
.getTag(Utils.getSerial())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
return schedule(getSnApi().getTag(Utils.getSerial()));
|
||||
}
|
||||
|
||||
public Observable<BaseResponse<SnTimeControlInfo>> getSnTimeControlObservable() {
|
||||
return getControlApi()
|
||||
.getSnTimeControl(Utils.getSerial())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
return schedule(getControlApi().getSnTimeControl(Utils.getSerial()));
|
||||
}
|
||||
|
||||
public Observable<BaseResponse<List<AppTimeControlInfo>>> getAppTimeControlInfoObservable() {
|
||||
return getControlApi()
|
||||
.getAppTimeControl(Utils.getSerial())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
return schedule(getControlApi().getAppTimeControl(Utils.getSerial()));
|
||||
}
|
||||
|
||||
public Observable<BaseResponse<DeviceInfo>> getDeviceInfoObservable() {
|
||||
return schedule(getDeviceInfoApi().getDeviceInfo(Utils.getSerial()));
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -2441,23 +2367,18 @@ public class NetInterfaceManager {
|
||||
}
|
||||
|
||||
private Observable<Integer> getScreenshot(String filePath) {
|
||||
Observable<Integer> screenshotObservable = Observable.create(new ObservableOnSubscribe<Integer>() {
|
||||
@Override
|
||||
public void subscribe(ObservableEmitter<Integer> 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<Integer>() {
|
||||
@Override
|
||||
public void subscribe(ObservableEmitter<Integer> e) {
|
||||
int code = CmdUtil.execute("screencap -p " + filePath).code;
|
||||
e.onNext(code);
|
||||
e.onComplete();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private Observable<BaseResponse> 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<String, Long> 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 {
|
||||
|
||||
|
||||
@@ -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<BaseResponse> getDeviceInfo(
|
||||
Observable<BaseResponse<DeviceInfo>> getDeviceInfo(
|
||||
@Query("sn") String sn
|
||||
);
|
||||
|
||||
|
||||
@@ -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<BaseResponse> getControlStatus(
|
||||
Observable<BaseResponse<ControlStatusInfo>> getControlStatus(
|
||||
@Query("sn") String sn
|
||||
);
|
||||
|
||||
@GET(UrlAddress.REMAINING_TIME)
|
||||
Observable<BaseResponse> getRemainingTime(
|
||||
Observable<BaseResponse<RemainingTimeBean>> getRemainingTime(
|
||||
@Query("sn") String sn
|
||||
);
|
||||
|
||||
@GET(UrlAddress.APP_USE_TIME)
|
||||
Observable<BaseResponse> getAppUseTime(
|
||||
Observable<BaseResponse<AppUseTimeData>> getAppUseTime(
|
||||
@Query("sn") String sn,
|
||||
@Query("query_type") int query_type,
|
||||
@Query("is_all") int is_all
|
||||
);
|
||||
|
||||
@GET(UrlAddress.APP_CONTROL_DETAIL)
|
||||
Observable<BaseResponse> getAppControlDetail(
|
||||
Observable<BaseResponse<List<AppControlDetail>>> getAppControlDetail(
|
||||
@Query("sn") String sn
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
152
app/src/main/java/com/fuying/sn/utils/GlideLoadUtils.java
Normal file
152
app/src/main/java/com/fuying/sn/utils/GlideLoadUtils.java
Normal file
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Boolean> 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<Boolean> 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<Boolean>)
|
||||
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<String> getForbidList() {
|
||||
ArrayList<String> 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<String>() {
|
||||
@Override
|
||||
public void subscribe(ObservableEmitter<String> e) throws Exception {
|
||||
Log.e("RemoveTask", "subscribe: " + packageName);
|
||||
List<DownloadEntity> 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<String> e) throws Exception {
|
||||
Log.e("RemoveTask", "subscribe: " + packageName);
|
||||
List<DownloadEntity> 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<String>() {
|
||||
@Override
|
||||
|
||||
33
app/src/main/java/com/fuying/sn/utils/ScreenUtils.java
Normal file
33
app/src/main/java/com/fuying/sn/utils/ScreenUtils.java
Normal file
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<PartTime> time_period = appControlDetail.getTime_period();
|
||||
return getPartTimeString(time_period);
|
||||
}
|
||||
|
||||
public static String getPartTimeString(List<PartTime> 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<PartTime> 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取星期几
|
||||
*
|
||||
|
||||
@@ -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");
|
||||
|
||||
17
app/src/main/res/drawable/bt_refresh_normnl.xml
Normal file
17
app/src/main/res/drawable/bt_refresh_normnl.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 内部颜色 -->
|
||||
<solid android:color="@color/default_blue" />
|
||||
<!-- 圆角的幅度 -->
|
||||
<corners
|
||||
android:bottomLeftRadius="@dimen/dp_16"
|
||||
android:bottomRightRadius="@dimen/dp_16"
|
||||
android:topLeftRadius="@dimen/dp_16"
|
||||
android:topRightRadius="@dimen/dp_16" />
|
||||
|
||||
<padding
|
||||
android:bottom="@dimen/dp_4"
|
||||
android:left="@dimen/dp_16"
|
||||
android:right="@dimen/dp_16"
|
||||
android:top="@dimen/dp_4" />
|
||||
</shape>
|
||||
9
app/src/main/res/drawable/ic_map_location.xml
Normal file
9
app/src/main/res/drawable/ic_map_location.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="240dp"
|
||||
android:height="240dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
android:pathData="M702.93,777.58c-11.69,-3.16 -23.74,3.76 -26.9,15.45 -3.16,11.7 3.76,23.74 15.45,26.9 73.03,19.75 105.71,47.55 105.71,66.33 0,14.49 -19.8,37.05 -75.4,56.91 -55.7,19.9 -130.21,30.84 -209.79,30.84 -79.59,0 -154.1,-10.95 -209.8,-30.84 -55.59,-19.85 -75.4,-42.41 -75.4,-56.91 0,-18.79 32.7,-46.6 105.76,-66.35 11.7,-3.16 18.62,-15.2 15.46,-26.9 -3.16,-11.69 -15.2,-18.62 -26.9,-15.45 -89.12,24.08 -138.2,62.69 -138.2,108.7 0,26.87 17.36,47.17 31.92,59.46 17.48,14.76 41.9,27.81 72.59,38.77 60.32,21.54 140.06,33.41 224.55,33.41 84.49,0 164.24,-11.87 224.56,-33.41 30.69,-10.96 55.12,-24 72.6,-38.77 14.56,-12.3 31.92,-32.59 31.92,-59.46 0,-46 -49.06,-84.6 -138.14,-108.69v0zM422.05,826.72c23.42,26.84 56.22,42.22 90.12,42.22h0c34.03,0 66.8,-15.51 90.08,-42.57 7.06,-7.7 67.09,-73.79 127.92,-160.35 88.15,-125.43 132.84,-229.37 132.84,-308.92 0,-47.37 -9.28,-93.34 -27.6,-136.64 -17.68,-41.8 -42.99,-79.34 -75.21,-111.56 -32.22,-32.22 -69.75,-57.53 -111.55,-75.2 -43.3,-18.32 -89.28,-27.6 -136.64,-27.6 -47.37,0 -93.34,9.29 -136.64,27.6 -41.8,17.68 -79.34,42.99 -111.56,75.2 -32.22,32.23 -57.53,69.76 -75.2,111.56 -18.32,43.3 -27.6,89.27 -27.6,136.64 0,79.55 44.72,183.52 132.93,309.03 61.25,87.15 121.68,153.57 128.12,160.59v0zM375.91,347.29c0,-77.42 62.99,-140.4 140.4,-140.4 77.42,0 140.4,62.99 140.4,140.4 0,77.42 -62.98,140.4 -140.4,140.4 -77.41,0 -140.4,-62.99 -140.4,-140.4v0zM375.91,347.29z"
|
||||
android:fillColor="#3884FF" />
|
||||
</vector>
|
||||
17
app/src/main/res/drawable/tv_time_disable_background.xml
Normal file
17
app/src/main/res/drawable/tv_time_disable_background.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 内部颜色 -->
|
||||
<solid android:color="#DD5052" />
|
||||
<!-- 圆角的幅度 -->
|
||||
<corners
|
||||
android:bottomLeftRadius="@dimen/dp_16"
|
||||
android:bottomRightRadius="@dimen/dp_16"
|
||||
android:topLeftRadius="@dimen/dp_16"
|
||||
android:topRightRadius="@dimen/dp_16" />
|
||||
|
||||
<padding
|
||||
android:bottom="@dimen/dp_2"
|
||||
android:left="@dimen/dp_8"
|
||||
android:right="@dimen/dp_8"
|
||||
android:top="@dimen/dp_2" />
|
||||
</shape>
|
||||
17
app/src/main/res/drawable/tv_time_duration_background.xml
Normal file
17
app/src/main/res/drawable/tv_time_duration_background.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 内部颜色 -->
|
||||
<solid android:color="#FF9E61" />
|
||||
<!-- 圆角的幅度 -->
|
||||
<corners
|
||||
android:bottomLeftRadius="@dimen/dp_16"
|
||||
android:bottomRightRadius="@dimen/dp_16"
|
||||
android:topLeftRadius="@dimen/dp_16"
|
||||
android:topRightRadius="@dimen/dp_16" />
|
||||
|
||||
<padding
|
||||
android:bottom="@dimen/dp_2"
|
||||
android:left="@dimen/dp_4"
|
||||
android:right="@dimen/dp_4"
|
||||
android:top="@dimen/dp_2" />
|
||||
</shape>
|
||||
17
app/src/main/res/drawable/tv_time_period_background.xml
Normal file
17
app/src/main/res/drawable/tv_time_period_background.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 内部颜色 -->
|
||||
<solid android:color="#3ACEFD" />
|
||||
<!-- 圆角的幅度 -->
|
||||
<corners
|
||||
android:bottomLeftRadius="@dimen/dp_16"
|
||||
android:bottomRightRadius="@dimen/dp_16"
|
||||
android:topLeftRadius="@dimen/dp_16"
|
||||
android:topRightRadius="@dimen/dp_16" />
|
||||
|
||||
<padding
|
||||
android:bottom="@dimen/dp_2"
|
||||
android:left="@dimen/dp_4"
|
||||
android:right="@dimen/dp_4"
|
||||
android:top="@dimen/dp_2" />
|
||||
</shape>
|
||||
17
app/src/main/res/drawable/tv_time_unlimited_background.xml
Normal file
17
app/src/main/res/drawable/tv_time_unlimited_background.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 内部颜色 -->
|
||||
<solid android:color="#34CE85" />
|
||||
<!-- 圆角的幅度 -->
|
||||
<corners
|
||||
android:bottomLeftRadius="@dimen/dp_16"
|
||||
android:bottomRightRadius="@dimen/dp_16"
|
||||
android:topLeftRadius="@dimen/dp_16"
|
||||
android:topRightRadius="@dimen/dp_16" />
|
||||
|
||||
<padding
|
||||
android:bottom="@dimen/dp_2"
|
||||
android:left="@dimen/dp_4"
|
||||
android:right="@dimen/dp_4"
|
||||
android:top="@dimen/dp_2" />
|
||||
</shape>
|
||||
79
app/src/main/res/layout-port/activity_home.xml
Normal file
79
app/src/main/res/layout-port/activity_home.xml
Normal file
@@ -0,0 +1,79 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context=".activity.home.HomeActivity">
|
||||
|
||||
<data>
|
||||
|
||||
<variable
|
||||
name="click"
|
||||
type="com.fuying.sn.activity.home.HomeActivity.BtnClick" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:background="@drawable/main_background"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/constraintLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/dp_32"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="@dimen/dp_20"
|
||||
android:layout_height="@dimen/dp_20"
|
||||
android:layout_marginStart="16dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:onClick="@{click::exit}"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/icon_blue_back"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.flyco.tablayout.SlidingTabLayout
|
||||
android:id="@+id/main_sliding_tab_layout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="visible"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:tl_indicator_color="@color/default_blue"
|
||||
app:tl_indicator_height="@dimen/dp_2"
|
||||
app:tl_indicator_style="NORMAL"
|
||||
app:tl_indicator_width_equal_title="true"
|
||||
app:tl_tab_gravity="Center"
|
||||
app:tl_tab_horizontal_gravity="Center"
|
||||
app:tl_tab_space_equal="false"
|
||||
app:tl_textBold="SELECT"
|
||||
app:tl_textSelectColor="@color/black"
|
||||
app:tl_textSelectSize="@dimen/sp_13"
|
||||
app:tl_textSize="@dimen/sp_13"
|
||||
app:tl_textUnSelectColor="@color/gray"
|
||||
app:tl_textUnSelectSize="@dimen/sp_14" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/constraintLayout">
|
||||
|
||||
<androidx.viewpager.widget.ViewPager
|
||||
android:id="@+id/viewPager"
|
||||
android:layout_marginStart="@dimen/dp_32"
|
||||
android:layout_marginEnd="@dimen/dp_32"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
||||
140
app/src/main/res/layout/activity_usage_detail.xml
Normal file
140
app/src/main/res/layout/activity_usage_detail.xml
Normal file
@@ -0,0 +1,140 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context=".activity.usage.UsageDetailActivity">
|
||||
|
||||
<data>
|
||||
|
||||
<variable
|
||||
name="click"
|
||||
type="com.fuying.sn.activity.usage.UsageDetailActivity.BtnClick" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/main_background">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/cl_top_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/dp_32"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="@dimen/dp_20"
|
||||
android:layout_height="@dimen/dp_20"
|
||||
android:layout_marginStart="16dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:onClick="@{click::onBackClick}"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/icon_blue_back"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<net.lucode.hackware.magicindicator.MagicIndicator
|
||||
android:id="@+id/magic_indicator"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="@dimen/dp_20"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/cl_use_time"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="@dimen/dp_8"
|
||||
android:background="@drawable/card_background"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/cl_top_bar">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/constraintLayout1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/dp_8"
|
||||
android:layout_marginTop="@dimen/dp_8"
|
||||
android:layout_marginEnd="@dimen/dp_8"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_use_time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="整机使用时长:"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="@dimen/sp_12"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_total_time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/dp_8"
|
||||
android:text="@string/unknown"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="@dimen/sp_9"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="@id/tv_use_time"
|
||||
app:layout_constraintStart_toEndOf="@id/tv_use_time"
|
||||
app:layout_constraintTop_toTopOf="@id/tv_use_time" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1px"
|
||||
android:layout_marginStart="@dimen/dp_8"
|
||||
android:layout_marginTop="@dimen/dp_8"
|
||||
android:layout_marginEnd="@dimen/dp_8"
|
||||
android:background="@color/gray"
|
||||
app:layout_constraintTop_toBottomOf="@+id/constraintLayout1" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/barChart"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginHorizontal="@dimen/dp_8"
|
||||
android:layout_marginTop="@dimen/dp_8"
|
||||
android:layout_marginBottom="@dimen/dp_8"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/view">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rv_left"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginEnd="@dimen/dp_8"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rv_right"
|
||||
android:layout_width="0dp"
|
||||
android:layout_marginStart="@dimen/dp_8"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
||||
@@ -592,10 +592,10 @@
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/cl_update"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:id="@+id/cl_update">
|
||||
android:layout_weight="1">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
@@ -15,169 +15,413 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="@dimen/dp_8"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginEnd="@dimen/dp_4"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="3"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginBottom="@dimen/dp_4"
|
||||
android:layout_weight="4"
|
||||
android:background="@drawable/card_background"
|
||||
android:orientation="vertical">
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView5"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/dp_8"
|
||||
android:layout_marginTop="@dimen/dp_8"
|
||||
android:text="设备信息"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="@dimen/sp_12"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/constraintLayout4"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
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">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView11"
|
||||
android:layout_width="wrap_content"
|
||||
android:id="@+id/tv_sn"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/dp_16"
|
||||
android:text="设备码"
|
||||
android:text="SN码"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="@dimen/sp_10"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:textSize="@dimen/sp_9"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_goneMarginStart="@dimen/dp_16" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_imei"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/dp_16"
|
||||
android:ellipsize="end"
|
||||
android:maxLength="30"
|
||||
android:singleLine="true"
|
||||
android:text="@string/unknown"
|
||||
android:textColor="@color/text_gray"
|
||||
android:textSize="@dimen/sp_10"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1px"
|
||||
android:layout_marginStart="@dimen/dp_8"
|
||||
android:layout_marginEnd="@dimen/dp_8"
|
||||
android:background="@color/text_gray"
|
||||
android:visibility="visible"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/cl_number"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView8"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/dp_16"
|
||||
android:text="软件版本"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="@dimen/sp_10"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_goneMarginStart="@dimen/dp_16" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_app_version"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/dp_16"
|
||||
android:ellipsize="end"
|
||||
android:maxLength="30"
|
||||
android:singleLine="true"
|
||||
android:text="@string/unknown"
|
||||
android:textColor="@color/text_gray"
|
||||
android:textSize="@dimen/sp_10"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1px"
|
||||
android:layout_marginStart="@dimen/dp_8"
|
||||
android:layout_marginEnd="@dimen/dp_8"
|
||||
android:background="@color/text_gray"
|
||||
android:visibility="visible"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/cl_class"
|
||||
android:id="@+id/constraintLayout6"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
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">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView9"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/dp_16"
|
||||
android:text="系统版本"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="@dimen/sp_10"
|
||||
<ImageView
|
||||
android:id="@+id/imageView4"
|
||||
android:layout_width="@dimen/dp_10"
|
||||
android:layout_height="@dimen/dp_10"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/ic_map_location"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_goneMarginStart="@dimen/dp_16" />
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_build_version"
|
||||
android:id="@+id/tv_address"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="1"
|
||||
android:singleLine="true"
|
||||
android:textColor="@color/gray"
|
||||
android:textSize="@dimen/sp_9"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/imageView4"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="地址" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/view2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1px"
|
||||
android:layout_marginStart="@dimen/dp_8"
|
||||
android:layout_marginTop="@dimen/dp_4"
|
||||
android:layout_marginEnd="@dimen/dp_8"
|
||||
android:background="@color/gray"
|
||||
app:layout_constraintTop_toBottomOf="@+id/constraintLayout6"
|
||||
tools:layout_editor_absoluteX="9dp" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/dp_8"
|
||||
android:layout_marginEnd="@dimen/dp_8"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/view2">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_last_sync_time"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/dp_8"
|
||||
android:text="最新数据同步时间"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="@dimen/sp_7"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/textView6"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView6"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/dp_16"
|
||||
android:ellipsize="end"
|
||||
android:maxLength="30"
|
||||
android:singleLine="true"
|
||||
android:text="@string/unknown"
|
||||
android:background="@drawable/bt_refresh_normnl"
|
||||
android:onClick="@{click::onRefreshClick}"
|
||||
android:text="刷新"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="@dimen/sp_8"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/cl_use_time"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="@dimen/dp_4"
|
||||
android:layout_weight="7"
|
||||
android:background="@drawable/card_background"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/constraintLayout1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/dp_8"
|
||||
android:layout_marginTop="@dimen/dp_8"
|
||||
android:layout_marginEnd="@dimen/dp_8"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_use_time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/usage_statistics_title"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="@dimen/sp_12"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_view_all"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:onClick="@{click::onViewAllClick}"
|
||||
android:text="@string/usage_view_all"
|
||||
android:textColor="@color/gray"
|
||||
android:textSize="@dimen/sp_9"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1px"
|
||||
android:layout_marginStart="@dimen/dp_8"
|
||||
android:layout_marginTop="@dimen/dp_8"
|
||||
android:layout_marginEnd="@dimen/dp_8"
|
||||
android:background="@color/gray"
|
||||
app:layout_constraintTop_toBottomOf="@+id/constraintLayout1" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/constraintLayout3"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/dp_8"
|
||||
android:layout_marginTop="@dimen/dp_8"
|
||||
android:layout_marginEnd="@dimen/dp_8"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/view">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_total_time_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/usage_duration_label"
|
||||
android:textColor="@color/text_gray"
|
||||
android:textSize="@dimen/sp_10"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1px"
|
||||
<TextView
|
||||
android:id="@+id/tv_total_time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/dp_8"
|
||||
android:layout_marginEnd="@dimen/dp_8"
|
||||
android:background="@color/text_gray"
|
||||
android:visibility="gone"
|
||||
android:text="@string/unknown"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="@dimen/sp_9"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="@id/tv_total_time_label"
|
||||
app:layout_constraintStart_toEndOf="@id/tv_total_time_label"
|
||||
app:layout_constraintTop_toTopOf="@id/tv_total_time_label" />
|
||||
|
||||
<net.lucode.hackware.magicindicator.MagicIndicator
|
||||
android:id="@+id/magic_indicator"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="@dimen/dp_20"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/constraintLayout3"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</LinearLayout>
|
||||
<com.github.mikephil.charting.charts.HorizontalBarChart
|
||||
android:id="@+id/barChart"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginHorizontal="@dimen/dp_8"
|
||||
android:layout_marginTop="@dimen/dp_8"
|
||||
android:layout_marginBottom="@dimen/dp_8"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/constraintLayout3" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="@dimen/dp_4"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginBottom="@dimen/dp_4"
|
||||
android:layout_weight="2"
|
||||
android:background="@drawable/card_background"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/constraintLayout5"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/dp_8"
|
||||
android:layout_marginTop="@dimen/dp_8"
|
||||
android:layout_marginEnd="@dimen/dp_8"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView7"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="整机使用"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="@dimen/sp_12"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_control_type"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/dp_12"
|
||||
android:gravity="center"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="@dimen/sp_8"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/textView7"
|
||||
app:layout_constraintStart_toEndOf="@+id/textView7"
|
||||
app:layout_constraintTop_toTopOf="@+id/textView7"
|
||||
tools:text="地址" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/dp_8"
|
||||
android:layout_marginEnd="@dimen/dp_8"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/constraintLayout5">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_today_time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="@dimen/sp_9"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="SN码" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_remaining_time"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/dp_8"
|
||||
android:textColor="@color/gray"
|
||||
android:textSize="@dimen/sp_9"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/tv_today_time"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="剩余" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="@dimen/dp_4"
|
||||
android:layout_weight="7"
|
||||
android:background="@drawable/card_background"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView12"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/dp_8"
|
||||
android:layout_marginTop="@dimen/dp_8"
|
||||
android:text="应用控制"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="@dimen/sp_12"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="@dimen/dp_8"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView12" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_no_data"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="1"
|
||||
android:singleLine="true"
|
||||
android:textSize="@dimen/sp_11"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:text="当前pad无管控限制" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
||||
</layout>
|
||||
|
||||
115
app/src/main/res/layout/item_app_time_detail.xml
Normal file
115
app/src/main/res/layout/item_app_time_detail.xml
Normal file
@@ -0,0 +1,115 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/dp_40"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_icon"
|
||||
android:layout_width="@dimen/dp_28"
|
||||
android:layout_height="@dimen/dp_28"
|
||||
android:layout_marginStart="@dimen/dp_4"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/dp_4"
|
||||
android:layout_marginEnd="@dimen/dp_4"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="@dimen/sp_9"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/tv_type"
|
||||
app:layout_constraintStart_toEndOf="@+id/iv_icon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="AppName" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_type"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/tv_time_disable_background"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="@dimen/sp_6"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.464"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.516"
|
||||
tools:text="AppName" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/dp_4"
|
||||
android:layout_marginEnd="@dimen/dp_4"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/tv_type"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_today_time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:gravity="center"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="@dimen/sp_8"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="每天" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_remaining_time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/gray"
|
||||
android:textSize="@dimen/sp_8"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="剩余" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1px"
|
||||
android:layout_marginStart="@dimen/dp_8"
|
||||
android:layout_marginTop="@dimen/dp_8"
|
||||
android:layout_marginEnd="@dimen/dp_8"
|
||||
android:background="@color/lightGray"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
47
app/src/main/res/layout/item_app_use_time.xml
Normal file
47
app/src/main/res/layout/item_app_use_time.xml
Normal file
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="@dimen/dp_12"
|
||||
android:paddingVertical="@dimen/dp_8">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_icon"
|
||||
android:layout_width="@dimen/dp_28"
|
||||
android:layout_height="@dimen/dp_28"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@mipmap/ic_launcher" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/dp_8"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="#333333"
|
||||
android:textSize="@dimen/sp_12"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/tv_time"
|
||||
app:layout_constraintStart_toEndOf="@+id/iv_icon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="AI 问答" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="#666666"
|
||||
android:textSize="@dimen/sp_12"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="1小时20分" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -20,4 +20,14 @@
|
||||
|
||||
<string name="unknown">—</string>
|
||||
|
||||
<string name="usage_statistics_title">使用时长统计</string>
|
||||
<string name="usage_view_all">查看全部></string>
|
||||
<string name="usage_duration_label">使用时长</string>
|
||||
<string name="usage_today">今日</string>
|
||||
<string name="usage_week">本周</string>
|
||||
<string name="usage_month">本月</string>
|
||||
<string name="usage_hour">小时</string>
|
||||
<string name="usage_minute">分钟</string>
|
||||
<string name="usage_no_data">暂无数据</string>
|
||||
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user