fix(settings): 重构SN上报并清理失效启动页

- SettingsViewModel改用SnRepository进行设备注册
- 移除RxLifecycle手动绑定基类,改用AndroidX Lifecycle
- 新增系统、硬件、网络、安全、定位等SN信息上报接口
- 修复ContactInfo equals逻辑错误
- 应用列表变化后自动清理失效启动页
This commit is contained in:
TongTongStudio
2026-08-22 12:31:20 +08:00
parent bbf6cc6679
commit 5072917758
23 changed files with 432 additions and 754 deletions

View File

@@ -288,12 +288,6 @@ dependencies {
//Gson
implementation 'com.google.code.gson:gson:2.14.0'
implementation 'com.google.zxing:core:3.5.4'
//生命周期管理
implementation 'com.trello.rxlifecycle4:rxlifecycle:4.0.2'
implementation 'com.trello.rxlifecycle4:rxlifecycle-android:4.0.2'
implementation 'com.trello.rxlifecycle4:rxlifecycle-components:4.0.2'
implementation 'com.trello.rxlifecycle4:rxlifecycle-components-preference:4.0.2'
implementation 'com.trello.rxlifecycle4:rxlifecycle-android-lifecycle:4.0.2'
implementation 'com.jakewharton.rxbinding4:rxbinding:4.0.0'

View File

@@ -1,14 +1,25 @@
package com.ttstd.dialer.activity.settings.home;
import com.ttstd.dialer.base.mvvm.BaseViewModel;
import com.ttstd.dialer.bean.BaseResponse;
import com.ttstd.dialer.network.OkHttpManager;
import android.app.Application;
import android.os.Build;
import com.ttstd.dialer.base.mvvm.BaseAndroidViewModel;
import com.ttstd.dialer.data.repository.SnRepository;
import com.ttstd.dialer.mdm.DeviceManagerService;
import com.ttstd.dialer.utils.Logger;
public class SettingsViewModel extends BaseViewModel {
public class SettingsViewModel extends BaseAndroidViewModel {
private final SnRepository mRepository;
public SettingsViewModel(Application application) {
super(application);
mRepository = SnRepository.getInstance(application);
}
public void snRegister() {
addDisposable(OkHttpManager.getInstance().getSnRegisterObservable()
addDisposable(mRepository.register(
DeviceManagerService.getInstance().getSerial(), Build.MODEL)
.subscribe(baseResponse -> Logger.e("snRegister", "onNext: " + baseResponse),
throwable -> Logger.e("snRegister", "onError: " + throwable.getMessage())));
}

View File

@@ -6,16 +6,16 @@ import android.view.View;
import androidx.annotation.CallSuper;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.OnApplyWindowInsetsListener;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.ttstd.dialer.R;
import com.ttstd.dialer.base.rx.BaseRxActivity;
import com.zackratos.ultimatebarx.ultimatebarx.java.UltimateBarX;
public abstract class BaseDataBindingActivity extends BaseRxActivity {
public abstract class BaseDataBindingActivity extends AppCompatActivity {
public BaseDataBindingActivity() {
super();

View File

@@ -2,9 +2,9 @@ package com.ttstd.dialer.base;
import android.os.Bundle;
import com.ttstd.dialer.base.rx.BaseRxDialogFragment;
import androidx.fragment.app.DialogFragment;
public abstract class BaseDialogFragment extends BaseRxDialogFragment {
public abstract class BaseDialogFragment extends DialogFragment {
protected boolean isViewInitiated;
protected boolean isVisibleToUser;

View File

@@ -4,8 +4,7 @@ import android.os.Bundle;
import android.util.Log;
import androidx.annotation.Nullable;
import com.ttstd.dialer.base.rx.BaseRxFragment;
import androidx.fragment.app.Fragment;
/**
* Fragment 基类,提供标准的懒加载框架
@@ -20,7 +19,7 @@ import com.ttstd.dialer.base.rx.BaseRxFragment;
* 2. {@link #fetchData()} 每次可见都会回调,用于需要刷新的场景。
* 3. View 销毁重建后自动重置懒加载状态,保证重建后的页面能重新渲染。
*/
public abstract class BaseFragment extends BaseRxFragment {
public abstract class BaseFragment extends Fragment {
private static final String TAG = "BaseFragment";

View File

@@ -1,43 +1,18 @@
package com.ttstd.dialer.base;
import android.content.Intent;
import androidx.lifecycle.LifecycleService;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LifecycleRegistry;
/**
* Service 基类(基于 AndroidX {@link LifecycleService})。
* <p>
* 通过继承 {@link LifecycleService} 自动获得:
* - {@link androidx.lifecycle.LifecycleOwner} 能力
* - 生命周期事件自动派发:onCreate → ON_CREATE,onStartCommand → ON_START / ON_RESUME,onDestroy → ON_DESTROY
* (其中 ON_START / ON_RESUME 满足 Service 中 LiveData 的触发条件,无需再手动修补)
* - 不再需要手动维护 {@link androidx.lifecycle.LifecycleRegistry} 或覆写生命周期方法派发事件
* <p>
* 子类如需在生命周期回调中做处理,可直接覆写 onStartCommand / onDestroy 并调用 super。
*/
public abstract class BaseService extends LifecycleService {
import com.ttstd.dialer.base.rx.BaseRxService;
import org.jetbrains.annotations.NotNull;
public abstract class BaseService extends BaseRxService implements LifecycleOwner {
private final LifecycleRegistry lifecycleRegistry = new LifecycleRegistry(this);
@NotNull
@Override
public Lifecycle getLifecycle() {
return lifecycleRegistry;
}
@Override
public void onCreate() {
super.onCreate();
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
//修补在Service中LiveData不能触发
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME);
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
}
}

View File

@@ -6,16 +6,16 @@ import android.view.View;
import androidx.annotation.CallSuper;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.OnApplyWindowInsetsListener;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.ttstd.dialer.R;
import com.ttstd.dialer.base.rx.BaseRxActivity;
import com.zackratos.ultimatebarx.ultimatebarx.java.UltimateBarX;
public abstract class BaseTransparentActivity extends BaseRxActivity {
public abstract class BaseTransparentActivity extends AppCompatActivity {
public BaseTransparentActivity() {
super();

View File

@@ -1,94 +0,0 @@
package com.ttstd.dialer.base.rx;
import android.os.Bundle;
import androidx.annotation.CallSuper;
import androidx.annotation.CheckResult;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import com.trello.rxlifecycle4.LifecycleProvider;
import com.trello.rxlifecycle4.LifecycleTransformer;
import com.trello.rxlifecycle4.RxLifecycle;
import com.trello.rxlifecycle4.android.ActivityEvent;
import com.trello.rxlifecycle4.android.RxLifecycleAndroid;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.subjects.BehaviorSubject;
/**
* {@link com.trello.rxlifecycle4.components.RxActivity}
* copied form RxActivity}
*/
public abstract class BaseRxActivity extends AppCompatActivity implements LifecycleProvider<ActivityEvent> {
private final BehaviorSubject<ActivityEvent> lifecycleSubject = BehaviorSubject.create();
public BehaviorSubject<ActivityEvent> getLifecycleSubject() {
return lifecycleSubject;
}
@Override
@NonNull
@CheckResult
public final Observable<ActivityEvent> lifecycle() {
return lifecycleSubject.hide();
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindUntilEvent(@NonNull ActivityEvent event) {
return RxLifecycle.bindUntilEvent(lifecycleSubject, event);
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindToLifecycle() {
return RxLifecycleAndroid.bindActivity(lifecycleSubject);
}
@Override
@CallSuper
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
lifecycleSubject.onNext(ActivityEvent.CREATE);
}
@Override
@CallSuper
protected void onStart() {
super.onStart();
lifecycleSubject.onNext(ActivityEvent.START);
}
@Override
@CallSuper
protected void onResume() {
super.onResume();
lifecycleSubject.onNext(ActivityEvent.RESUME);
}
@Override
@CallSuper
protected void onPause() {
lifecycleSubject.onNext(ActivityEvent.PAUSE);
super.onPause();
}
@Override
@CallSuper
protected void onStop() {
lifecycleSubject.onNext(ActivityEvent.STOP);
super.onStop();
}
@Override
@CallSuper
protected void onDestroy() {
lifecycleSubject.onNext(ActivityEvent.DESTROY);
super.onDestroy();
}
}

View File

@@ -1,123 +0,0 @@
package com.ttstd.dialer.base.rx;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.CallSuper;
import androidx.annotation.CheckResult;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import com.trello.rxlifecycle4.LifecycleProvider;
import com.trello.rxlifecycle4.LifecycleTransformer;
import com.trello.rxlifecycle4.RxLifecycle;
import com.trello.rxlifecycle4.android.FragmentEvent;
import com.trello.rxlifecycle4.android.RxLifecycleAndroid;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.subjects.BehaviorSubject;
/**
* {@link com.trello.rxlifecycle4.components.RxFragment}
* copied form RxFragment}
*/
public class BaseRxDialogFragment extends DialogFragment implements LifecycleProvider<FragmentEvent> {
private final BehaviorSubject<FragmentEvent> lifecycleSubject = BehaviorSubject.create();
public BehaviorSubject<FragmentEvent> getLifecycleSubject() {
return lifecycleSubject;
}
@Override
@NonNull
@CheckResult
public final Observable<FragmentEvent> lifecycle() {
return lifecycleSubject.hide();
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindUntilEvent(@NonNull FragmentEvent event) {
return RxLifecycle.bindUntilEvent(lifecycleSubject, event);
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindToLifecycle() {
return RxLifecycleAndroid.bindFragment(lifecycleSubject);
}
@Override
@CallSuper
public void onAttach(android.app.Activity activity) {
super.onAttach(activity);
lifecycleSubject.onNext(FragmentEvent.ATTACH);
}
@Override
@CallSuper
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
lifecycleSubject.onNext(FragmentEvent.CREATE);
}
@Override
@CallSuper
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
lifecycleSubject.onNext(FragmentEvent.CREATE_VIEW);
}
@Override
@CallSuper
public void onStart() {
super.onStart();
lifecycleSubject.onNext(FragmentEvent.START);
}
@Override
@CallSuper
public void onResume() {
super.onResume();
lifecycleSubject.onNext(FragmentEvent.RESUME);
}
@Override
@CallSuper
public void onPause() {
lifecycleSubject.onNext(FragmentEvent.PAUSE);
super.onPause();
}
@Override
@CallSuper
public void onStop() {
lifecycleSubject.onNext(FragmentEvent.STOP);
super.onStop();
}
@Override
@CallSuper
public void onDestroyView() {
lifecycleSubject.onNext(FragmentEvent.DESTROY_VIEW);
super.onDestroyView();
}
@Override
@CallSuper
public void onDestroy() {
lifecycleSubject.onNext(FragmentEvent.DESTROY);
super.onDestroy();
}
@Override
@CallSuper
public void onDetach() {
lifecycleSubject.onNext(FragmentEvent.DETACH);
super.onDetach();
}
}

View File

@@ -1,123 +0,0 @@
package com.ttstd.dialer.base.rx;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.CallSuper;
import androidx.annotation.CheckResult;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.trello.rxlifecycle4.LifecycleProvider;
import com.trello.rxlifecycle4.LifecycleTransformer;
import com.trello.rxlifecycle4.RxLifecycle;
import com.trello.rxlifecycle4.android.FragmentEvent;
import com.trello.rxlifecycle4.android.RxLifecycleAndroid;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.subjects.BehaviorSubject;
/**
* {@link com.trello.rxlifecycle4.components.RxFragment}
* copied form RxFragment}
*/
public class BaseRxFragment extends Fragment implements LifecycleProvider<FragmentEvent> {
private final BehaviorSubject<FragmentEvent> lifecycleSubject = BehaviorSubject.create();
public BehaviorSubject<FragmentEvent> getLifecycleSubject() {
return lifecycleSubject;
}
@Override
@NonNull
@CheckResult
public final Observable<FragmentEvent> lifecycle() {
return lifecycleSubject.hide();
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindUntilEvent(@NonNull FragmentEvent event) {
return RxLifecycle.bindUntilEvent(lifecycleSubject, event);
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindToLifecycle() {
return RxLifecycleAndroid.bindFragment(lifecycleSubject);
}
@Override
@CallSuper
public void onAttach(android.app.Activity activity) {
super.onAttach(activity);
lifecycleSubject.onNext(FragmentEvent.ATTACH);
}
@Override
@CallSuper
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
lifecycleSubject.onNext(FragmentEvent.CREATE);
}
@Override
@CallSuper
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
lifecycleSubject.onNext(FragmentEvent.CREATE_VIEW);
}
@Override
@CallSuper
public void onStart() {
super.onStart();
lifecycleSubject.onNext(FragmentEvent.START);
}
@Override
@CallSuper
public void onResume() {
super.onResume();
lifecycleSubject.onNext(FragmentEvent.RESUME);
}
@Override
@CallSuper
public void onPause() {
lifecycleSubject.onNext(FragmentEvent.PAUSE);
super.onPause();
}
@Override
@CallSuper
public void onStop() {
lifecycleSubject.onNext(FragmentEvent.STOP);
super.onStop();
}
@Override
@CallSuper
public void onDestroyView() {
lifecycleSubject.onNext(FragmentEvent.DESTROY_VIEW);
super.onDestroyView();
}
@Override
@CallSuper
public void onDestroy() {
lifecycleSubject.onNext(FragmentEvent.DESTROY);
super.onDestroy();
}
@Override
@CallSuper
public void onDetach() {
lifecycleSubject.onNext(FragmentEvent.DETACH);
super.onDetach();
}
}

View File

@@ -1,56 +0,0 @@
package com.ttstd.dialer.base.rx;
import android.app.Service;
import androidx.annotation.CheckResult;
import androidx.annotation.NonNull;
import com.trello.rxlifecycle4.LifecycleProvider;
import com.trello.rxlifecycle4.LifecycleTransformer;
import com.trello.rxlifecycle4.RxLifecycle;
import com.trello.rxlifecycle4.android.ActivityEvent;
import com.trello.rxlifecycle4.android.RxLifecycleAndroid;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.subjects.BehaviorSubject;
public abstract class BaseRxService extends Service implements LifecycleProvider<ActivityEvent> {
private final BehaviorSubject<ActivityEvent> lifecycleSubject = BehaviorSubject.create();
public BehaviorSubject<ActivityEvent> getLifecycleSubject() {
return lifecycleSubject;
}
@Override
@NonNull
@CheckResult
public final Observable<ActivityEvent> lifecycle() {
return lifecycleSubject.hide();
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindUntilEvent(@NonNull ActivityEvent event) {
return RxLifecycle.bindUntilEvent(lifecycleSubject, event);
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindToLifecycle() {
return RxLifecycleAndroid.bindActivity(lifecycleSubject);
}
@Override
public void onCreate() {
super.onCreate();
lifecycleSubject.onNext(ActivityEvent.CREATE);
}
@Override
public void onDestroy() {
super.onDestroy();
lifecycleSubject.onNext(ActivityEvent.DESTROY);
}
}

View File

@@ -2,14 +2,28 @@ package com.ttstd.dialer.data.remote;
import androidx.annotation.NonNull;
import com.ttstd.dialer.bean.BaseResponse;
import com.ttstd.dialer.bean.DeveloperOptions;
import com.ttstd.dialer.bean.req.SnHardwareInfoReq;
import com.ttstd.dialer.bean.req.SnLocationReq;
import com.ttstd.dialer.bean.req.SnNetworkInfoReq;
import com.ttstd.dialer.bean.req.SnOtherInfoReq;
import com.ttstd.dialer.bean.req.SnSecurityInfoReq;
import com.ttstd.dialer.bean.req.SnSystemInfoReq;
import com.ttstd.dialer.network.OkHttpManager;
import com.ttstd.dialer.network.api.SnApi;
import java.util.List;
import io.reactivex.rxjava3.core.Observable;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
/**
* 设备(SN)远程数据源(Data Layer - Remote DataSource)。
* <p>
* 只负责发起网络请求并做基础解包,不做线程调度、不碰缓存。
* 所有线程调度与缓存策略由上层 {@link com.ttstd.dialer.data.repository.SnRepository} 统一处理。
*/
public class SnRemoteDataSource {
@@ -23,4 +37,89 @@ public class SnRemoteDataSource {
public Observable<DeveloperOptions> getDeveloperOptions() {
return ApiResponseMapper.unwrap(mApi.getDeveloperOptions());
}
/**
* 设备注册(sn + model)
*/
@NonNull
public Observable<BaseResponse> register(@NonNull String sn, @NonNull String model) {
return mApi.register(sn, model);
}
/**
* 上传设备基本信息
*/
@NonNull
public Observable<BaseResponse<Void>> updateSystemInfo(@NonNull SnSystemInfoReq req) {
return mApi.updateSystemInfo(OkHttpManager.convertToRequestBodyjson(req));
}
/**
* 上传设备硬件信息
*/
@NonNull
public Observable<BaseResponse<Void>> updateHardwareInfo(@NonNull SnHardwareInfoReq req) {
return mApi.updateHardwareInfo(OkHttpManager.convertToRequestBodyjson(req));
}
/**
* 上传设备网络信息
*/
@NonNull
public Observable<BaseResponse<Void>> updateNetworkInfo(@NonNull SnNetworkInfoReq req) {
return mApi.updateNetworkInfo(OkHttpManager.convertToRequestBodyjson(req));
}
/**
* 上传设备安全信息
*/
@NonNull
public Observable<BaseResponse<Void>> updateSecurityInfo(@NonNull SnSecurityInfoReq req) {
return mApi.updateSecurityInfo(OkHttpManager.convertToRequestBodyjson(req));
}
/**
* 上传设备其他信息
*/
@NonNull
public Observable<BaseResponse<Void>> updateOtherInfo(@NonNull SnOtherInfoReq req) {
return mApi.updateOtherInfo(OkHttpManager.convertToRequestBodyjson(req));
}
/**
* 上传定位信息
*/
@NonNull
public Observable<BaseResponse<Void>> uploadLocation(@NonNull SnLocationReq req) {
return mApi.uploadLocation(OkHttpManager.convertToRequestBodyjson(req));
}
/**
* 上传截图(或拍照文件)
*/
@NonNull
public Observable<BaseResponse> uploadScreenshot(@NonNull MultipartBody.Part part) {
return mApi.uploadScreenshot(part);
}
/**
* 上传已安装应用列表
*/
@NonNull
public Observable<BaseResponse> uploadInstallApks(@NonNull RequestBody body) {
return mApi.uploadInstallApks(body);
}
/**
* 上传应用图标(全局图标库)
*/
@NonNull
public Observable<BaseResponse> uploadAppIcon(@NonNull String packageName,
String appName,
@NonNull MultipartBody.Part part) {
return mApi.uploadAppIcon(
OkHttpManager.convertToRequestBody(packageName),
OkHttpManager.convertToRequestBody(appName == null ? "" : appName),
part);
}
}

View File

@@ -4,7 +4,14 @@ import android.content.Context;
import androidx.annotation.NonNull;
import com.ttstd.dialer.bean.BaseResponse;
import com.ttstd.dialer.bean.DeveloperOptions;
import com.ttstd.dialer.bean.req.SnHardwareInfoReq;
import com.ttstd.dialer.bean.req.SnLocationReq;
import com.ttstd.dialer.bean.req.SnNetworkInfoReq;
import com.ttstd.dialer.bean.req.SnOtherInfoReq;
import com.ttstd.dialer.bean.req.SnSecurityInfoReq;
import com.ttstd.dialer.bean.req.SnSystemInfoReq;
import com.ttstd.dialer.data.NetworkBoundResource;
import com.ttstd.dialer.data.Resource;
import com.ttstd.dialer.data.cache.CacheManager;
@@ -13,6 +20,9 @@ import com.ttstd.dialer.data.remote.SnRemoteDataSource;
import java.lang.reflect.Type;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.schedulers.Schedulers;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
/**
* 设备(SN)配置仓库(Data Layer - Repository)。
@@ -86,4 +96,98 @@ public class SnRepository {
}
}.asObservable();
}
// ==================== 设备上报类(写操作,fire-and-forget)====================
/**
* 设备注册(sn + model)。调用方按需 subscribe。
*/
@NonNull
public Observable<BaseResponse> register(@NonNull String sn, @NonNull String model) {
return mRemoteDataSource.register(sn, model)
.subscribeOn(Schedulers.io());
}
/**
* 上传设备基本信息
*/
@NonNull
public Observable<BaseResponse<Void>> updateSystemInfo(@NonNull SnSystemInfoReq req) {
return mRemoteDataSource.updateSystemInfo(req)
.subscribeOn(Schedulers.io());
}
/**
* 上传设备硬件信息
*/
@NonNull
public Observable<BaseResponse<Void>> updateHardwareInfo(@NonNull SnHardwareInfoReq req) {
return mRemoteDataSource.updateHardwareInfo(req)
.subscribeOn(Schedulers.io());
}
/**
* 上传设备网络信息
*/
@NonNull
public Observable<BaseResponse<Void>> updateNetworkInfo(@NonNull SnNetworkInfoReq req) {
return mRemoteDataSource.updateNetworkInfo(req)
.subscribeOn(Schedulers.io());
}
/**
* 上传设备安全信息
*/
@NonNull
public Observable<BaseResponse<Void>> updateSecurityInfo(@NonNull SnSecurityInfoReq req) {
return mRemoteDataSource.updateSecurityInfo(req)
.subscribeOn(Schedulers.io());
}
/**
* 上传设备其他信息
*/
@NonNull
public Observable<BaseResponse<Void>> updateOtherInfo(@NonNull SnOtherInfoReq req) {
return mRemoteDataSource.updateOtherInfo(req)
.subscribeOn(Schedulers.io());
}
/**
* 上传定位信息
*/
@NonNull
public Observable<BaseResponse<Void>> uploadLocation(@NonNull SnLocationReq req) {
return mRemoteDataSource.uploadLocation(req)
.subscribeOn(Schedulers.io());
}
/**
* 上传截图 / 拍照文件
*/
@NonNull
public Observable<BaseResponse> uploadScreenshot(@NonNull MultipartBody.Part part) {
return mRemoteDataSource.uploadScreenshot(part)
.subscribeOn(Schedulers.io());
}
/**
* 上传已安装应用列表(body 由调用方构建为 JSON RequestBody)
*/
@NonNull
public Observable<BaseResponse> uploadInstallApks(@NonNull RequestBody body) {
return mRemoteDataSource.uploadInstallApks(body)
.subscribeOn(Schedulers.io());
}
/**
* 上传应用图标(全局图标库)
*/
@NonNull
public Observable<BaseResponse> uploadAppIcon(@NonNull String packageName,
String appName,
@NonNull MultipartBody.Part part) {
return mRemoteDataSource.uploadAppIcon(packageName, appName, part)
.subscribeOn(Schedulers.io());
}
}

View File

@@ -203,7 +203,7 @@ public class ContactInfo implements Serializable {
public boolean equals(@Nullable Object obj) {
if (obj instanceof ContactInfo) {
return Objects.equals(((ContactInfo) obj).phoneNumber, phoneNumber)
|| ((ContactInfo) obj).id == id;
&& ((ContactInfo) obj).id == id;
}
return false;
}

View File

@@ -200,6 +200,8 @@ public class AppManager {
updatePositionSync();
}
reorderAppPositionsSync();
// 初始化时清理已失效的启动页
clearInvalidSplash();
notifyCompleted(true, "应用列表处理完成");
} catch (Exception e) {
Logger.e(TAG, "异步处理失败", e);
@@ -490,9 +492,63 @@ public class AppManager {
}
}
// app 安装信息变化后,清理已失效的启动页
clearInvalidSplash();
LiveDataBus.get().send(LiveDataAction.ACTION_UPDATE_APPS, TAG);
}
// ============================================================
// 启动页(开屏页)清理
// ============================================================
/**
* 清除数据库中不存在的启动页。
* <p>
* 遍历数据库中的所有 AppInfo(每条记录对应一个启动组件 ComponentName),
* 通过 PackageManager 校验组件是否仍可解析。当应用被卸载、更新导致 Activity
* 类名变化或被禁用时,对应的 ComponentName 将失效,此时从数据库删除该记录。
* <p>
* 在每次初始化以及 app 安装信息变化(updateApp)时调用。
*
* @return 本次删除的失效启动页数量
*/
public int clearInvalidSplash() {
if (mAppRepository == null) return 0;
try {
List<AppInfo> allApps = mAppRepository.getAllApp();
if (allApps == null || allApps.isEmpty()) return 0;
List<AppInfo> toDelete = new ArrayList<>();
for (AppInfo app : allApps) {
if (app == null) continue;
ComponentName componentName = app.getComponentName();
// 组件为空、或组件已不存在(Activity 被卸载/重命名/禁用)则标记为删除
if (componentName == null || !ApkUtils.isComponentExist(mContext, componentName)) {
toDelete.add(app);
}
}
if (toDelete.isEmpty()) return 0;
for (AppInfo app : toDelete) {
try {
mAppRepository.delete(app);
Logger.i(TAG, "清除失效启动页: " + (app.getComponentName() != null
? app.getComponentName().flattenToShortString() : "null"));
} catch (Exception e) {
Logger.e(TAG, "删除失效启动页失败: " + app.getPackageName(), e);
}
}
Logger.i(TAG, "共清除 " + toDelete.size() + " 条失效启动页");
LiveDataBus.get().send(LiveDataAction.ACTION_UPDATE_APPS, TAG);
return toDelete.size();
} catch (Exception e) {
Logger.e(TAG, "清除失效启动页异常", e);
return 0;
}
}
// 重构getAllApp方法,复用现有处理逻辑
/**
@@ -831,6 +887,8 @@ public class AppManager {
@Override
public void run() {
manager.reorderAppPositionsSync();
// 初始化时清理已失效的启动页
manager.clearInvalidSplash();
manager.notifyCompleted(true, "应用列表处理完成");
}
}).exceptionally(new java.util.function.Function<Throwable, Void>() {

View File

@@ -3,13 +3,10 @@ package com.ttstd.dialer.manager;
import android.content.Context;
import android.util.Log;
import com.trello.rxlifecycle4.RxLifecycle;
import com.trello.rxlifecycle4.android.ActivityEvent;
import com.ttstd.dialer.bean.BaseResponse;
import com.ttstd.dialer.data.Resource;
import com.ttstd.dialer.data.repository.ContactDataRepository;
import com.ttstd.dialer.db.contact.ContactInfo;
import com.ttstd.dialer.db.contact.ContactRepository;
import com.ttstd.dialer.network.BaseObserver;
import com.ttstd.dialer.network.OkHttpManager;
import com.ttstd.dialer.utils.Logger;
import java.util.HashMap;
@@ -25,7 +22,6 @@ import io.reactivex.rxjava3.core.ObservableOnSubscribe;
import io.reactivex.rxjava3.core.Observer;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
import io.reactivex.rxjava3.subjects.BehaviorSubject;
public class ContactManager {
private static final String TAG = "ContactManager";
@@ -57,84 +53,51 @@ public class ContactManager {
/**
* 获取联系人列表(优先从网络获取,失败则从本地数据库获取)
* <p>
* 统一走 {@link ContactDataRepository} 的多级缓存 + SWR:命中缓存秒出,
* 网络失败自动降级 Room 本地数据。旧逻辑在 Manager 层直连 OkHttpManager,
* 现收敛到 Data Layer(Repository),Manager 仅做回调转发。
*
* @param lifecycle RxLifecycle 生命周期绑定
* @param callback 回调接口
* @param callback 回调接口
*/
public void getContacts(BehaviorSubject<ActivityEvent> lifecycle, ContactCallback callback) {
public void getContacts(ContactCallback callback) {
Logger.e(TAG, "开始获取联系人列表");
OkHttpManager.getInstance().getContactListObservable(lifecycle)
.compose(RxLifecycle.bindUntilEvent(lifecycle, ActivityEvent.DESTROY))
.subscribe(new BaseObserver<BaseResponse<List<ContactInfo>>>() {
ContactDataRepository.getInstance(mContext).getContactList(false)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<Resource<List<ContactInfo>>>() {
@Override
public void onSuccess(BaseResponse<List<ContactInfo>> listBaseResponse) {
Log.e(TAG, "网络请求成功: " + listBaseResponse);
if (listBaseResponse.isSuccess()) {
List<ContactInfo> networkContacts = listBaseResponse.getData();
if (networkContacts != null && !networkContacts.isEmpty()) {
// 在子线程中处理数据库同步
Observable.create(new ObservableOnSubscribe<List<ContactInfo>>() {
@Override
public void subscribe(@NonNull ObservableEmitter<List<ContactInfo>> emitter) throws Throwable {
try {
// 同步网络和本地数据
syncContacts(networkContacts);
// 返回排序后的联系人列表
List<ContactInfo> sortedContacts = networkContacts.stream()
.sorted((c1, c2) -> Integer.compare(c1.getPosition(), c2.getPosition()))
.collect(Collectors.toList());
emitter.onNext(sortedContacts);
emitter.onComplete();
} catch (Exception e) {
emitter.onError(e);
}
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<List<ContactInfo>>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
Logger.e(TAG, "开始处理联系人数据");
}
public void onSubscribe(@NonNull Disposable d) {
Logger.e(TAG, "开始获取联系人");
}
@Override
public void onNext(@NonNull List<ContactInfo> contactInfos) {
Logger.e(TAG, "联系人数据处理完成,数量: " + contactInfos.size());
if (callback != null) {
callback.onSuccess(contactInfos);
}
}
@Override
public void onError(@NonNull Throwable e) {
Logger.e(TAG, "处理联系人数据失败: " + e.getMessage());
if (callback != null) {
callback.onFailure(e);
}
}
@Override
public void onComplete() {
Logger.e(TAG, "联系人数据处理完成");
}
});
} else {
// 网络返回空数据,从本地获取
getLocalContacts(callback);
@Override
public void onNext(@NonNull Resource<List<ContactInfo>> resource) {
if (resource.data != null) {
Logger.e(TAG, "联系人数据获取成功,数量: " + resource.data.size());
if (callback != null) {
callback.onSuccess(resource.data);
}
} else if (resource.isError()) {
Logger.e(TAG, "联系人获取失败: " + resource.message);
if (callback != null) {
callback.onFailure(new Exception(resource.message));
}
} else {
// 网络请求业务失败,从本地获取
getLocalContacts(callback);
}
}
@Override
public void onFailure(Throwable e) {
Log.e(TAG, "网络请求失败: " + e.getMessage());
// 网络请求失败,从本地获取
getLocalContacts(callback);
public void onError(@NonNull Throwable e) {
Logger.e(TAG, "联系人获取异常: " + e.getMessage());
if (callback != null) {
callback.onFailure(e);
}
}
@Override
public void onComplete() {
Logger.e(TAG, "联系人获取完成");
}
});
}

View File

@@ -2,10 +2,10 @@ package com.ttstd.dialer.manager;
import android.content.Context;
import com.ttstd.dialer.data.repository.ContactDataRepository;
import com.ttstd.dialer.db.contact.ContactInfo;
import com.ttstd.dialer.db.contact.ContactRepository;
import com.ttstd.dialer.db.contact.SyncStatus;
import com.ttstd.dialer.network.OkHttpManager;
import com.ttstd.dialer.utils.Logger;
import java.util.List;
@@ -19,11 +19,13 @@ public class ContactSyncManager {
private static final String TAG = "ContactSyncManager";
private static volatile ContactSyncManager INSTANCE;
private ContactRepository mRepository;
private CompositeDisposable mDisposable;
private final ContactRepository mRepository;
private final ContactDataRepository mDataRepository;
private final CompositeDisposable mDisposable;
private ContactSyncManager(Context context) {
mRepository = new ContactRepository(context);
mDataRepository = ContactDataRepository.getInstance(context);
mDisposable = new CompositeDisposable();
}
@@ -39,7 +41,8 @@ public class ContactSyncManager {
}
public void syncPendingContacts() {
Observable.fromCallable(() -> {
// 订阅结果加入 CompositeDisposable,dispose() 时可统一取消
mDisposable.add(Observable.fromCallable(() -> {
List<ContactInfo> pendingContacts = mRepository.getUnsyncedContacts();
Logger.d(TAG, "查询到 " + pendingContacts.size() + " 个待同步联系人");
return pendingContacts;
@@ -53,21 +56,22 @@ public class ContactSyncManager {
.subscribe(
result -> Logger.d(TAG, "同步完成: " + result),
throwable -> Logger.e(TAG, "同步流程异常: " + throwable.getMessage())
);
));
}
private Observable<String> syncSingleContactObservable(ContactInfo contactInfo) {
return OkHttpManager.getInstance().getContactInsertObservable(contactInfo)
// 统一走 Data Layer(Repository → RemoteDataSource),不再直连 OkHttpManager
return mDataRepository.addContact(contactInfo)
.subscribeOn(Schedulers.io())
.flatMap(baseResponse -> {
if (baseResponse.isSuccess()) {
Long onlineId = baseResponse.getData();
return saveSuccessSyncStatus(contactInfo.getId(), onlineId)
.flatMap(result -> {
if (result.online) {
// 服务端插入成功,更新同步状态并记录在线 id
return saveSuccessSyncStatus(contactInfo.getId(), result.id)
.map(v -> "成功: " + contactInfo.getName());
} else {
return saveFailedSyncStatus(contactInfo.getId())
.flatMap(v -> Observable.error(new Exception("业务失败: " + baseResponse.getMsg())));
}
// 网络不可用降级到本地保存,标记为失败待后续重试
return saveFailedSyncStatus(contactInfo.getId())
.flatMap(v -> Observable.error(new Exception("离线降级保存: " + contactInfo.getName())));
})
.onErrorResumeNext(throwable -> {
return saveFailedSyncStatus(contactInfo.getId())

View File

@@ -1,26 +1,11 @@
package com.ttstd.dialer.network;
import android.content.Context;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
import com.google.gson.Gson;
import com.trello.rxlifecycle4.RxLifecycle;
import com.trello.rxlifecycle4.android.ActivityEvent;
import com.ttstd.dialer.bean.BaseResponse;
import com.ttstd.dialer.bean.DeveloperOptions;
import com.ttstd.dialer.bean.req.SnHardwareInfoReq;
import com.ttstd.dialer.bean.req.SnLocationReq;
import com.ttstd.dialer.bean.req.SnNetworkInfoReq;
import com.ttstd.dialer.bean.req.SnOtherInfoReq;
import com.ttstd.dialer.bean.req.SnSecurityInfoReq;
import com.ttstd.dialer.bean.req.SnSystemInfoReq;
import com.ttstd.dialer.db.contact.ContactInfo;
import com.ttstd.dialer.mdm.DeviceManagerService;
import com.ttstd.dialer.network.api.ContactApi;
import com.ttstd.dialer.network.api.SnApi;
import com.ttstd.dialer.network.interceptor.AuthInterceptor;
import com.ttstd.dialer.utils.Logger;
import com.ttstd.iconloader.utils.FileUtils;
@@ -28,13 +13,8 @@ import com.ttstd.iconloader.utils.FileUtils;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.util.List;
import java.util.concurrent.TimeUnit;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.schedulers.Schedulers;
import io.reactivex.rxjava3.subjects.BehaviorSubject;
import okhttp3.Cache;
import okhttp3.ConnectionPool;
import okhttp3.Dispatcher;
@@ -50,6 +30,12 @@ import retrofit2.converter.gson.GsonConverterFactory;
/**
* OkHttp + Retrofit 管理类
* 单例模式 + 避免内存泄漏 + 增强异常处理 + 提升扩展性
*
* <p>职责收敛为「网络基础设施」:
* - 创建并持有 OkHttpClient / Retrofit
* - 通过 {@link #getApiService(Class)} 提供 Retrofit 接口实例
* - 提供 RequestBody / MultipartBody 转换工具
* <p>线程调度由 Data Layer(Repository / RemoteDataSource)统一负责,不再在此层处理。
*/
public class OkHttpManager {
private static final String TAG = "OkHttpManager";
@@ -121,8 +107,8 @@ public class OkHttpManager {
// 1. 构建调度器(控制并发)
Dispatcher dispatcher = new Dispatcher();
dispatcher.setMaxRequests(1); // 全局最大并发
dispatcher.setMaxRequestsPerHost(1); // 单Host最大并发
// dispatcher.setMaxRequests(1); // 全局最大并发
// dispatcher.setMaxRequestsPerHost(1); // 单Host最大并发
// 2. 构建缓存(增强异常处理)
Cache cache = createCache();
@@ -179,8 +165,6 @@ public class OkHttpManager {
}
Cache cache = new Cache(cacheDir, CACHE_SIZE_BYTES);
// 验证缓存是否可用(避免创建了但不可用的情况)
// cache.getCacheDirectory().getFreeSpace();
return cache;
} catch (SecurityException e) {
Log.e(TAG, "No permission to create cache directory", e);
@@ -248,6 +232,13 @@ public class OkHttpManager {
}
}
/**
* 获取 Retrofit API 接口实例(Data Layer 通过它创建 RemoteDataSource)
*/
public <T> T getApiService(Class<T> tClass) {
return mRetrofit.create(tClass);
}
public static MultipartBody.Part getBodyPart(String type, String name, File file) {
MediaType mediaType = MediaType.Companion.parse(type);
RequestBody fileBody = RequestBody.Companion.create(file, mediaType);
@@ -284,148 +275,4 @@ public class OkHttpManager {
RequestBody requestBody = RequestBody.create(MediaType.parse("text/plain"), String.valueOf(param));
return requestBody;
}
/**
* 统一线程调度 + 生命周期管理
*
* @param observable 接口Observable
* @param provider 生命周期管理者(Activity/Fragment)
* @return 处理后的Observable
*/
public <T> Observable<T> schedule(Observable<T> observable, BehaviorSubject<ActivityEvent> provider) {
return observable
// 子线程执行请求
.subscribeOn(Schedulers.io())
// 主线程回调
.observeOn(AndroidSchedulers.mainThread())
// 生命周期管理:页面销毁自动取消请求,防止内存泄漏
.compose(RxLifecycle.bindUntilEvent(provider, ActivityEvent.DESTROY));
}
public <T> T getApiService(Class<T> tClass) {
return mRetrofit.create(tClass);
}
public <T> Observable<T> schedule(Observable<T> observable) {
return observable
// 子线程执行请求
.subscribeOn(Schedulers.io())
// 主线程回调
.observeOn(AndroidSchedulers.mainThread());
}
public Observable<BaseResponse> getSnRegisterObservable(BehaviorSubject<ActivityEvent> provider) {
return schedule(getApiService(SnApi.class).register(DeviceManagerService.getInstance().getSerial(), Build.MODEL), provider);
}
/**
* 设备注册(由 ViewModel 通过 addDisposable 管理订阅生命周期)
*/
public Observable<BaseResponse> getSnRegisterObservable() {
return schedule(getApiService(SnApi.class).register(DeviceManagerService.getInstance().getSerial(), Build.MODEL));
}
/**
* 上传设备基本信息
*/
public Observable<BaseResponse<Void>> getUpdateSystemInfoObservable(SnSystemInfoReq req) {
RequestBody body = convertToRequestBodyjson(req);
return schedule(getApiService(SnApi.class).updateSystemInfo(body));
}
/**
* 上传设备硬件信息
*/
public Observable<BaseResponse<Void>> getUpdateHardwareInfoObservable(SnHardwareInfoReq req) {
RequestBody body = convertToRequestBodyjson(req);
return schedule(getApiService(SnApi.class).updateHardwareInfo(body));
}
/**
* 上传设备网络信息
*/
public Observable<BaseResponse<Void>> getUpdateNetworkInfoObservable(SnNetworkInfoReq req) {
RequestBody body = convertToRequestBodyjson(req);
return schedule(getApiService(SnApi.class).updateNetworkInfo(body));
}
/**
* 上传设备安全信息
*/
public Observable<BaseResponse<Void>> getUpdateSecurityInfoObservable(SnSecurityInfoReq req) {
RequestBody body = convertToRequestBodyjson(req);
return schedule(getApiService(SnApi.class).updateSecurityInfo(body));
}
/**
* 上传设备其他信息
*/
public Observable<BaseResponse<Void>> getUpdateOtherInfoObservable(SnOtherInfoReq req) {
RequestBody body = convertToRequestBodyjson(req);
return schedule(getApiService(SnApi.class).updateOtherInfo(body));
}
public Observable<BaseResponse> getUploadScreenshotObservable(MultipartBody.Part part) {
return schedule(getApiService(SnApi.class).uploadScreenshot(part));
}
public Observable<BaseResponse> getUploadLocationObservable(SnLocationReq locationReq, BehaviorSubject<ActivityEvent> provider) {
return schedule(getApiService(SnApi.class).uploadLocation(convertToRequestBodyjson(locationReq)), provider);
}
public Observable<BaseResponse> getUploadLocationObservable(SnLocationReq locationReq) {
return schedule(getApiService(SnApi.class).uploadLocation(convertToRequestBodyjson(locationReq)));
}
public Observable<BaseResponse<DeveloperOptions>> getDeveloperOptionsObservable(BehaviorSubject<ActivityEvent> provider) {
return schedule(getApiService(SnApi.class).getDeveloperOptions(), provider);
}
public Observable<BaseResponse<DeveloperOptions>> getDeveloperOptionsObservable() {
return schedule(getApiService(SnApi.class).getDeveloperOptions());
}
public Observable<BaseResponse> getUploadInstallApksObservable(RequestBody body) {
return schedule(getApiService(SnApi.class).uploadInstallApks(body));
}
/**
* 上传应用图标(全局图标库,packageName/appName 作为 form 字段,file 作为文件 part)
* <p>
* 注意:Retrofit 的 @Part 不允许传 null,appName 为空时统一转为空串,
* 由服务端按「未上报」处理(沿用已有名称)。
*/
public Observable<BaseResponse> getUploadAppIconObservable(String packageName, String appName, MultipartBody.Part part) {
MediaType textPlain = MediaType.Companion.parse("text/plain");
RequestBody packageNameBody = RequestBody.Companion.create(packageName, textPlain);
RequestBody appNameBody = RequestBody.Companion.create(appName == null ? "" : appName, textPlain);
return schedule(getApiService(SnApi.class).uploadAppIcon(packageNameBody, appNameBody, part));
}
public Observable<BaseResponse<List<ContactInfo>>> getContactListObservable(BehaviorSubject<ActivityEvent> provider) {
return schedule(getApiService(ContactApi.class).getContactList(), provider);
}
public Observable<BaseResponse<Long>> getContactInsertObservable(ContactInfo contactInfo, BehaviorSubject<ActivityEvent> provider) {
return schedule(getApiService(ContactApi.class).insertContact(convertToRequestBodyjson(contactInfo)), provider);
}
public Observable<BaseResponse<Long>> getContactInsertObservable(ContactInfo contactInfo) {
return schedule(getApiService(ContactApi.class).insertContact(convertToRequestBodyjson(contactInfo)));
}
public Observable<BaseResponse<ContactInfo>> getContactFormObservable(long id, BehaviorSubject<ActivityEvent> provider) {
return schedule(getApiService(ContactApi.class).getContactForm(id), provider);
}
public Observable<BaseResponse<Void>> getContactUpdateObservable(long id, ContactInfo contactInfo, BehaviorSubject<ActivityEvent> provider) {
return schedule(getApiService(ContactApi.class).updateContact(id, convertToRequestBodyjson(contactInfo)), provider);
}
public Observable<BaseResponse<Void>> getContactDeleteObservable(long id, BehaviorSubject<ActivityEvent> provider) {
return schedule(getApiService(ContactApi.class).deleteContact(id), provider);
}
}
}

View File

@@ -78,7 +78,7 @@ public interface SnApi {
);
@POST(UrlConstants.UPLOAD_LOCATION)
Observable<BaseResponse> uploadLocation(
Observable<BaseResponse<Void>> uploadLocation(
@Body RequestBody body
);

View File

@@ -87,12 +87,16 @@ public class PushExecutor {
*/
private List<ApkInstalledInfo> mApkInstalledInfos;
private SnRepository mSnRepository;
private PushExecutor(Context context) {
if (context == null) {
throw new RuntimeException("Context is NULL");
}
this.mContext = context;
this.mPackageManager = mContext.getPackageManager();
// 数据访问统一走 Data Layer(Repository),避免直连 OkHttpManager
this.mSnRepository = SnRepository.getInstance(mContext);
}
public static void init(Context context) {
@@ -203,7 +207,7 @@ public class PushExecutor {
public Observable<BaseResponse> apply(List<ApkInstalledInfo> apkInstalledInfos) throws Throwable {
mApkInstalledInfos = apkInstalledInfos;
RequestBody body = OkHttpManager.convertToRequestBodyjson(apkInstalledInfos);
return OkHttpManager.getInstance().getUploadInstallApksObservable(body);
return mSnRepository.uploadInstallApks(body);
}
})
.subscribeOn(Schedulers.io())
@@ -279,8 +283,7 @@ public class PushExecutor {
return Observable.empty();
}
MultipartBody.Part part = OkHttpManager.getBodyPart("image/png", "file", iconFile);
return OkHttpManager.getInstance()
.getUploadAppIconObservable(packageName, apkInfo.getAppName(), part)
return mSnRepository.uploadAppIcon(packageName, apkInfo.getAppName(), part)
// getUploadAppIconObservable 内部已 observeOn(mainThread),
// 这里必须切回 IO 线程,否则下面的 Room 写入会发生在主线程
.observeOn(Schedulers.io())
@@ -323,33 +326,33 @@ public class PushExecutor {
*/
private void uploadAllDeviceInfo() {
DeviceInfoCollector collector = new DeviceInfoCollector(mContext);
OkHttpManager http = OkHttpManager.getInstance();
// defer + fromCallable 保证采集动作发生在订阅时的 IO 线程,而非当前线程
// 上报统一走 Data Layer(SnRepository → SnRemoteDataSource),不再直连 OkHttpManager
Observable<BaseResponse<Void>> system = Observable
.fromCallable(collector::collectSystemInfo)
.subscribeOn(Schedulers.io())
.flatMap(http::getUpdateSystemInfoObservable);
.flatMap(mSnRepository::updateSystemInfo);
Observable<BaseResponse<Void>> hardware = Observable
.fromCallable(collector::collectHardwareInfo)
.subscribeOn(Schedulers.io())
.flatMap(http::getUpdateHardwareInfoObservable);
.flatMap(mSnRepository::updateHardwareInfo);
Observable<BaseResponse<Void>> network = Observable
.fromCallable(collector::collectNetworkInfo)
.subscribeOn(Schedulers.io())
.flatMap(http::getUpdateNetworkInfoObservable);
.flatMap(mSnRepository::updateNetworkInfo);
Observable<BaseResponse<Void>> security = Observable
.fromCallable(collector::collectSecurityInfo)
.subscribeOn(Schedulers.io())
.flatMap(http::getUpdateSecurityInfoObservable);
.flatMap(mSnRepository::updateSecurityInfo);
Observable<BaseResponse<Void>> other = Observable
.fromCallable(collector::collectOtherInfo)
.subscribeOn(Schedulers.io())
.flatMap(http::getUpdateOtherInfoObservable);
.flatMap(mSnRepository::updateOtherInfo);
Observable.mergeDelayError(
Observable.just(system, hardware, network, security, other))
@@ -395,7 +398,7 @@ public class PushExecutor {
if (integer == 0) {
// 截图成功,上传文件
MultipartBody.Part part = OkHttpManager.getBodyPart("image/png", "file", new File(filepath));
return OkHttpManager.getInstance().getUploadScreenshotObservable(part)
return mSnRepository.uploadScreenshot(part)
.subscribeOn(Schedulers.io()); // 确保上传操作在子线程执行
} else {
// 截图失败,返回一个空的Observable
@@ -436,7 +439,7 @@ public class PushExecutor {
.flatMap(integer -> {
if (integer == 0) {
MultipartBody.Part part = OkHttpManager.getBodyPart("image/jpeg", "file", file);
return OkHttpManager.getInstance().getUploadScreenshotObservable(part)
return mSnRepository.uploadScreenshot(part)
.subscribeOn(Schedulers.io());
} else {
return Observable.empty();

View File

@@ -27,6 +27,7 @@ import com.ttstd.dialer.activity.main.MainActivity;
import com.ttstd.dialer.base.BaseService;
import com.ttstd.dialer.bean.req.SnLocationReq;
import com.ttstd.dialer.config.CommonConfig;
import com.ttstd.dialer.data.repository.SnRepository;
import com.ttstd.dialer.manager.ContactSyncManager;
import com.ttstd.dialer.manager.MqttManager;
import com.ttstd.dialer.utils.ApkUtils;
@@ -69,6 +70,7 @@ public class MainService extends BaseService implements NetworkUtils.OnNetworkSt
@Nullable
@Override
public IBinder onBind(Intent intent) {
super.onBind(intent);
return null;
}
@@ -81,8 +83,8 @@ public class MainService extends BaseService implements NetworkUtils.OnNetworkSt
// 初始化ViewModel(绑定上下文+View回调)
mViewModel = new MainServiceModel();
// 绑定Service生命周期(关键:让请求跟随生命周期)
mViewModel.setLifecycleSubject(getLifecycleSubject());
// 注入 Repository(数据访问统一走 Data 层,避免 ViewModel 直接触网)
mViewModel.setSnRepository(SnRepository.getInstance(this));
LiveEventBus.get(CommonConfig.LOCATION_CHANGED_KEY, SnLocationReq.class)
.observeSticky(this, new Observer<SnLocationReq>() {

View File

@@ -1,47 +1,36 @@
package com.ttstd.dialer.service.main;
import android.text.TextUtils;
import android.util.Log;
import androidx.lifecycle.ViewModel;
import com.trello.rxlifecycle4.LifecycleTransformer;
import com.trello.rxlifecycle4.RxLifecycle;
import com.trello.rxlifecycle4.android.ActivityEvent;
import com.ttstd.dialer.bean.BaseResponse;
import com.ttstd.dialer.bean.req.SnLocationReq;
import com.ttstd.dialer.data.repository.SnRepository;
import com.ttstd.dialer.gson.GsonUtils;
import com.ttstd.dialer.network.BaseObserver;
import com.ttstd.dialer.network.OkHttpManager;
import io.reactivex.rxjava3.subjects.BehaviorSubject;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.observers.DisposableObserver;
public class MainServiceModel extends ViewModel {
private static final String TAG = "MainServiceModel";
private BehaviorSubject<ActivityEvent> lifecycleSubject;
private final CompositeDisposable mDisposables = new CompositeDisposable();
private SnRepository mSnRepository;
// 设置Service生命周期Subject
public void setLifecycleSubject(BehaviorSubject<ActivityEvent> lifecycleSubject) {
this.lifecycleSubject = lifecycleSubject;
}
// 绑定请求到Service销毁事件(自动取消请求)
private <T> LifecycleTransformer<T> bindToLifecycle() {
if (lifecycleSubject == null) {
throw new IllegalStateException("请先设置LifecycleSubject!");
}
return RxLifecycle.bindUntilEvent(lifecycleSubject, ActivityEvent.DESTROY);
public void setSnRepository(SnRepository snRepository) {
this.mSnRepository = snRepository;
}
@Override
protected void onCleared() {
// ViewModel 销毁(Service 停止)时自动取消进行中的上报,避免内存泄漏
if (!mDisposables.isDisposed()) {
mDisposables.clear();
}
super.onCleared();
// 清空生命周期引用,防止内存泄漏
this.lifecycleSubject = null;
}
public void uploadLocation(SnLocationReq locationReq) {
// 空校验:位置信息缺失时不发起请求,避免后端收到无效数据
if (locationReq == null) {
@@ -49,19 +38,26 @@ public class MainServiceModel extends ViewModel {
return;
}
Log.e(TAG, "uploadLocation: " + GsonUtils.toJSONString(locationReq));
OkHttpManager.getInstance().getUploadLocationObservable(locationReq)
.compose(bindToLifecycle())
.subscribe(new BaseObserver<BaseResponse>() {
if (mSnRepository == null) {
Log.e(TAG, "uploadLocation: SnRepository 未注入,跳过");
return;
}
mDisposables.add(mSnRepository.uploadLocation(locationReq)
.subscribeWith(new DisposableObserver<BaseResponse<Void>>() {
@Override
public void onSuccess(BaseResponse baseResponse) {
Log.e("uploadLocation", "onSuccess: " + baseResponse);
public void onNext(BaseResponse<Void> baseResponse) {
Log.e("uploadLocation", "onNext: " + baseResponse);
}
@Override
public void onFailure(Throwable e) {
Log.e("uploadLocation", "onFailure: " + e.getMessage());
public void onError(Throwable e) {
Log.e("uploadLocation", "onError: " + e.getMessage());
}
});
@Override
public void onComplete() {
Log.e("uploadLocation", "onComplete");
}
}));
}
}

View File

@@ -16,6 +16,7 @@ import android.net.Uri;
import android.os.Build;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.kongzue.dialogx.dialogs.PopTip;
@@ -37,6 +38,24 @@ public class ApkUtils {
}
}
/**
* 判断指定组件(Activity)是否仍然存在且可被解析。
* 应用卸载、更新导致 Activity 类名变化或被禁用时,均会返回 false。
*
* @param context 上下文
* @param componentName 待校验的组件
* @return true 表示组件仍存在
*/
public static boolean isComponentExist(Context context, @Nullable ComponentName componentName) {
if (context == null || componentName == null) return false;
try {
ActivityInfo info = context.getPackageManager().getActivityInfo(componentName, 0);
return info != null;
} catch (PackageManager.NameNotFoundException e) {
return false;
}
}
/**
* 判断是否为系统应用
*