diff --git a/app/build.gradle b/app/build.gradle index 1c2fc3a..5c8d7c1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,7 +16,7 @@ android { applicationId "com.ttstd.dialer" //There are no CERT files because If the mini sdk version is 23+, the AGP will ignore the V1 scheme signature. - minSdkVersion 23 + minSdkVersion 22 targetSdkVersion 29 versionCode 1 versionName "1.0" @@ -25,6 +25,8 @@ android { vectorDrawables.useSupportLibrary = true + multiDexEnabled true + compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -194,6 +196,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.3.1' //2.0.4以上无法预览 implementation 'androidx.constraintlayout:constraintlayout:2.0.4' + implementation "androidx.multidex:multidex:2.0.1" implementation "androidx.recyclerview:recyclerview:1.2.1" // For control over item selection of both touch and mouse driven selection implementation "androidx.recyclerview:recyclerview-selection:1.1.0" @@ -210,6 +213,8 @@ dependencies { implementation "androidx.lifecycle:lifecycle-livedata:2.3.0" implementation "androidx.lifecycle:lifecycle-runtime:2.3.0" annotationProcessor "androidx.lifecycle:lifecycle-compiler:2.3.0" + // LifecycleService 核心库 + implementation "androidx.lifecycle:lifecycle-service:2.3.0" // 添加这行,使用 BOM 统一 Kotlin 相关库的版本 implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.10")) @@ -222,7 +227,7 @@ dependencies { //glide implementation 'com.github.bumptech.glide:glide:4.13.1' annotationProcessor 'com.github.bumptech.glide:compiler:4.13.1' - // SVG支持库 + // SVG支持库f implementation 'com.caverock:androidsvg:1.4' // 如果需要,添加Glide SVG模块(社区版) // implementation 'com.github.qoqa:glide-svg:2.0.4' @@ -231,7 +236,8 @@ dependencies { implementation 'io.reactivex.rxjava3:rxjava:3.0.0' implementation 'io.reactivex.rxjava3:rxandroid:3.0.0' // - implementation 'com.squareup.okhttp3:okhttp:4.7.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + implementation 'com.squareup.okhttp3:logging-interceptor:4.9.0' implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' // implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0' @@ -266,6 +272,7 @@ dependencies { //推送 //阿里云 implementation 'com.aliyun.ams:alicloud-android-push:3.9.3' + //极光 // 此处以JPush 5.6.0 版本为例,注意:从 5.0.0 版本开始可以自动拉取 JCore 包,无需另外配置 implementation "cn.jiguang.sdk:jpush:$jpush.version" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2874ff8..9567e3a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,12 @@ package="com.ttstd.dialer" android:sharedUserId="android.uid.system"> + + + + + + @@ -15,7 +21,8 @@ - + + @@ -48,9 +55,9 @@ diff --git a/app/src/main/java/com/ttstd/dialer/activity/app/AppListViewModel.java b/app/src/main/java/com/ttstd/dialer/activity/app/AppListViewModel.java index 35c7e94..980ca60 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/app/AppListViewModel.java +++ b/app/src/main/java/com/ttstd/dialer/activity/app/AppListViewModel.java @@ -1,9 +1,6 @@ package com.ttstd.dialer.activity.app; -import android.content.ComponentName; import android.content.Context; -import android.content.pm.ResolveInfo; -import android.util.Log; import androidx.lifecycle.MutableLiveData; @@ -13,21 +10,16 @@ import com.ttstd.dialer.base.mvvm.BaseViewModel; import com.ttstd.dialer.databinding.ActivityAppListBinding; import com.ttstd.dialer.db.app.AppInfo; import com.ttstd.dialer.db.app.AppRepository; -import com.ttstd.dialer.utils.ApkUtils; +import com.ttstd.dialer.utils.Logger; -import java.text.Collator; -import java.util.Comparator; import java.util.List; -import java.util.Locale; import java.util.concurrent.Callable; -import java.util.stream.Collectors; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.annotations.NonNull; import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.core.Observer; import io.reactivex.rxjava3.disposables.Disposable; -import io.reactivex.rxjava3.functions.Function; import io.reactivex.rxjava3.schedulers.Schedulers; public class AppListViewModel extends BaseViewModel { @@ -55,23 +47,23 @@ public class AppListViewModel extends BaseViewModel>() { @Override public void onSubscribe(@NonNull Disposable d) { - Log.e("getDbAppList", "onSubscribe: "); + Logger.e("getDbAppList", "onSubscribe: "); } @Override public void onNext(@NonNull List appInfos) { - Log.e("getDbAppList", "onNext: " + appInfos); + Logger.e("getDbAppList", "onNext: " + appInfos); mDesktopSortAppData.setValue(appInfos); } @Override public void onError(@NonNull Throwable e) { - Log.e("getDbAppList", "onError: " + e.getMessage()); + Logger.e("getDbAppList", "onError: " + e.getMessage()); } @Override public void onComplete() { - Log.e("getDbAppList", "onComplete: "); + Logger.e("getDbAppList", "onComplete: "); } }); } @@ -90,23 +82,23 @@ public class AppListViewModel extends BaseViewModel() { @Override public void onSubscribe(@NonNull Disposable d) { - Log.e("updateAppInfo", "onSubscribe: "); + Logger.e("updateAppInfo", "onSubscribe: "); } @Override public void onNext(@NonNull Integer row) { - Log.e("updateAppInfo", "onNext: " + row); + Logger.e("updateAppInfo", "onNext: " + row); mAppUpdateData.setValue(row); } @Override public void onError(@NonNull Throwable e) { - Log.e("updateAppInfo", "onError: " + e.getMessage()); + Logger.e("updateAppInfo", "onError: " + e.getMessage()); } @Override public void onComplete() { - Log.e("updateAppInfo", "onComplete: "); + Logger.e("updateAppInfo", "onComplete: "); } }); } diff --git a/app/src/main/java/com/ttstd/dialer/activity/contact/add/ContactAddViewModel.java b/app/src/main/java/com/ttstd/dialer/activity/contact/add/ContactAddViewModel.java index 23875eb..717c6ea 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/contact/add/ContactAddViewModel.java +++ b/app/src/main/java/com/ttstd/dialer/activity/contact/add/ContactAddViewModel.java @@ -1,7 +1,6 @@ package com.ttstd.dialer.activity.contact.add; import android.content.Context; -import android.util.Log; import androidx.lifecycle.MutableLiveData; @@ -11,6 +10,7 @@ import com.ttstd.dialer.base.mvvm.BaseViewModel; import com.ttstd.dialer.databinding.ActivityContactAddBinding; import com.ttstd.dialer.db.contact.ContactInfo; import com.ttstd.dialer.db.contact.ContactRepository; +import com.ttstd.dialer.utils.Logger; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.annotations.NonNull; @@ -39,7 +39,7 @@ public class ContactAddViewModel extends BaseViewModel emitter) throws Throwable { int count = mRepository.getTotalCount() + 1; ContactInfo contactInfo = new ContactInfo(name, phone, count); - Log.e(TAG, "saveContact: " + contactInfo); + Logger.e(TAG, "saveContact: " + contactInfo); emitter.onNext(mRepository.insert(contactInfo)); emitter.onComplete(); } @@ -49,23 +49,23 @@ public class ContactAddViewModel extends BaseViewModel() { @Override public void onSubscribe(@NonNull Disposable d) { - Log.e("saveContact", "onSubscribe: "); + Logger.e("saveContact", "onSubscribe: "); } @Override public void onNext(@NonNull Long aLong) { - Log.e("saveContact", "onNext: " + aLong); + Logger.e("saveContact", "onNext: " + aLong); mIntegerMutableLiveData.setValue(aLong); } @Override public void onError(@NonNull Throwable e) { - Log.e("saveContact", "onError: " + e.getMessage()); + Logger.e("saveContact", "onError: " + e.getMessage()); } @Override public void onComplete() { - Log.e("saveContact", "onComplete: "); + Logger.e("saveContact", "onComplete: "); } }); } diff --git a/app/src/main/java/com/ttstd/dialer/activity/contact/list/ContactListActivity.java b/app/src/main/java/com/ttstd/dialer/activity/contact/list/ContactListActivity.java index 1fa759b..f5a9541 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/contact/list/ContactListActivity.java +++ b/app/src/main/java/com/ttstd/dialer/activity/contact/list/ContactListActivity.java @@ -2,7 +2,6 @@ package com.ttstd.dialer.activity.contact.list; import android.content.Intent; import android.content.pm.PackageManager; -import android.util.Log; import android.view.View; import androidx.annotation.NonNull; @@ -17,6 +16,7 @@ import com.ttstd.dialer.base.mvvm.BaseMvvmActivity; import com.ttstd.dialer.databinding.ActivityContactListBinding; import com.ttstd.dialer.db.contact.ContactInfo; import com.ttstd.dialer.fragment.dialog.call.CallFragment; +import com.ttstd.dialer.utils.Logger; import com.ttstd.dialer.view.ItemTouchHelperCallback; import java.util.List; @@ -59,7 +59,7 @@ public class ContactListActivity extends BaseMvvmActivity>() { @Override public void onChanged(List contactInfos) { - Log.e(TAG, "mContactListData: " + contactInfos); + Logger.e(TAG, "mContactListData: " + contactInfos); mContactInfos = contactInfos; mContactInfoAdapter.setContactInfos(mContactInfos); } diff --git a/app/src/main/java/com/ttstd/dialer/activity/contact/list/ContactListViewModel.java b/app/src/main/java/com/ttstd/dialer/activity/contact/list/ContactListViewModel.java index ece0a01..67a6201 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/contact/list/ContactListViewModel.java +++ b/app/src/main/java/com/ttstd/dialer/activity/contact/list/ContactListViewModel.java @@ -1,7 +1,6 @@ package com.ttstd.dialer.activity.contact.list; import android.content.Context; -import android.util.Log; import androidx.lifecycle.MutableLiveData; @@ -11,6 +10,7 @@ import com.ttstd.dialer.base.mvvm.BaseViewModel; import com.ttstd.dialer.databinding.ActivityContactListBinding; import com.ttstd.dialer.db.contact.ContactInfo; import com.ttstd.dialer.db.contact.ContactRepository; +import com.ttstd.dialer.utils.Logger; import java.util.List; @@ -49,12 +49,12 @@ public class ContactListViewModel extends BaseViewModel>() { @Override public void onSubscribe(@NonNull Disposable d) { - Log.e("getAllContacts", "onSubscribe: "); + Logger.e("getAllContacts", "onSubscribe: "); } @Override public void onNext(@NonNull List contactInfos) { - Log.e("getAllContacts", "onNext: "); + Logger.e("getAllContacts", "onNext: "); mContactListData.setValue(contactInfos); // List sorted = contacts.stream().sorted(new Comparator() { @@ -68,18 +68,18 @@ public class ContactListViewModel extends BaseViewModel() { @Override public void subscribe(@NonNull ObservableEmitter emitter) throws Throwable { @@ -93,22 +93,22 @@ public class ContactListViewModel extends BaseViewModel() { @Override public void onSubscribe(@NonNull Disposable d) { - Log.e("updateItemPosition", "onComplete: "); + Logger.e("updateItemPosition", "onComplete: "); } @Override public void onNext(@NonNull Integer integer) { - Log.e("updateItemPosition", "onNext: " + integer); + Logger.e("updateItemPosition", "onNext: " + integer); } @Override public void onError(@NonNull Throwable e) { - Log.e("updateItemPosition", "onError: " + e.getMessage()); + Logger.e("updateItemPosition", "onError: " + e.getMessage()); } @Override public void onComplete() { - Log.e("updateItemPosition", "onComplete: "); + Logger.e("updateItemPosition", "onComplete: "); } }); } diff --git a/app/src/main/java/com/ttstd/dialer/activity/main/MainActivity.java b/app/src/main/java/com/ttstd/dialer/activity/main/MainActivity.java index b63a2ef..f7fe0a9 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/main/MainActivity.java +++ b/app/src/main/java/com/ttstd/dialer/activity/main/MainActivity.java @@ -4,7 +4,7 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.util.Log; +import android.view.KeyEvent; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; @@ -22,6 +22,7 @@ import com.ttstd.dialer.fragment.contact.ContactFragment; import com.ttstd.dialer.fragment.home.HomeFragment; import com.ttstd.dialer.fragment.settings.SettingsFragment; import com.ttstd.dialer.service.main.MainService; +import com.ttstd.dialer.utils.Logger; import com.ttstd.dialer.view.ApkPagerAdapter; import com.ttstd.dialer.view.ScaleCircleNavigator; @@ -124,7 +125,7 @@ public class MainActivity extends BaseMvvmActivity appInfos) { - Log.e("getOutsideApp", "onNext: " + appInfos); + Logger.e("getOutsideApp", "onNext: " + appInfos); mDesktopSortAppData.setValue(appInfos); } @Override public void onError(@NonNull Throwable e) { - Log.e("getOutsideApp", "onError: " + e.getMessage()); + Logger.e("getOutsideApp", "onError: " + e.getMessage()); } @Override public void onComplete() { - Log.e("getOutsideApp", "onComplete: "); + Logger.e("getOutsideApp", "onComplete: "); } }); } + } diff --git a/app/src/main/java/com/ttstd/dialer/activity/settings/home/SettingsActivity.java b/app/src/main/java/com/ttstd/dialer/activity/settings/home/SettingsActivity.java index 6f74ad9..87e333a 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/settings/home/SettingsActivity.java +++ b/app/src/main/java/com/ttstd/dialer/activity/settings/home/SettingsActivity.java @@ -42,6 +42,7 @@ public class SettingsActivity extends BaseMvvmActivity { + public void snRegister() { + OkHttpManager.getInstance().getSnRegisterObservable(getLifecycle()) + .subscribe(new Observer() { + @Override + public void onSubscribe(@NonNull Disposable d) { + Logger.e("snRegister", "onSubscribe: "); + } + + @Override + public void onNext(@NonNull BaseResponse baseResponse) { + Logger.e("snRegister", "onNext: " + baseResponse); + } + + @Override + public void onError(@NonNull Throwable e) { + Logger.e("snRegister", "onError: " + e.getMessage()); + } + + @Override + public void onComplete() { + Logger.e("snRegister", "onComplete: "); + } + }); + + + } } diff --git a/app/src/main/java/com/ttstd/dialer/activity/settings/utils/SettingsUtilsActivity.java b/app/src/main/java/com/ttstd/dialer/activity/settings/utils/SettingsUtilsActivity.java index 60bae6c..0e17582 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/settings/utils/SettingsUtilsActivity.java +++ b/app/src/main/java/com/ttstd/dialer/activity/settings/utils/SettingsUtilsActivity.java @@ -2,7 +2,7 @@ package com.ttstd.dialer.activity.settings.utils; import android.content.Intent; import android.graphics.Bitmap; -import android.util.Log; +import android.graphics.BitmapFactory; import android.view.View; import com.tencent.mmkv.MMKV; @@ -13,9 +13,21 @@ import com.ttstd.dialer.base.mvvm.BaseMvvmActivity; import com.ttstd.dialer.config.CommonConfig; import com.ttstd.dialer.databinding.ActivitySettingsUtilsBinding; import com.ttstd.dialer.service.main.MainService; +import com.ttstd.dialer.utils.CameraUtil; +import com.ttstd.dialer.utils.Logger; import com.ttstd.dialer.utils.SystemUtils; import com.ttstd.dialer.view.ToggleButton; +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.internal.observers.BlockingBaseObserver; +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.RequestBody; + public class SettingsUtilsActivity extends BaseMvvmActivity { private static final String TAG = "SettingsActivity"; @@ -97,15 +109,15 @@ public class SettingsUtilsActivity extends BaseMvvmActivity() { + @Override + public void onNext(@NonNull Integer integer) { + if (integer == 0) { + Bitmap bitmap = BitmapFactory.decodeFile(filepath); + mViewDataBinding.ivSnap.setImageBitmap(bitmap); + } + } + + @Override + public void onError(@NonNull Throwable e) { + + } + }); } + + public void getCameraSnap(View view) { + long createTime = System.currentTimeMillis() / 1000; + CameraUtil cameraUtil = new CameraUtil(SettingsUtilsActivity.this, new CameraUtil.CameraCallBack() { + @Override + public void onErr(String msg) { + Logger.e("CameraUtil", "onErr: " + msg); + } + + @Override + public void onTakePhotoOk(String path) { + Logger.e("CameraUtil", "onTakePhotoOk: " + path); + File file = new File(path); + Bitmap bitmap = BitmapFactory.decodeFile(path); + mViewDataBinding.ivCamera.setImageBitmap(bitmap); + + MediaType mediaType = MediaType.Companion.parse("image/png"); + RequestBody fileBody = RequestBody.Companion.create(file, mediaType); + //设置一个file文件 + MultipartBody.Part body = MultipartBody.Part.createFormData("file", file.getName(), fileBody); + Map params = new HashMap<>(); + params.put("sn", SystemUtils.getSerial()); + params.put("createtime", String.valueOf(createTime)); +// Call call = NetInterfaceManager.getInstance().getScreenshotCall().sendScreenshot(params, body); +// call.enqueue(new RetryCallback(call, 10, 30 * 1000) { +// @Override +// public void onRequestResponse(Call call, Response response) { +// Logger.e(TAG, "onRequestResponse: " + response.body().toString()); +// } +// +// @Override +// public void onRequestFail(Call call, Throwable t) { +// Logger.e(TAG, "onRequestFail: "); +// } +// +// @Override +// public void onStartRetry() { +// Logger.e(TAG, "onStartRetry: "); +// } +// }); + + } + }); + cameraUtil.startTakePicture(getExternalCacheDir().getAbsolutePath() + File.separator + createTime + ".jpg"); + } + } } diff --git a/app/src/main/java/com/ttstd/dialer/activity/weather/main/WeatherMainActivity.java b/app/src/main/java/com/ttstd/dialer/activity/weather/main/WeatherMainActivity.java index 5768c53..d9e9497 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/weather/main/WeatherMainActivity.java +++ b/app/src/main/java/com/ttstd/dialer/activity/weather/main/WeatherMainActivity.java @@ -1,11 +1,8 @@ package com.ttstd.dialer.activity.weather.main; -import android.util.Log; - import androidx.lifecycle.Observer; import androidx.recyclerview.widget.LinearLayoutManager; -import com.baidu.location.BDLocation; import com.jeremyliao.liveeventbus.LiveEventBus; import com.qweather.sdk.response.weather.WeatherDaily; import com.qweather.sdk.response.weather.WeatherHourly; @@ -15,9 +12,11 @@ import com.ttstd.dialer.R; import com.ttstd.dialer.adapter.HourlyWeatherAdapter; import com.ttstd.dialer.adapter.WeatherAdapter; import com.ttstd.dialer.base.mvvm.BaseMvvmActivity; +import com.ttstd.dialer.bean.req.SnLocationReq; import com.ttstd.dialer.config.CommonConfig; import com.ttstd.dialer.databinding.ActivityWeatherMainBinding; import com.ttstd.dialer.utils.DateUtil; +import com.ttstd.dialer.utils.Logger; import java.util.List; import java.util.Optional; @@ -71,7 +70,7 @@ public class WeatherMainActivity extends BaseMvvmActivity() { @Override @@ -108,14 +107,14 @@ public class WeatherMainActivity extends BaseMvvmActivity() { + LiveEventBus.get(CommonConfig.LOCATION_CHANGED_KEY, SnLocationReq.class) + .observeSticky(this, new Observer() { @Override - public void onChanged(BDLocation bdLocation) { - mViewDataBinding.tvLocation.setText(bdLocation.getDistrict()); - mViewModel.getWeatherNow(bdLocation.getAdCode()); - mViewModel.getWeather24h(bdLocation.getAdCode()); - mViewModel.getWeather10D(bdLocation.getAdCode()); + public void onChanged(SnLocationReq snLocationReq) { + mViewDataBinding.tvLocation.setText(snLocationReq.getDistrict()); + mViewModel.getWeatherNow(snLocationReq.getAdCode()); + mViewModel.getWeather24h(snLocationReq.getAdCode()); + mViewModel.getWeather10D(snLocationReq.getAdCode()); } }); diff --git a/app/src/main/java/com/ttstd/dialer/activity/weather/main/WeatherMainViewModel.java b/app/src/main/java/com/ttstd/dialer/activity/weather/main/WeatherMainViewModel.java index 2cbc2fc..cf57c40 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/weather/main/WeatherMainViewModel.java +++ b/app/src/main/java/com/ttstd/dialer/activity/weather/main/WeatherMainViewModel.java @@ -1,7 +1,5 @@ package com.ttstd.dialer.activity.weather.main; -import android.util.Log; - import androidx.lifecycle.MutableLiveData; import com.qweather.sdk.Callback; @@ -17,6 +15,7 @@ import com.ttstd.dialer.base.mvvm.BaseViewModel; import com.ttstd.dialer.databinding.ActivityWeatherMainBinding; import com.ttstd.dialer.gson.GsonUtils; import com.ttstd.dialer.manager.WeatherManager; +import com.ttstd.dialer.utils.Logger; import java.util.List; @@ -26,25 +25,25 @@ public class WeatherMainViewModel extends BaseViewModel mWeatherNowData = new MutableLiveData<>(); public void getWeatherNow(String adCode) { - Log.e(TAG, "getWeatherNow: " + adCode); + Logger.e(TAG, "getWeatherNow: " + adCode); WeatherManager.getInstance().getWeatherNow(adCode, new Callback() { @Override public void onSuccess(WeatherNowResponse response) { - Log.e("getWeatherNow", "onSuccess: " + response); + Logger.e("getWeatherNow", "onSuccess: " + response); WeatherNow weatherNow = response.getNow(); - Log.e("getWeatherNow", "onSuccess: " + weatherNow); + Logger.e("getWeatherNow", "onSuccess: " + weatherNow); mWeatherNowData.postValue(weatherNow); } @Override public void onFailure(ErrorResponse errorResponse) { - Log.e("getWeatherNow", "onFailure: " + errorResponse.getError().getStatus()); - Log.e("getWeatherNow", "onFailure: " + errorResponse.getError().getDetail()); + Logger.e("getWeatherNow", "onFailure: " + errorResponse.getError().getStatus()); + Logger.e("getWeatherNow", "onFailure: " + errorResponse.getError().getDetail()); } @Override public void onException(Throwable e) { - Log.e("getWeatherNow", "onException: " + e.getMessage()); + Logger.e("getWeatherNow", "onException: " + e.getMessage()); } }); } @@ -52,25 +51,25 @@ public class WeatherMainViewModel extends BaseViewModel> mWeather24hData = new MutableLiveData<>(); public void getWeather24h(String adCode) { - Log.e(TAG, "getWeather24h: " + adCode); + Logger.e(TAG, "getWeather24h: " + adCode); WeatherManager.getInstance().getWeather24h(adCode, new Callback() { @Override public void onSuccess(WeatherHourlyResponse weatherHourlyResponse) { - Log.e("getWeather24h", "onSuccess: " + weatherHourlyResponse); + Logger.e("getWeather24h", "onSuccess: " + weatherHourlyResponse); List weatherHourlies = weatherHourlyResponse.getHourly(); - Log.e("getWeather24h", "onSuccess: " + GsonUtils.toJSONString(weatherHourlies)); + Logger.e("getWeather24h", "onSuccess: " + GsonUtils.toJSONString(weatherHourlies)); mWeather24hData.postValue(weatherHourlies); } @Override public void onFailure(ErrorResponse errorResponse) { - Log.e("getWeather24h", "onFailure: " + errorResponse.getError().getStatus()); - Log.e("getWeather24h", "onFailure: " + errorResponse.getError().getDetail()); + Logger.e("getWeather24h", "onFailure: " + errorResponse.getError().getStatus()); + Logger.e("getWeather24h", "onFailure: " + errorResponse.getError().getDetail()); } @Override public void onException(Throwable throwable) { - Log.e("getWeather24h", "onException: " + throwable.getMessage()); + Logger.e("getWeather24h", "onException: " + throwable.getMessage()); } }); } @@ -79,24 +78,24 @@ public class WeatherMainViewModel extends BaseViewModel> mWeatherDailyListData = new MutableLiveData<>(); public void getWeather10D(String adCode) { - Log.e(TAG, "getWeather10D: " + adCode); + Logger.e(TAG, "getWeather10D: " + adCode); WeatherManager.getInstance().getWeather10D(adCode, new Callback() { @Override public void onSuccess(WeatherDailyResponse weatherDailyResponse) { - Log.e("getWeather10D", "onSuccess: "); + Logger.e("getWeather10D", "onSuccess: "); List weatherDailyList = weatherDailyResponse.getDaily(); - Log.e("getWeather10D", "onSuccess: " + GsonUtils.toJSONString(weatherDailyList)); + Logger.e("getWeather10D", "onSuccess: " + GsonUtils.toJSONString(weatherDailyList)); mWeatherDailyListData.postValue(weatherDailyList); } @Override public void onFailure(ErrorResponse errorResponse) { - Log.e("getWeather10D", "onFailure: "); + Logger.e("getWeather10D", "onFailure: "); } @Override public void onException(Throwable throwable) { - Log.e("getWeather10D", "onException: " + throwable.getMessage()); + Logger.e("getWeather10D", "onException: " + throwable.getMessage()); } }); } diff --git a/app/src/main/java/com/ttstd/dialer/adapter/AppAdapter.java b/app/src/main/java/com/ttstd/dialer/adapter/AppAdapter.java index 5799114..83646ec 100644 --- a/app/src/main/java/com/ttstd/dialer/adapter/AppAdapter.java +++ b/app/src/main/java/com/ttstd/dialer/adapter/AppAdapter.java @@ -3,7 +3,6 @@ package com.ttstd.dialer.adapter; import android.content.ComponentName; import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -24,6 +23,7 @@ import com.ttstd.dialer.config.CommonConfig; import com.ttstd.dialer.db.app.AppInfo; import com.ttstd.dialer.fragment.dialog.shortcut.ShortcutDialogFagment; import com.ttstd.dialer.utils.ApkUtils; +import com.ttstd.dialer.utils.Logger; import com.ttstd.iconloader.IconCacheManager; import com.ttstd.iconloader.IconLoader; @@ -132,7 +132,7 @@ public class AppAdapter extends RecyclerView.Adapter imple @Override public void onLoaderReset(@NonNull @NotNull Loader loader) { - Log.e(TAG, "onLoaderReset: "); + Logger.e(TAG, "onLoaderReset: "); } public class AppHolder extends RecyclerView.ViewHolder { diff --git a/app/src/main/java/com/ttstd/dialer/adapter/ContactInfoAdapter.java b/app/src/main/java/com/ttstd/dialer/adapter/ContactInfoAdapter.java index 73d5861..33b2de4 100644 --- a/app/src/main/java/com/ttstd/dialer/adapter/ContactInfoAdapter.java +++ b/app/src/main/java/com/ttstd/dialer/adapter/ContactInfoAdapter.java @@ -1,6 +1,5 @@ package com.ttstd.dialer.adapter; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -14,6 +13,7 @@ import androidx.recyclerview.widget.RecyclerView; import com.shehuan.niv.NiceImageView; import com.ttstd.dialer.R; import com.ttstd.dialer.db.contact.ContactInfo; +import com.ttstd.dialer.utils.Logger; import java.util.Collections; import java.util.List; @@ -69,7 +69,7 @@ public class ContactInfoAdapter extends RecyclerView.Adapter loader) { - Log.e(TAG, "onLoaderReset: "); + Logger.e(TAG, "onLoaderReset: "); } public class AppHolder extends RecyclerView.ViewHolder { diff --git a/app/src/main/java/com/ttstd/dialer/adapter/WeatherAdapter.java b/app/src/main/java/com/ttstd/dialer/adapter/WeatherAdapter.java index c8bf9a5..c95224c 100644 --- a/app/src/main/java/com/ttstd/dialer/adapter/WeatherAdapter.java +++ b/app/src/main/java/com/ttstd/dialer/adapter/WeatherAdapter.java @@ -1,7 +1,6 @@ package com.ttstd.dialer.adapter; import android.graphics.drawable.PictureDrawable; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -19,9 +18,9 @@ import com.ttstd.dialer.config.CommonConfig; import com.ttstd.dialer.glide.GlideApp; import com.ttstd.dialer.glide.svg.SvgSoftwareLayerSetter; import com.ttstd.dialer.utils.DateUtil; +import com.ttstd.dialer.utils.Logger; import java.util.List; -import java.util.Locale; import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade; @@ -83,7 +82,7 @@ public class WeatherAdapter extends RecyclerView.Adapter) ((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments()[0]; boolean isAbstract = Modifier.isAbstract(vmClass.getModifiers()); - Log.e(TAG, "isLocalClass:" + vmClass.getSimpleName().equals(ViewModel.class.getSimpleName()) + " isAbstract:" + isAbstract); + Logger.e(TAG, "isLocalClass:" + vmClass.getSimpleName().equals(ViewModel.class.getSimpleName()) + " isAbstract:" + isAbstract); if (!isAbstract) {//不是一个抽象类 mViewModel = new ViewModelProvider(this).get(vmClass); } diff --git a/app/src/main/java/com/ttstd/dialer/base/mvvm/fragment/BaseMvvmDialogFragment.java b/app/src/main/java/com/ttstd/dialer/base/mvvm/fragment/BaseMvvmDialogFragment.java index 7c78404..38e781f 100644 --- a/app/src/main/java/com/ttstd/dialer/base/mvvm/fragment/BaseMvvmDialogFragment.java +++ b/app/src/main/java/com/ttstd/dialer/base/mvvm/fragment/BaseMvvmDialogFragment.java @@ -4,7 +4,6 @@ import android.app.Activity; import android.content.Context; import android.content.res.Configuration; import android.os.Bundle; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -20,7 +19,7 @@ import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; import com.ttstd.dialer.base.BaseDialogFragment; -import com.ttstd.dialer.base.BaseFragment; +import com.ttstd.dialer.utils.Logger; import java.lang.ref.WeakReference; import java.lang.reflect.ParameterizedType; @@ -76,7 +75,7 @@ public abstract class BaseMvvmDialogFragment> isAttached >>", "flag = " + flag); + Logger.e(" >> isAttached >>", "flag = " + flag); return flag; } @@ -234,7 +233,7 @@ public abstract class BaseMvvmDialogFragment> hideLoading :: isShow: %s", isShow); // if (isShow) -// mWaitDialog.dismiss(); +// mWaitDiaLogger.dismiss(); // } catch (Exception e) { // e.printStackTrace(); // } diff --git a/app/src/main/java/com/ttstd/dialer/base/mvvm/fragment/BaseMvvmFragment.java b/app/src/main/java/com/ttstd/dialer/base/mvvm/fragment/BaseMvvmFragment.java index 40f2b1f..29fe070 100644 --- a/app/src/main/java/com/ttstd/dialer/base/mvvm/fragment/BaseMvvmFragment.java +++ b/app/src/main/java/com/ttstd/dialer/base/mvvm/fragment/BaseMvvmFragment.java @@ -4,7 +4,6 @@ import android.app.Activity; import android.content.Context; import android.content.res.Configuration; import android.os.Bundle; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -20,6 +19,7 @@ import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; import com.ttstd.dialer.base.BaseFragment; +import com.ttstd.dialer.utils.Logger; import java.lang.ref.WeakReference; import java.lang.reflect.ParameterizedType; @@ -75,7 +75,7 @@ public abstract class BaseMvvmFragment> isAttached >>", "flag = " + flag); + Logger.e(" >> isAttached >>", "flag = " + flag); return flag; } @@ -233,7 +233,7 @@ public abstract class BaseMvvmFragment> hideLoading :: isShow: %s", isShow); // if (isShow) -// mWaitDialog.dismiss(); +// mWaitDiaLogger.dismiss(); // } catch (Exception e) { // e.printStackTrace(); // } diff --git a/app/src/main/java/com/ttstd/dialer/base/rx/BaseRxService.java b/app/src/main/java/com/ttstd/dialer/base/rx/BaseRxService.java index 0b2975a..a374548 100644 --- a/app/src/main/java/com/ttstd/dialer/base/rx/BaseRxService.java +++ b/app/src/main/java/com/ttstd/dialer/base/rx/BaseRxService.java @@ -1,7 +1,6 @@ package com.ttstd.dialer.base.rx; import android.app.Service; -import android.content.Intent; import androidx.annotation.CheckResult; import androidx.annotation.NonNull; @@ -49,14 +48,9 @@ public abstract class BaseRxService extends Service implements LifecycleProvider lifecycleSubject.onNext(ActivityEvent.CREATE); } - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - return super.onStartCommand(intent, flags, startId); - } - @Override public void onDestroy() { super.onDestroy(); - lifecycleSubject.onNext(ActivityEvent.STOP); + lifecycleSubject.onNext(ActivityEvent.DESTROY); } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/ttstd/dialer/bean/ApkInstalledInfo.java b/app/src/main/java/com/ttstd/dialer/bean/ApkInstalledInfo.java new file mode 100644 index 0000000..74cb1ff --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/bean/ApkInstalledInfo.java @@ -0,0 +1,143 @@ +package com.ttstd.dialer.bean; + +import androidx.annotation.NonNull; + +import com.google.gson.Gson; +import com.google.gson.JsonParser; +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; + +public class ApkInstalledInfo implements Serializable { + private static final long serialVersionUID = 3322347581650924289L; + + @SerializedName("package_name") + private String packageName; + + @SerializedName("app_name") + private String appName; + + @SerializedName("version_name") + private String versionName; + + @SerializedName("version_code") + private long versionCode; + + @SerializedName("install_time") + private long installTime; + + @SerializedName("last_update_time") + private long lastUpdateTime; + + @SerializedName("apk_size") + private long apkSize; + + @SerializedName("data_size") + private long dataSize; + + @SerializedName("cache_size") + private long cacheSize; + + @SerializedName("md5") + private String md5; + + @SerializedName("system_app") + private boolean systemApp; + + public ApkInstalledInfo() { + } + + public String getPackageName() { + return packageName; + } + + public void setPackageName(String packageName) { + this.packageName = packageName; + } + + public String getAppName() { + return appName; + } + + public void setAppName(String appName) { + this.appName = appName; + } + + public String getVersionName() { + return versionName; + } + + public void setVersionName(String versionName) { + this.versionName = versionName; + } + + public long getVersionCode() { + return versionCode; + } + + public void setVersionCode(long versionCode) { + this.versionCode = versionCode; + } + + public long getInstallTime() { + return installTime; + } + + public void setInstallTime(long installTime) { + this.installTime = installTime; + } + + public long getLastUpdateTime() { + return lastUpdateTime; + } + + public void setLastUpdateTime(long lastUpdateTime) { + this.lastUpdateTime = lastUpdateTime; + } + + public long getApkSize() { + return apkSize; + } + + public void setApkSize(long apkSize) { + this.apkSize = apkSize; + } + + public long getDataSize() { + return dataSize; + } + + public void setDataSize(long dataSize) { + this.dataSize = dataSize; + } + + public long getCacheSize() { + return cacheSize; + } + + public void setCacheSize(long cacheSize) { + this.cacheSize = cacheSize; + } + + public String getMd5() { + return md5; + } + + public void setMd5(String md5) { + this.md5 = md5; + } + + public boolean isSystemApp() { + return systemApp; + } + + public void setSystemApp(boolean systemApp) { + this.systemApp = systemApp; + } + + @NonNull + @Override + public String toString() { + return JsonParser.parseString(new Gson().toJson(this)).getAsJsonObject().toString(); + } +} diff --git a/app/src/main/java/com/ttstd/dialer/bean/BaseResponse.java b/app/src/main/java/com/ttstd/dialer/bean/BaseResponse.java new file mode 100644 index 0000000..c97057c --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/bean/BaseResponse.java @@ -0,0 +1,52 @@ +package com.ttstd.dialer.bean; + +import androidx.annotation.NonNull; + +import com.google.gson.Gson; +import com.google.gson.JsonParser; + +/** + * 通用响应实体基类 + * + * @param 具体数据实体 + */ +public class BaseResponse { + private String code; + private String msg; + private T data; + + // 判断请求是否成功(根据后端 code 定义) + public boolean isSuccess() { + return "00000".equals(code); + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } + + public T getData() { + return data; + } + + public void setData(T data) { + this.data = data; + } + + @NonNull + @Override + public String toString() { + return JsonParser.parseString(new Gson().toJson(this)).getAsJsonObject().toString(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ttstd/dialer/bean/DeveloperOptions.java b/app/src/main/java/com/ttstd/dialer/bean/DeveloperOptions.java new file mode 100644 index 0000000..300ed6b --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/bean/DeveloperOptions.java @@ -0,0 +1,17 @@ +package com.ttstd.dialer.bean; + +import java.io.Serializable; + +public class DeveloperOptions implements Serializable { + private static final long serialVersionUID = 6146726170647484306L; + + int developerOptions; + + public int getDeveloperOptions() { + return developerOptions; + } + + public void setDeveloperOptions(int developerOptions) { + this.developerOptions = developerOptions; + } +} diff --git a/app/src/main/java/com/ttstd/dialer/bean/PushMessage.java b/app/src/main/java/com/ttstd/dialer/bean/PushMessage.java new file mode 100644 index 0000000..6a2d4bc --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/bean/PushMessage.java @@ -0,0 +1,115 @@ +package com.ttstd.dialer.bean; + +import androidx.annotation.NonNull; + +import com.google.gson.Gson; +import com.google.gson.JsonParser; + +import java.io.Serializable; + +import cn.jpush.android.api.CustomMessage; + + +/** + * copy from + * { + * {@link cn.jpush.android.api.CustomMessage} + * } + * add Serializable + */ +public class PushMessage implements Serializable { + private static final long serialVersionUID = -3312355696577675582L; + + public String messageId; + public String extra; + public String message; + public String contentType; + public String title; + public String senderId; + public String appId; + public byte platform; + + public PushMessage() { + } + + public PushMessage(CustomMessage customMessage) { + this.messageId = customMessage.messageId; + this.extra = customMessage.extra; + this.message = customMessage.message; + this.contentType = customMessage.contentType; + this.title = customMessage.title; + this.senderId = customMessage.senderId; + this.appId = customMessage.appId; + this.platform = customMessage.platform; + } + + public String getMessageId() { + return messageId; + } + + public void setMessageId(String messageId) { + this.messageId = messageId; + } + + public String getExtra() { + return extra; + } + + public void setExtra(String extra) { + this.extra = extra; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getContentType() { + return contentType; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getSenderId() { + return senderId; + } + + public void setSenderId(String senderId) { + this.senderId = senderId; + } + + public String getAppId() { + return appId; + } + + public void setAppId(String appId) { + this.appId = appId; + } + + public byte getPlatform() { + return platform; + } + + public void setPlatform(byte platform) { + this.platform = platform; + } + + @NonNull + @Override + public String toString() { + return JsonParser.parseString(new Gson().toJson(this)).getAsJsonObject().toString(); + } +} diff --git a/app/src/main/java/com/ttstd/dialer/bean/req/SnHardwareInfoReq.java b/app/src/main/java/com/ttstd/dialer/bean/req/SnHardwareInfoReq.java new file mode 100644 index 0000000..3070395 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/bean/req/SnHardwareInfoReq.java @@ -0,0 +1,141 @@ +package com.ttstd.dialer.bean.req; + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; + +public class SnHardwareInfoReq implements Serializable { + private static final long serialVersionUID = 2728925136862797858L; + + @SerializedName("sn_imei") + private String imei; + + @SerializedName("sn_imsi") + private String imsi; + + @SerializedName("sn_wlan_mac") + private String wlanMac; + + @SerializedName("sn_device_mac") + private String deviceMac; + + @SerializedName("sn_bluetooth_mac") + private String bluetoothMac; + + @SerializedName("sn_model") + private String model; + + @SerializedName("sn_brand") + private String brand; + + @SerializedName("sn_board") + private String board; + + @SerializedName("sn_android_version") + private String androidVersion; + + @SerializedName("sn_android_api") + private int androidApi; + + @SerializedName("sn_build_id") + private String buildId; + + @SerializedName("sn_build_display_id") + private String buildDisplayId; + + public String getImei() { + return imei; + } + + public void setImei(String imei) { + this.imei = imei; + } + + public String getImsi() { + return imsi; + } + + public void setImsi(String imsi) { + this.imsi = imsi; + } + + public String getWlanMac() { + return wlanMac; + } + + public void setWlanMac(String wlanMac) { + this.wlanMac = wlanMac; + } + + public String getDeviceMac() { + return deviceMac; + } + + public void setDeviceMac(String deviceMac) { + this.deviceMac = deviceMac; + } + + public String getBluetoothMac() { + return bluetoothMac; + } + + public void setBluetoothMac(String bluetoothMac) { + this.bluetoothMac = bluetoothMac; + } + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + public String getBrand() { + return brand; + } + + public void setBrand(String brand) { + this.brand = brand; + } + + public String getBoard() { + return board; + } + + public void setBoard(String board) { + this.board = board; + } + + public String getAndroidVersion() { + return androidVersion; + } + + public void setAndroidVersion(String androidVersion) { + this.androidVersion = androidVersion; + } + + public int getAndroidApi() { + return androidApi; + } + + public void setAndroidApi(int androidApi) { + this.androidApi = androidApi; + } + + public String getBuildId() { + return buildId; + } + + public void setBuildId(String buildId) { + this.buildId = buildId; + } + + public String getBuildDisplayId() { + return buildDisplayId; + } + + public void setBuildDisplayId(String buildDisplayId) { + this.buildDisplayId = buildDisplayId; + } +} diff --git a/app/src/main/java/com/ttstd/dialer/bean/req/SnLocationReq.java b/app/src/main/java/com/ttstd/dialer/bean/req/SnLocationReq.java new file mode 100644 index 0000000..0a6ba3a --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/bean/req/SnLocationReq.java @@ -0,0 +1,224 @@ +package com.ttstd.dialer.bean.req; + +import com.baidu.location.BDLocation; +import com.google.gson.annotations.SerializedName; +import com.ttstd.dialer.manager.MapManager; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 设备定位信息请求模型 + */ +public class SnLocationReq implements Serializable { + private static final long serialVersionUID = -1673953474925103299L; + + @SerializedName("country") + private String country; + + @SerializedName("countryCode") + private String countryCode; + + @SerializedName("province") + private String province; + + @SerializedName("city") + private String city; + + @SerializedName("cityCode") + private String cityCode; + + @SerializedName("district") + private String district; + + @SerializedName("street") + private String street; + + @SerializedName("streetNumber") + private String streetNumber; + + @SerializedName("address") + private String address; + + @SerializedName("adCode") + private String adCode; + + @SerializedName("town") + private String town; + + @SerializedName("townCode") + private String townCode; + + @SerializedName("locationDescribe") + private String locationDescribe; + + @SerializedName("longitude") + private String longitude; + + @SerializedName("latitude") + private String latitude; + + @SerializedName("mapError") + private String mapError; + + @SerializedName("lastSuccessfulTime") + private LocalDateTime lastSuccessfulTime; + + public SnLocationReq() { + } + + public SnLocationReq(BDLocation bdLocation) { + setCountry(bdLocation.getCountry()); + setCountryCode(bdLocation.getCountryCode()); + setProvince(bdLocation.getProvince()); + setCity(bdLocation.getCity()); + setCityCode(bdLocation.getCityCode()); + setDistrict(bdLocation.getDistrict()); + setStreet(bdLocation.getStreet()); + setStreetNumber(bdLocation.getStreetNumber()); + setAddress(bdLocation.getAddrStr()); + setAdCode(bdLocation.getAdCode()); + setTown(bdLocation.getTown()); + setTownCode(bdLocation.getTownCode()); + setLocationDescribe(bdLocation.getLocationDescribe()); + setLongitude(MapManager.doubleToStringExact(bdLocation.getLongitude())); + setLatitude(MapManager.doubleToStringExact(bdLocation.getLatitude())); + } + + // Getters and Setters + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + + public String getCountryCode() { + return countryCode; + } + + public void setCountryCode(String countryCode) { + this.countryCode = countryCode; + } + + public String getProvince() { + return province; + } + + public void setProvince(String province) { + this.province = province; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public String getCityCode() { + return cityCode; + } + + public void setCityCode(String cityCode) { + this.cityCode = cityCode; + } + + public String getDistrict() { + return district; + } + + public void setDistrict(String district) { + this.district = district; + } + + public String getStreet() { + return street; + } + + public void setStreet(String street) { + this.street = street; + } + + public String getStreetNumber() { + return streetNumber; + } + + public void setStreetNumber(String streetNumber) { + this.streetNumber = streetNumber; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + + public String getAdCode() { + return adCode; + } + + public void setAdCode(String adCode) { + this.adCode = adCode; + } + + public String getTown() { + return town; + } + + public void setTown(String town) { + this.town = town; + } + + public String getTownCode() { + return townCode; + } + + public void setTownCode(String townCode) { + this.townCode = townCode; + } + + public String getLocationDescribe() { + return locationDescribe; + } + + public void setLocationDescribe(String locationDescribe) { + this.locationDescribe = locationDescribe; + } + + public String getLongitude() { + return longitude; + } + + public void setLongitude(String longitude) { + this.longitude = longitude; + } + + public String getLatitude() { + return latitude; + } + + public void setLatitude(String latitude) { + this.latitude = latitude; + } + + public String getMapError() { + return mapError; + } + + public void setMapError(String mapError) { + this.mapError = mapError; + } + + public LocalDateTime getLastSuccessfulTime() { + return lastSuccessfulTime; + } + + public void setLastSuccessfulTime(LocalDateTime lastSuccessfulTime) { + this.lastSuccessfulTime = lastSuccessfulTime; + } +} diff --git a/app/src/main/java/com/ttstd/dialer/fragment/app/AppFragment.java b/app/src/main/java/com/ttstd/dialer/fragment/app/AppFragment.java index 257fff9..74f9bf7 100644 --- a/app/src/main/java/com/ttstd/dialer/fragment/app/AppFragment.java +++ b/app/src/main/java/com/ttstd/dialer/fragment/app/AppFragment.java @@ -2,7 +2,6 @@ package com.ttstd.dialer.fragment.app; import android.app.Activity; import android.os.Bundle; -import android.util.Log; import androidx.lifecycle.Observer; import androidx.loader.app.LoaderManager; @@ -13,9 +12,9 @@ import com.ttstd.dialer.adapter.AppAdapter; import com.ttstd.dialer.base.mvvm.fragment.BaseMvvmFragment; import com.ttstd.dialer.databinding.FragmentAppBinding; import com.ttstd.dialer.db.app.AppInfo; +import com.ttstd.dialer.utils.Logger; import com.ttstd.dialer.view.EqualHeightDecoration; -import java.io.Serializable; import java.util.List; public class AppFragment extends BaseMvvmFragment { @@ -30,7 +29,7 @@ public class AppFragment extends BaseMvvmFragment apkList) { // AppFragment appFragment = new AppFragment(); -// Log.e(TAG, "newInstance: " + appFragment); +// Logger.e(TAG, "newInstance: " + appFragment); // Bundle args = new Bundle(); // args.putSerializable(ARG_APP_LIST, (Serializable) apkList); // appFragment.setArguments(args); @@ -43,7 +42,7 @@ public class AppFragment extends BaseMvvmFragment appInfos) { mAppInfos = appInfos; - Log.e(TAG, "AppFragment: mAppInfos = " + mAppInfos.hashCode()); + Logger.e(TAG, "AppFragment: mAppInfos = " + mAppInfos.hashCode()); } public interface UpdateCallback { @@ -72,7 +71,7 @@ public class AppFragment extends BaseMvvmFragment() { @Override public void onChanged(Integer integer) { @@ -117,14 +116,14 @@ public class AppFragment extends BaseMvvmFragment() { @Override public void onSubscribe(@NonNull Disposable d) { - Log.e("updateAppInfo", "onSubscribe: "); + Logger.e("updateAppInfo", "onSubscribe: "); } @Override public void onNext(@NonNull Integer row) { - Log.e("updateAppInfo", "onNext: " + row); + Logger.e("updateAppInfo", "onNext: " + row); mAppUpdateData.setValue(row); } @Override public void onError(@NonNull Throwable e) { - Log.e("updateAppInfo", "onError: " + e.getMessage()); + Logger.e("updateAppInfo", "onError: " + e.getMessage()); } @Override public void onComplete() { - Log.e("updateAppInfo", "onComplete: "); + Logger.e("updateAppInfo", "onComplete: "); } }); } diff --git a/app/src/main/java/com/ttstd/dialer/fragment/contact/ContactFragment.java b/app/src/main/java/com/ttstd/dialer/fragment/contact/ContactFragment.java index 06ff019..23dba73 100644 --- a/app/src/main/java/com/ttstd/dialer/fragment/contact/ContactFragment.java +++ b/app/src/main/java/com/ttstd/dialer/fragment/contact/ContactFragment.java @@ -2,7 +2,6 @@ package com.ttstd.dialer.fragment.contact; import android.app.Activity; import android.os.Bundle; -import android.util.Log; import androidx.lifecycle.Observer; import androidx.recyclerview.widget.GridLayoutManager; @@ -13,6 +12,7 @@ import com.ttstd.dialer.base.mvvm.fragment.BaseMvvmFragment; import com.ttstd.dialer.databinding.FragmentContactBinding; import com.ttstd.dialer.db.contact.ContactInfo; import com.ttstd.dialer.fragment.dialog.call.CallFragment; +import com.ttstd.dialer.utils.Logger; import com.ttstd.dialer.view.EqualHeightDecoration; import java.util.List; @@ -63,7 +63,7 @@ public class ContactFragment extends BaseMvvmFragment>() { @Override public void onChanged(List contactInfos) { - Log.e(TAG, "mContactListData: " + contactInfos); + Logger.e(TAG, "mContactListData: " + contactInfos); mContactInfos = contactInfos; mHomeContactAdapter.setContactInfos(mContactInfos); } diff --git a/app/src/main/java/com/ttstd/dialer/fragment/contact/ContactViewModel.java b/app/src/main/java/com/ttstd/dialer/fragment/contact/ContactViewModel.java index 97b6def..59bbe5d 100644 --- a/app/src/main/java/com/ttstd/dialer/fragment/contact/ContactViewModel.java +++ b/app/src/main/java/com/ttstd/dialer/fragment/contact/ContactViewModel.java @@ -1,7 +1,6 @@ package com.ttstd.dialer.fragment.contact; import android.content.Context; -import android.util.Log; import androidx.lifecycle.MutableLiveData; @@ -11,6 +10,7 @@ import com.ttstd.dialer.base.mvvm.BaseViewModel; import com.ttstd.dialer.databinding.FragmentContactBinding; import com.ttstd.dialer.db.contact.ContactInfo; import com.ttstd.dialer.db.contact.ContactRepository; +import com.ttstd.dialer.utils.Logger; import java.util.List; @@ -48,12 +48,12 @@ public class ContactViewModel extends BaseViewModel>() { @Override public void onSubscribe(@NonNull Disposable d) { - Log.e("getAllContacts", "onSubscribe: "); + Logger.e("getAllContacts", "onSubscribe: "); } @Override public void onNext(@NonNull List contactInfos) { - Log.e("getAllContacts", "onNext: "); + Logger.e("getAllContacts", "onNext: "); mContactListData.setValue(contactInfos); // List sorted = contacts.stream().sorted(new Comparator() { @@ -67,12 +67,12 @@ public class ContactViewModel extends BaseViewModel { private static final String TAG = "CallFragment"; @@ -99,7 +99,7 @@ public class CallFragment extends BaseMvvmDialogFragment { private static final String TAG = "ShortcutDialogFagment"; @@ -160,7 +160,7 @@ public class ShortcutDialogFagment extends BaseMvvmDialogFragment { private static final String TAG = "SettingsFragment"; @@ -55,7 +55,7 @@ public class SettingsFragment extends BaseMvvmFragment extends MutableLiveData { public void observe(androidx.lifecycle.LifecycleOwner owner, final Observer observer) { // 警告:多个观察者同时注册时,只有一个会收到通知 if (hasActiveObservers()) { - Log.w(TAG, "多个观察者注册,但仅第一个会收到事件"); + Logger.w(TAG, "多个观察者注册,但仅第一个会收到事件"); } // 包装观察者,仅在事件未处理时触发 diff --git a/app/src/main/java/com/ttstd/dialer/manager/AppManager.java b/app/src/main/java/com/ttstd/dialer/manager/AppManager.java index 43ed6d3..73e8a8c 100644 --- a/app/src/main/java/com/ttstd/dialer/manager/AppManager.java +++ b/app/src/main/java/com/ttstd/dialer/manager/AppManager.java @@ -1,33 +1,46 @@ package com.ttstd.dialer.manager; import android.annotation.SuppressLint; +import android.app.usage.StorageStats; +import android.app.usage.StorageStatsManager; import android.content.ComponentName; import android.content.Context; +import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.os.Build; +import android.os.UserHandle; import android.util.Log; import com.tencent.mmkv.MMKV; +import com.ttstd.dialer.bean.ApkInstalledInfo; import com.ttstd.dialer.config.CommonConfig; import com.ttstd.dialer.db.app.AppInfo; import com.ttstd.dialer.db.app.AppRepository; import com.ttstd.dialer.gson.GsonUtils; import com.ttstd.dialer.utils.ApkUtils; +import com.ttstd.dialer.utils.HashUtils; +import com.ttstd.dialer.utils.Logger; +import java.io.File; import java.text.Collator; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.Set; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.function.Function; import java.util.function.IntConsumer; +import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -44,8 +57,10 @@ public class AppManager { private MMKV mMMKV = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE); @SuppressLint("StaticFieldLeak") - private static AppManager INSTANCE; + private static volatile AppManager INSTANCE; private Context mContext; + private PackageManager mPackageManager; + private AppRepository mAppRepository; private Set mProgressCallbacks = new CopyOnWriteArraySet<>(); @@ -83,13 +98,17 @@ public class AppManager { public static void init(Context context) { if (INSTANCE == null) { - INSTANCE = new AppManager(context); + synchronized (AppManager.class) { + if (INSTANCE == null) { + INSTANCE = new AppManager(context); + } + } } } public static AppManager getInstance() { if (INSTANCE == null) { - throw new IllegalStateException("You must be init AppManager first"); + throw new IllegalStateException("You must first initialize the AppManager"); } return INSTANCE; } @@ -97,6 +116,7 @@ public class AppManager { private AppManager(Context context) { this.mContext = context.getApplicationContext(); this.mAppRepository = new AppRepository(context); + this.mPackageManager = mContext.getPackageManager(); executeAppListProcessing(); } @@ -138,7 +158,7 @@ public class AppManager { }, ASYNC_EXECUTOR) .thenRun(() -> notifyCompleted(true, "应用列表处理完成")) .exceptionally(throwable -> { - Log.e(TAG, "异步处理失败", throwable); + Logger.e(TAG, "异步处理失败", throwable); notifyCompleted(false, "处理失败: " + throwable.getMessage()); return null; }); @@ -150,7 +170,7 @@ public class AppManager { List result = mAppRepository.getAllApp(); return result != null ? result : Collections.emptyList(); } catch (Exception e) { - Log.e(TAG, "获取桌面应用列表失败", e); + Logger.e(TAG, "获取桌面应用列表失败", e); return Collections.emptyList(); } } @@ -159,13 +179,13 @@ public class AppManager { private CompletableFuture processFirstTimeApps() { return CompletableFuture.runAsync(() -> { notifyProgress(STEP_FIRST_INIT, 1, TOTAL_STEPS_FIRST); - Log.i(TAG, "进入首次应用数据初始化流程"); + Logger.i(TAG, "进入首次应用数据初始化流程"); try { // 获取所有可启动应用 List allLauncherApps = ApkUtils.getAllLauncherResolveInfo(mContext); if (allLauncherApps.isEmpty()) { - Log.w(TAG, "未获取到任何可启动应用,无法完成首次初始化"); + Logger.w(TAG, "未获取到任何可启动应用,无法完成首次初始化"); return; } @@ -196,13 +216,13 @@ public class AppManager { try { mAppRepository.insert(app); } catch (Exception e) { - Log.e(TAG, "首次初始化插入应用失败: " + app.getPackageName(), e); + Logger.e(TAG, "首次初始化插入应用失败: " + app.getPackageName(), e); } }); - Log.i(TAG, "首次初始化完成,插入默认应用: " + allFirstApps.size() + " 个"); + Logger.i(TAG, "首次初始化完成,插入默认应用: " + allFirstApps.size() + " 个"); } catch (Exception e) { - Log.e(TAG, "首次应用数据初始化失败", e); + Logger.e(TAG, "首次应用数据初始化失败", e); throw new RuntimeException("首次初始化失败", e); // 抛出异常触发回调 } }, ASYNC_EXECUTOR); @@ -213,7 +233,7 @@ public class AppManager { return CompletableFuture.runAsync(() -> { notifyProgress(STEP_REMOVE_UNINSTALLED, 1, TOTAL_STEPS_NORMAL); if (appInfos.isEmpty()) { - Log.w(TAG, "桌面应用列表为空,跳过删除处理"); + Logger.w(TAG, "桌面应用列表为空,跳过删除处理"); return; } @@ -224,7 +244,7 @@ public class AppManager { try { return mAppRepository.getIdByPackageAndClass(app.getPackageName(), app.getClassName()); } catch (Exception e) { - Log.e(TAG, "获取应用ID失败: " + app.getPackageName(), e); + Logger.e(TAG, "获取应用ID失败: " + app.getPackageName(), e); return -1; } }) @@ -236,10 +256,10 @@ public class AppManager { try { mAppRepository.deleteById(id); } catch (Exception e) { - Log.e(TAG, "删除应用失败, ID: " + id, e); + Logger.e(TAG, "删除应用失败, ID: " + id, e); } }); - Log.i(TAG, "成功删除 " + ids.size() + " 个未安装应用"); + Logger.i(TAG, "成功删除 " + ids.size() + " 个未安装应用"); } }, ASYNC_EXECUTOR); } @@ -250,7 +270,7 @@ public class AppManager { try { List resolveInfos = ApkUtils.getAllLauncherResolveInfo(mContext); if (resolveInfos.isEmpty()) { - Log.w(TAG, "未获取到启动器应用列表,跳过新应用处理"); + Logger.w(TAG, "未获取到启动器应用列表,跳过新应用处理"); return; } @@ -269,15 +289,15 @@ public class AppManager { try { mAppRepository.insert(app); } catch (Exception e) { - Log.e(TAG, "插入新应用失败: " + app.getPackageName(), e); + Logger.e(TAG, "插入新应用失败: " + app.getPackageName(), e); } }); - Log.i(TAG, "成功插入 " + newApps.size() + " 个新应用"); + Logger.i(TAG, "成功插入 " + newApps.size() + " 个新应用"); } else { - Log.i(TAG, "没有需要插入的新应用"); + Logger.i(TAG, "没有需要插入的新应用"); } } catch (Exception e) { - Log.e(TAG, "处理新应用时发生异常", e); + Logger.e(TAG, "处理新应用时发生异常", e); throw new RuntimeException("新应用处理失败", e); } }, ASYNC_EXECUTOR); @@ -289,7 +309,7 @@ public class AppManager { try { List appInfos = getAllDesktopSortApps(); if (appInfos.isEmpty()) { - Log.w(TAG, "无应用数据,跳过位置更新"); + Logger.w(TAG, "无应用数据,跳过位置更新"); return; } @@ -305,12 +325,12 @@ public class AppManager { try { mAppRepository.update(app); } catch (Exception e) { - Log.e(TAG, "更新应用位置失败: " + app.getPackageName(), e); + Logger.e(TAG, "更新应用位置失败: " + app.getPackageName(), e); } }); - Log.i(TAG, "成功更新 " + sortedApps.size() + " 个应用的位置"); + Logger.i(TAG, "成功更新 " + sortedApps.size() + " 个应用的位置"); } catch (Exception e) { - Log.e(TAG, "更新应用位置时发生异常", e); + Logger.e(TAG, "更新应用位置时发生异常", e); throw new RuntimeException("位置更新失败", e); } }, ASYNC_EXECUTOR); @@ -330,7 +350,7 @@ public class AppManager { try { return mAppRepository.checkAppInfoExists(app.getPackageName(), app.getClassName()) > 0; } catch (Exception e) { - Log.e(TAG, "检查应用存在性失败: " + app.getPackageName(), e); + Logger.e(TAG, "检查应用存在性失败: " + app.getPackageName(), e); return false; } } @@ -353,7 +373,7 @@ public class AppManager { try { mAppRepository.insert(app); } catch (Exception e) { - Log.e(TAG, "更新应用插入失败: " + app.getPackageName(), e); + Logger.e(TAG, "更新应用插入失败: " + app.getPackageName(), e); } }); } @@ -373,13 +393,12 @@ public class AppManager { } public String getDefaultAppList() { - PackageManager pm = mContext.getPackageManager(); List resolveInfos = ApkUtils.getAllLauncherResolveInfo(mContext); List defaultApps = resolveInfos.stream() .filter(ri -> DEFAULT_APP_PACKAGES.contains(ri.activityInfo.packageName)) .sorted((o1, o2) -> Collator.getInstance(Locale.CHINESE) - .compare(o1.activityInfo.loadLabel(pm), o2.activityInfo.loadLabel(pm))) + .compare(o1.activityInfo.loadLabel(mPackageManager), o2.activityInfo.loadLabel(mPackageManager))) .map(ri -> new ComponentName(ri.activityInfo.packageName, ri.activityInfo.name)) .map(component -> new AppInfo(mContext, component)) .sorted((a, b) -> Boolean.compare( @@ -392,4 +411,64 @@ public class AppManager { return GsonUtils.toJSONString(defaultApps); } + + private static final Set WHITE_SYSTEM_PACKAGES = new HashSet() {{ +// this.add("com.android.luancher3"); + }}; + + public List getApkInstallInfos() { + StorageStatsManager ssm = mContext.getSystemService(StorageStatsManager.class); + // 获取所有已安装的应用 + List installedApps = mPackageManager.getInstalledPackages(0); + // 遍历并筛选第三方应用 + List apkInstalledInfos = installedApps.stream().filter(new Predicate() { + @Override + public boolean test(PackageInfo packageInfo) { + String packageName = packageInfo.packageName; + return !ApkUtils.isSystemApp(mContext, packageName) || WHITE_SYSTEM_PACKAGES.contains(packageName); + } + }).map(new Function() { + @Override + public ApkInstalledInfo apply(PackageInfo packageInfo) { + ApkInstalledInfo apkInstalledInfo = new ApkInstalledInfo(); + apkInstalledInfo.setPackageName(packageInfo.packageName); + apkInstalledInfo.setAppName(packageInfo.applicationInfo.loadLabel(mPackageManager).toString()); + apkInstalledInfo.setVersionName(packageInfo.versionName); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + apkInstalledInfo.setVersionCode(packageInfo.getLongVersionCode()); + } else { + apkInstalledInfo.setVersionCode(packageInfo.versionCode); + } + apkInstalledInfo.setInstallTime(packageInfo.firstInstallTime); + apkInstalledInfo.setLastUpdateTime(packageInfo.lastUpdateTime); + + try { + StorageStats stats = ssm.queryStatsForPackage( + UUID.fromString(packageInfo.applicationInfo.storageUuid.toString()), + mContext.getPackageName(), + UserHandle.of(UserHandle.myUserId()) + ); + + long appSize = stats.getAppBytes(); // 应用大小 + Log.e(TAG, "apply: appSize = " + appSize); + long dataSize = stats.getDataBytes(); // 用户数据 + long cacheSize = stats.getCacheBytes(); // 缓存 + + apkInstalledInfo.setApkSize(appSize); + apkInstalledInfo.setDataSize(dataSize); + apkInstalledInfo.setCacheSize(cacheSize); + } catch (Exception e) { + Log.e(TAG, "getApkInstallInfos: " + e.getMessage()); + } + + Log.e(TAG, "apply: " + packageInfo.applicationInfo.publicSourceDir); + Log.e(TAG, "apply: publicSourceDir = " + new File(packageInfo.applicationInfo.publicSourceDir).length()); + Log.e(TAG, "apply: " + packageInfo.applicationInfo.dataDir); + apkInstalledInfo.setMd5(HashUtils.getFileMd5(new File(packageInfo.applicationInfo.publicSourceDir))); + apkInstalledInfo.setSystemApp(ApkUtils.isSystemApp(mContext, packageInfo.packageName)); + return apkInstalledInfo; + } + }).collect(Collectors.toList()); + return apkInstalledInfos; + } } \ No newline at end of file diff --git a/app/src/main/java/com/ttstd/dialer/manager/CsvDeserializer.java b/app/src/main/java/com/ttstd/dialer/manager/CsvDeserializer.java index 5867515..0e2883a 100644 --- a/app/src/main/java/com/ttstd/dialer/manager/CsvDeserializer.java +++ b/app/src/main/java/com/ttstd/dialer/manager/CsvDeserializer.java @@ -1,12 +1,12 @@ package com.ttstd.dialer.manager; import android.content.Context; -import android.util.Log; import com.opencsv.bean.CsvToBean; import com.opencsv.bean.CsvToBeanBuilder; import com.opencsv.bean.HeaderColumnNameMappingStrategy; import com.ttstd.dialer.bean.CityInfo; +import com.ttstd.dialer.utils.Logger; import java.io.IOException; import java.io.InputStream; @@ -46,14 +46,14 @@ public class CsvDeserializer { .withIgnoreEmptyLine(true) // 忽略空行 .withThrowExceptions(false) // 不抛出异常,便于调试 .build(); - Log.e(TAG, "deserializeFromAssets: finish " + (System.currentTimeMillis() - time) + "ms"); + Logger.e(TAG, "deserializeFromAssets: finish " + (System.currentTimeMillis() - time) + "ms"); // 转换并返回结果列表 return csvToBean.parse(); } catch (IOException e) { - Log.e(TAG, "Error reading CSV file: " + e.getMessage()); + Logger.e(TAG, "Error reading CSV file: " + e.getMessage()); e.printStackTrace(); } catch (Exception e) { - Log.e(TAG, "Error during CSV deserialization: " + e.getMessage()); + Logger.e(TAG, "Error during CSV deserialization: " + e.getMessage()); e.printStackTrace(); } finally { // 确保关闭流,避免资源泄漏 @@ -65,7 +65,7 @@ public class CsvDeserializer { inputStream.close(); } } catch (IOException e) { - Log.w(TAG, "Failed to close stream: " + e.getMessage()); + Logger.w(TAG, "Failed to close stream: " + e.getMessage()); } } return new ArrayList<>(); @@ -93,7 +93,7 @@ public class CsvDeserializer { return csvToBean.parse(); } catch (Exception e) { - Log.e(TAG, "Error during CSV deserialization: " + e.getMessage()); + Logger.e(TAG, "Error during CSV deserialization: " + e.getMessage()); e.printStackTrace(); } diff --git a/app/src/main/java/com/ttstd/dialer/manager/MapManager.java b/app/src/main/java/com/ttstd/dialer/manager/MapManager.java index 3a107e6..ad0c41a 100644 --- a/app/src/main/java/com/ttstd/dialer/manager/MapManager.java +++ b/app/src/main/java/com/ttstd/dialer/manager/MapManager.java @@ -15,11 +15,13 @@ import com.baidu.location.LocationClient; import com.baidu.location.LocationClientOption; import com.jeremyliao.liveeventbus.LiveEventBus; import com.tencent.mmkv.MMKV; +import com.ttstd.dialer.bean.req.SnLocationReq; import com.ttstd.dialer.config.CommonConfig; import com.ttstd.dialer.gson.GsonUtils; import com.ttstd.dialer.utils.Logger; import com.ttstd.dialer.utils.SystemUtils; +import java.math.BigDecimal; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -237,39 +239,41 @@ public class MapManager { /** * 存储定位结果(解耦存储逻辑) */ - private void saveLocationResult(BDLocation location) { - if (location == null) return; - mMMKV.encode(CommonConfig.CURRENT_LOCATION_MAP_ADDRESS_KEY, location.getAddrStr()); - mMMKV.encode(CommonConfig.CURRENT_LOCATION_MAP_LOCATION_DESCRIBE_KEY, location.getLocationDescribe()); - mMMKV.encode(CommonConfig.CURRENT_LOCATION_PROVINCE_KEY, location.getProvince()); - mMMKV.encode(CommonConfig.CURRENT_LOCATION_CITY_KEY, location.getCity()); - mMMKV.encode(CommonConfig.CURRENT_LOCATION_DISTRICT_KEY, location.getDistrict()); - mMMKV.encode(CommonConfig.CURRENT_LOCATION_TOWN_KEY, location.getTown()); - mMMKV.encode(CommonConfig.CURRENT_LOCATION_STREET_KEY, location.getStreet()); + private void saveLocationResult(BDLocation bdLocation) { + if (bdLocation == null) return; + mMMKV.encode(CommonConfig.CURRENT_LOCATION_MAP_ADDRESS_KEY, bdLocation.getAddrStr()); + mMMKV.encode(CommonConfig.CURRENT_LOCATION_MAP_LOCATION_DESCRIBE_KEY, bdLocation.getLocationDescribe()); + mMMKV.encode(CommonConfig.CURRENT_LOCATION_PROVINCE_KEY, bdLocation.getProvince()); + mMMKV.encode(CommonConfig.CURRENT_LOCATION_CITY_KEY, bdLocation.getCity()); + mMMKV.encode(CommonConfig.CURRENT_LOCATION_DISTRICT_KEY, bdLocation.getDistrict()); + mMMKV.encode(CommonConfig.CURRENT_LOCATION_TOWN_KEY, bdLocation.getTown()); + mMMKV.encode(CommonConfig.CURRENT_LOCATION_STREET_KEY, bdLocation.getStreet()); - mMMKV.encode(CommonConfig.CURRENT_LOCATION_LONGITUDE_KEY, location.getLongitude()); - mMMKV.encode(CommonConfig.CURRENT_LOCATION_LATITUDE_KEY, location.getLatitude()); - mMMKV.encode(CommonConfig.CURRENT_LOCATION_AD_CODE_KEY, location.getAdCode()); + mMMKV.encode(CommonConfig.CURRENT_LOCATION_LONGITUDE_KEY, bdLocation.getLongitude()); + mMMKV.encode(CommonConfig.CURRENT_LOCATION_LATITUDE_KEY, bdLocation.getLatitude()); + mMMKV.encode(CommonConfig.CURRENT_LOCATION_AD_CODE_KEY, bdLocation.getAdCode()); -// mMMKV.encode(CommonConfig.CURRENT_LOCATION_LOCATION_ID_KEY, location.getLocationID()); +// mMMKV.encode(CommonConfig.CURRENT_LOCATION_LOCATION_ID_KEY, bdLocation.getLocationID()); mMMKV.encode("MapError", "-"); Map params = new LinkedHashMap<>(); params.put("sn", SystemUtils.getSerial()); - params.put("address", location.getAddrStr()); - params.put("location_describe", location.getLocationDescribe()); - params.put("longitude", location.getLongitude()); - params.put("latitude", location.getLatitude()); - params.put("ad_code", location.getAdCode()); - params.put("coor_type", location.getCoorType()); + params.put("address", bdLocation.getAddrStr()); + params.put("location_describe", bdLocation.getLocationDescribe()); + params.put("longitude", bdLocation.getLongitude()); + params.put("latitude", bdLocation.getLatitude()); + params.put("ad_code", bdLocation.getAdCode()); + params.put("coor_type", bdLocation.getCoorType()); - Logger.e(TAG, "saveLocationResult", "location = " + GsonUtils.toJSONString(location)); + Logger.e(TAG, "saveLocationResult", "bdLocation = " + GsonUtils.toJSONString(bdLocation)); - WeatherManager.getInstance().setAdCode(location.getAdCode()); + WeatherManager.getInstance().setAdCode(bdLocation.getAdCode()); + + SnLocationReq snLocation = new SnLocationReq(bdLocation); LiveEventBus.get(CommonConfig.LOCATION_CHANGED_KEY) - .post(location); + .post(snLocation); } /** @@ -362,4 +366,10 @@ public class MapManager { } mListeners.clear(); // 清除所有回调,避免内存泄漏 } + + public static String doubleToStringExact(double value) { + // 使用 BigDecimal 避免科学计数法 + return new BigDecimal(Double.toString(value)).toPlainString(); + } + } diff --git a/app/src/main/java/com/ttstd/dialer/manager/WeatherManager.java b/app/src/main/java/com/ttstd/dialer/manager/WeatherManager.java index 43aff5d..bfef50a 100644 --- a/app/src/main/java/com/ttstd/dialer/manager/WeatherManager.java +++ b/app/src/main/java/com/ttstd/dialer/manager/WeatherManager.java @@ -3,7 +3,6 @@ package com.ttstd.dialer.manager; import android.annotation.SuppressLint; import android.content.Context; import android.text.TextUtils; -import android.util.Log; import com.jeremyliao.liveeventbus.LiveEventBus; import com.qweather.sdk.Callback; @@ -59,7 +58,7 @@ public class WeatherManager { .setLogEnable(BuildConfig.DEBUG) // 启用调试日志(生产环境建议设置为 false) .setTokenGenerator(jwt); } catch (Throwable e) { - Log.e(TAG, "QWeatherUtils: " + e.getMessage()); + Logger.e(TAG, "QWeatherUtils: " + e.getMessage()); e.printStackTrace(); } } @@ -72,7 +71,7 @@ public class WeatherManager { public static WeatherManager getInstance() { if (INSTANCE == null) { - throw new IllegalStateException("You must be init WeatherManager first"); + throw new IllegalStateException("You must first initialize the WeatherManager"); } return INSTANCE; } @@ -99,7 +98,7 @@ public class WeatherManager { public void subscribe(@NonNull ObservableEmitter> emitter) throws Throwable { long time = System.currentTimeMillis(); List cityInfos = CsvDeserializer.deserializeFromAssets(mContext, "China-City-List-latest.csv"); - Log.e(TAG, "subscribe: deserializeFromAssets time = " + (System.currentTimeMillis() - time) + "ms"); + Logger.e(TAG, "subscribe: deserializeFromAssets time = " + (System.currentTimeMillis() - time) + "ms"); emitter.onNext(cityInfos); } }) @@ -152,7 +151,7 @@ public class WeatherManager { public void getWeatherNow(String adCode, Callback callback) { String locationID = getLocationID(adCode); - Log.e(TAG, "getWeatherNow: locationID = " + locationID); + Logger.e(TAG, "getWeatherNow: locationID = " + locationID); WeatherParameter parameter = new WeatherParameter(locationID) .lang(Lang.ZH_HANS) .unit(Unit.METRIC); @@ -161,7 +160,7 @@ public class WeatherManager { public void getWeather24h(String adCode, Callback callback) { String locationID = getLocationID(adCode); - Log.e(TAG, "getWeatherNow: locationID = " + locationID); + Logger.e(TAG, "getWeatherNow: locationID = " + locationID); WeatherParameter parameter = new WeatherParameter(locationID) .lang(Lang.ZH_HANS) .unit(Unit.METRIC); @@ -170,7 +169,7 @@ public class WeatherManager { public void getWeather10D(String adCode, Callback callback) { String locationID = getLocationID(adCode); - Log.e(TAG, "getWeather10D: locationID = " + locationID); + Logger.e(TAG, "getWeather10D: locationID = " + locationID); WeatherParameter parameter = new WeatherParameter(locationID) .lang(Lang.ZH_HANS) .unit(Unit.METRIC); diff --git a/app/src/main/java/com/ttstd/dialer/mdm/DeviceControlException.java b/app/src/main/java/com/ttstd/dialer/mdm/DeviceControlException.java new file mode 100644 index 0000000..aa1e72a --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/mdm/DeviceControlException.java @@ -0,0 +1,14 @@ +package com.ttstd.dialer.mdm; + +/** + * 设备控制统一异常:屏蔽不同SDK的异常差异 + */ +public class DeviceControlException extends RuntimeException { + public DeviceControlException(String message) { + super(message); + } + + public DeviceControlException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ttstd/dialer/mdm/DeviceControllerFactory.java b/app/src/main/java/com/ttstd/dialer/mdm/DeviceControllerFactory.java new file mode 100644 index 0000000..c659004 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/mdm/DeviceControllerFactory.java @@ -0,0 +1,59 @@ +package com.ttstd.dialer.mdm; + +import android.content.Context; + +import com.ttstd.dialer.mdm.impl.EmuiMdmDeviceController; +import com.ttstd.dialer.mdm.impl.HonorHemDeviceController; +import com.ttstd.dialer.mdm.impl.SystemSignatureController; + +/** + * 设备控制器工厂:根据配置创建对应厂家的适配器实例 + */ +public class DeviceControllerFactory { + // 厂家类型枚举 + public enum VendorType { + SYSTEM_SIGNATURE,//系统签名 + HONOR_HEM, // 荣耀HEM + HUAWEI_MDM, // 华为MDM + XIAOMI_MDM // 小米MDM(扩展用) + } + + // 单例工厂(可选,也可使用依赖注入) + private static DeviceControllerFactory instance; + private Context appContext; + + private DeviceControllerFactory(Context context) { + this.appContext = context.getApplicationContext(); + } + + public static DeviceControllerFactory getInstance(Context context) { + if (instance == null) { + synchronized (DeviceControllerFactory.class) { + if (instance == null) { + instance = new DeviceControllerFactory(context); + } + } + } + return instance; + } + + // 创建对应厂家的控制器 + public IDeviceController createController(VendorType vendorType) { + switch (vendorType) { + case SYSTEM_SIGNATURE: + return new SystemSignatureController(appContext); + case HONOR_HEM: + return new HonorHemDeviceController(appContext); + case HUAWEI_MDM: + // 扩展:实现华为MDM的适配器 + return new EmuiMdmDeviceController(appContext); +// throw new UnsupportedOperationException("华为MDM适配器未实现"); + case XIAOMI_MDM: + // 扩展:实现小米MDM的适配器 + // return new XiaomiMdmDeviceController(appContext); + throw new UnsupportedOperationException("小米MDM适配器未实现"); + default: + throw new IllegalArgumentException("不支持的厂家类型: " + vendorType); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ttstd/dialer/mdm/DeviceManagerService.java b/app/src/main/java/com/ttstd/dialer/mdm/DeviceManagerService.java new file mode 100644 index 0000000..8f3e21d --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/mdm/DeviceManagerService.java @@ -0,0 +1,85 @@ +package com.ttstd.dialer.mdm; + +import android.content.Context; + +import androidx.annotation.NonNull; + +/** + * 业务层示例:只依赖抽象接口,不耦合具体SDK + */ +public class DeviceManagerService { + private static DeviceManagerService sInstance; + private Context mContext; + private IDeviceController deviceController; + + public static void init(@NonNull Context context) { + if (context == null) { + throw new IllegalArgumentException("Context cannot be null!"); + } + if (sInstance == null) { + synchronized (DeviceManagerService.class) { + if (sInstance == null) { + sInstance = new DeviceManagerService(context.getApplicationContext()); + } + } + } + } + + public static DeviceManagerService getInstance() { + if (sInstance == null) { + throw new IllegalStateException("DeviceManagerService must be initialized first! Call init() in Application."); + } + return sInstance; + } + + // 初始化:通过工厂创建控制器(可配置化,比如从sp/配置文件读取厂家类型) + public DeviceManagerService(Context context) { + this.mContext = context; + // 实际项目中可通过配置动态指定厂家(如从服务器下发、本地配置读取) + DeviceControllerFactory.VendorType vendor = DeviceControllerFactory.VendorType.SYSTEM_SIGNATURE; + this.deviceController = DeviceControllerFactory.getInstance(context) + .createController(vendor); + } + + + public String getSerial() { + return deviceController.getSerial(); + } + + public String getImei() { + return deviceController.getImei(); + } + + public String getImsi() { + return deviceController.getImsi(); + } + + public String getWlanMac() { + return deviceController.getWlanMac(); + } + + public String getDeviceMac() { + return deviceController.getDeviceMac(); + } + + public String getBluetoothMac() { + return deviceController.getBluetoothMac(); + } + + public String getBuildId() { + return deviceController.getBuildId(); + } + + public String getBuildDisplayId() { + return deviceController.getBuildDisplayId(); + } + + public boolean setDevelopmentOption(boolean enabled) { + return deviceController.setDevelopmentOption(enabled); + } + + public boolean setUsbDebugMode(boolean enabled) { + return deviceController.setUsbDebugMode(enabled); + } + +} diff --git a/app/src/main/java/com/ttstd/dialer/mdm/IDeviceController.java b/app/src/main/java/com/ttstd/dialer/mdm/IDeviceController.java new file mode 100644 index 0000000..f172b5f --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/mdm/IDeviceController.java @@ -0,0 +1,41 @@ +package com.ttstd.dialer.mdm; + +/** + * 设备控制抽象接口:定义所有需要的设备控制能力,与具体SDK解耦 + */ +public interface IDeviceController { + //获取设备SN + String getSerial(); + + //IMEI + String getImei(); + + //IMSI + String getImsi(); + + //WLAN MAC + String getWlanMac(); + + //设备WLAN MAC + String getDeviceMac(); + + //蓝牙地址 + String getBluetoothMac(); + + String getBuildId(); + + String getBuildDisplayId(); + + boolean setDevelopmentOption(boolean enabled); + + boolean setUsbDebugMode(boolean enabled); + + //设置默认桌面 + boolean setDefaultLauncher(String packageNmae, String className); + + //安装应用 + void installPackage(String path); + + //卸载应用 + void uninstallPackage(String packageNmae); +} diff --git a/app/src/main/java/com/ttstd/dialer/mdm/SystemDeviceController.java b/app/src/main/java/com/ttstd/dialer/mdm/SystemDeviceController.java deleted file mode 100644 index 6be44ec..0000000 --- a/app/src/main/java/com/ttstd/dialer/mdm/SystemDeviceController.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.ttstd.dialer.mdm; - -import android.content.Context; - -import java.util.List; - -public class SystemDeviceController implements IDeviceController { - private Context mContext; - - public SystemDeviceController(Context context) { - mContext = context; - } - - @Override - public void addDisallowedRunningApp(String pkg) { - - } - - @Override - public void removeDisallowedRunningApp(String pkg) { - - } - - @Override - public boolean isDisallowedRunningApp(String pkg) { - return false; - } - - @Override - public void uninstallPackage(String pkg) { - - } - - @Override - public void shutdownDevice() { - - } - - @Override - public void rebootDevice() { - - } - - @Override - public void setDefaultLauncher(String pkg, String className) { - - } - - @Override - public boolean disableBluetoothTransfer(boolean disable) { - return false; - } - - @Override - public void addInstallPackageTrustList(List packages) { - - } - - @Override - public List getInstallPackageTrustList() { - return null; - } -} diff --git a/app/src/main/java/com/ttstd/dialer/mdm/impl/EmuiMdmDeviceController.java b/app/src/main/java/com/ttstd/dialer/mdm/impl/EmuiMdmDeviceController.java new file mode 100644 index 0000000..c1abb61 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/mdm/impl/EmuiMdmDeviceController.java @@ -0,0 +1,83 @@ +package com.ttstd.dialer.mdm.impl; + +import android.content.ComponentName; +import android.content.Context; + +import com.ttstd.dialer.mdm.IDeviceController; + +/** + * 华为MDM SDK适配器:适配抽象接口到华为MDM SDK的具体实现 + */ +public class EmuiMdmDeviceController implements IDeviceController { + private ComponentName adminName; + + + public EmuiMdmDeviceController(Context context) { + + } + + @Override + public String getSerial() { + return null; + } + + @Override + public String getImei() { + return null; + } + + @Override + public String getImsi() { + return null; + } + + @Override + public String getWlanMac() { + return null; + } + + @Override + public String getDeviceMac() { + return null; + } + + @Override + public String getBluetoothMac() { + return null; + } + + @Override + public String getBuildId() { + return null; + } + + @Override + public String getBuildDisplayId() { + return null; + } + + @Override + public boolean setDevelopmentOption(boolean enabled) { + return false; + } + + @Override + public boolean setUsbDebugMode(boolean enabled) { + return false; + } + + @Override + public boolean setDefaultLauncher(String packageNmae, String className) { + return false; + } + + @Override + public void installPackage(String path) { + + } + + @Override + public void uninstallPackage(String packageNmae) { + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ttstd/dialer/mdm/impl/HonorHemDeviceController.java b/app/src/main/java/com/ttstd/dialer/mdm/impl/HonorHemDeviceController.java new file mode 100644 index 0000000..7f0b039 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/mdm/impl/HonorHemDeviceController.java @@ -0,0 +1,86 @@ +package com.ttstd.dialer.mdm.impl; + +import android.content.ComponentName; +import android.content.Context; + +import com.ttstd.dialer.mdm.IDeviceController; + +/** + * 荣耀HEM SDK适配器:适配抽象接口到荣耀HEM SDK的具体实现 + */ +public class HonorHemDeviceController implements IDeviceController { + private ComponentName adminName; // 荣耀HEM需要的管理员组件 + + // 构造器:初始化荣耀HEM SDK的核心实例 + public HonorHemDeviceController(Context context) { + + } + + // 实现抽象接口:适配荣耀HEM的具体API + @Override + public String getSerial() { + return null; + } + + @Override + public String getImei() { + return null; + } + + @Override + public String getImsi() { + return null; + } + + @Override + public String getWlanMac() { + return null; + } + + @Override + public String getDeviceMac() { + return null; + } + + @Override + public String getBluetoothMac() { + return null; + } + + @Override + public String getBuildId() { + return null; + } + + @Override + public String getBuildDisplayId() { + return null; + } + + @Override + public boolean setDevelopmentOption(boolean enabled) { + return false; + } + + @Override + public boolean setUsbDebugMode(boolean enabled) { + return false; + } + + @Override + public boolean setDefaultLauncher(String packageNmae, String className) { + return false; + } + + @Override + public void installPackage(String path) { + + } + + @Override + public void uninstallPackage(String packageNmae) { + + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/ttstd/dialer/mdm/impl/SystemSignatureController.java b/app/src/main/java/com/ttstd/dialer/mdm/impl/SystemSignatureController.java new file mode 100644 index 0000000..4ed1915 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/mdm/impl/SystemSignatureController.java @@ -0,0 +1,84 @@ +package com.ttstd.dialer.mdm.impl; + +import android.content.Context; +import android.provider.Settings; + +import com.ttstd.dialer.mdm.IDeviceController; +import com.ttstd.dialer.utils.SystemUtils; + +/** + * 具有系统签名时使用这个 + */ +public class SystemSignatureController implements IDeviceController { + private Context mContext; + + public SystemSignatureController(Context context) { + mContext = context; + } + + + @Override + public String getSerial() { + return SystemUtils.getSerial(); + } + + @Override + public String getImei() { + return SystemUtils.getDefaultImei(mContext); + } + + @Override + public String getImsi() { + return SystemUtils.getDefaultImSi(mContext); + } + + @Override + public String getWlanMac() { + return SystemUtils.getWlanMacAddress(mContext); + } + + @Override + public String getDeviceMac() { + return SystemUtils.getFactoryMacAddresses(mContext); + } + + @Override + public String getBluetoothMac() { + return SystemUtils.getBluetoothMacAddress(mContext); + } + + @Override + public String getBuildId() { + return SystemUtils.getBuildId(); + } + + @Override + public String getBuildDisplayId() { + return SystemUtils.getBuildDisplayId(); + } + + @Override + public boolean setDevelopmentOption(boolean enabled) { + return Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, enabled ? 1 : 0); + } + + @Override + public boolean setUsbDebugMode(boolean enabled) { + return Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.ADB_ENABLED, enabled ? 1 : 0); + } + + @Override + public boolean setDefaultLauncher(String packageNmae, String className) { + return false; + } + + @Override + public void installPackage(String path) { + + } + + @Override + public void uninstallPackage(String packageNmae) { + + } +} diff --git a/app/src/main/java/com/ttstd/dialer/network/BaseObserver.java b/app/src/main/java/com/ttstd/dialer/network/BaseObserver.java new file mode 100644 index 0000000..9f9d181 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/network/BaseObserver.java @@ -0,0 +1,36 @@ +package com.ttstd.dialer.network; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; + +public abstract class BaseObserver implements Observer { + + @Override + public void onSubscribe(@NonNull Disposable d) { + // 订阅开始(可添加加载框) + } + + @Override + public void onNext(@NonNull T t) { + // 请求成功 + onSuccess(t); + } + + @Override + public void onError(@NonNull Throwable e) { + // 请求失败 + onFailure(e); + } + + @Override + public void onComplete() { + // 请求完成 + } + + // 成功回调(子类实现) + public abstract void onSuccess(T t); + + // 失败回调(子类实现) + public abstract void onFailure(Throwable e); +} \ No newline at end of file diff --git a/app/src/main/java/com/ttstd/dialer/network/OkHttpManager.java b/app/src/main/java/com/ttstd/dialer/network/OkHttpManager.java new file mode 100644 index 0000000..d319f1d --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/network/OkHttpManager.java @@ -0,0 +1,340 @@ +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.network.api.SnApi; +import com.ttstd.dialer.network.interceptor.AuthInterceptor; +import com.ttstd.dialer.utils.FileUtils; +import com.ttstd.dialer.utils.Logger; +import com.ttstd.dialer.utils.SystemUtils; + +import org.jetbrains.annotations.NotNull; + +import java.io.File; +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; +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.OkHttpClient; +import okhttp3.RequestBody; +import okhttp3.logging.HttpLoggingInterceptor; +import retrofit2.Retrofit; +import retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory; +import retrofit2.converter.gson.GsonConverterFactory; + +/** + * OkHttp + Retrofit 管理类 + * 单例模式 + 避免内存泄漏 + 增强异常处理 + 提升扩展性 + */ +public class OkHttpManager { + private static final String TAG = "OkHttpManager"; + + // 超时时间(抽成常量,便于统一维护) + private static final int DEFAULT_TIMEOUT_SECONDS = 5; + // 缓存大小 64MB(使用更语义化的命名) + private static final long CACHE_SIZE_BYTES = 1024 * 1024 * 64L; + // 缓存目录名称(避免硬编码) + private static final String OKHTTP_CACHE_DIR = "OkHttpCache"; + + // 静态单例(使用volatile保证可见性,防止指令重排) + private static volatile OkHttpManager sInstance; + + // 使用Application Context避免内存泄漏 + private final Context mAppContext; + private Retrofit mRetrofit; + private OkHttpClient mOkHttpClient; + + /** + * 初始化(建议在Application中调用) + * + * @param context 上下文(内部会转为Application Context) + */ + public static void init(@NonNull Context context) { + if (context == null) { + throw new IllegalArgumentException("Context cannot be null!"); + } + if (sInstance == null) { + synchronized (OkHttpManager.class) { + if (sInstance == null) { + sInstance = new OkHttpManager(context.getApplicationContext()); + } + } + } + } + + /** + * 获取单例实例 + * + * @return OkHttpManager实例 + * @throws IllegalStateException 未初始化时抛出 + */ + public static OkHttpManager getInstance() { + if (sInstance == null) { + throw new IllegalStateException("OkHttpManager must be initialized first! Call init() in Application."); + } + return sInstance; + } + + /** + * 私有构造方法(禁止外部实例化) + * + * @param appContext Application Context + */ + private OkHttpManager(@NonNull Context appContext) { + this.mAppContext = appContext; + initOkHttpClient(); + initRetrofit(); + } + + /** + * 初始化OkHttpClient(拆分方法,单一职责) + */ + private void initOkHttpClient() { + if (mOkHttpClient != null) { + return; + } + + // 1. 构建调度器(控制并发) + Dispatcher dispatcher = new Dispatcher(); + dispatcher.setMaxRequests(1); // 全局最大并发 + dispatcher.setMaxRequestsPerHost(1); // 单Host最大并发 + + // 2. 构建缓存(增强异常处理) + Cache cache = createCache(); + + // 3. 构建OkHttpClient + OkHttpClient.Builder builder = new OkHttpClient.Builder() + .connectTimeout(DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .writeTimeout(DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .readTimeout(DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .dispatcher(dispatcher) + .connectionPool(new ConnectionPool(20, 1, TimeUnit.SECONDS)) + .addInterceptor(new AuthInterceptor()) // 添加认证拦截器 + .addInterceptor(new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() { + @Override + public void log(@NotNull String s) { + Logger.e("HttpLoggingInterceptor", "log: " + s); + } + }).setLevel(HttpLoggingInterceptor.Level.BASIC)) + .retryOnConnectionFailure(true);// 连接失败重试 + + + // 缓存非空时才设置(避免空指针) + if (cache != null) { + builder.cache(cache); + } + + mOkHttpClient = builder.build(); + } + + /** + * 创建OkHttp缓存(单独抽离,便于维护) + * + * @return Cache实例(创建失败返回null) + */ + private Cache createCache() { + try { + String cacheDirPath = FileUtils.getCacheDir(mAppContext); + if (cacheDirPath == null || cacheDirPath.isEmpty()) { + Log.w(TAG, "Cache directory path is empty, skip cache creation"); + return null; + } + + File cacheDir = new File(cacheDirPath, OKHTTP_CACHE_DIR); + // 确保缓存目录存在(否则OkHttp会抛异常) + if (!cacheDir.exists() && !cacheDir.mkdirs()) { + Log.e(TAG, "Failed to create cache directory: " + cacheDir.getAbsolutePath()); + return null; + } + + // 校验缓存目录可写 + if (!cacheDir.canWrite()) { + Log.e(TAG, "Cache directory is not writable: " + cacheDir.getAbsolutePath()); + return null; + } + + 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); + } catch (NullPointerException e) { + Log.e(TAG, "Cache directory path is null", e); + } + return null; + } + + /** + * 初始化Retrofit(拆分方法,单一职责) + */ + private void initRetrofit() { + if (mRetrofit != null || mOkHttpClient == null) { + return; + } + + mRetrofit = new Retrofit.Builder() + .client(mOkHttpClient) + .baseUrl(UrlConstants.ROOT_URL) // 抽离URL常量,便于环境切换(开发/测试/生产) + .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(RxJava3CallAdapterFactory.create()) + .build(); + } + + /** + * 获取Retrofit实例(对外提供API创建入口) + * + * @return Retrofit实例 + */ + public Retrofit getRetrofit() { + if (mRetrofit == null) { + // 双重检查,防止并发问题 + synchronized (this) { + if (mRetrofit == null) { + initRetrofit(); + } + } + } + return mRetrofit; + } + + /** + * 获取OkHttpClient实例(对外暴露,便于扩展) + * + * @return OkHttpClient实例 + */ + public OkHttpClient getOkHttpClient() { + return mOkHttpClient; + } + + /** + * 销毁单例(可选,比如App退出时) + * 避免内存泄漏(虽然用了AppContext,但主动销毁更规范) + */ + public static void destroy() { + if (sInstance != null) { + // 关闭OkHttp连接池,释放资源 + sInstance.mOkHttpClient.connectionPool().evictAll(); + // 关闭Dispatcher,取消所有请求 + sInstance.mOkHttpClient.dispatcher().cancelAll(); + sInstance.mRetrofit = null; + sInstance.mOkHttpClient = null; + sInstance = null; + } + } + + public static MultipartBody.Part getBodyPart(String type, String name, File file) { + MediaType mediaType = MediaType.Companion.parse(type); + RequestBody fileBody = RequestBody.Companion.create(file, mediaType); + MultipartBody.Part part = MultipartBody.Part.createFormData(name, file.getName(), fileBody); + return part; + } + + public static RequestBody convertToRequestBodyjson(Object param) { + Gson gson = new Gson(); + String json = gson.toJson(param); + Log.e(TAG, "convertToRequestBodyjson: " + json); + MediaType mediaType = MediaType.Companion.parse("application/json; charset=utf-8"); + RequestBody requestBody = RequestBody.Companion.create(json, mediaType); + return requestBody; + } + + public static RequestBody convertToRequestBody(Object param) { + MediaType mediaType = MediaType.Companion.parse("text/plain"); + RequestBody requestBody = RequestBody.Companion.create(String.valueOf(param), mediaType); + return requestBody; + } + + public static RequestBody convertToRequestBody(String param) { + RequestBody requestBody = RequestBody.create(MediaType.parse("text/plain"), param); + return requestBody; + } + + public static RequestBody convertToRequestBody(int param) { + RequestBody requestBody = RequestBody.create(MediaType.parse("text/plain"), String.valueOf(param)); + return requestBody; + } + + public static RequestBody convertToRequestBody(long param) { + RequestBody requestBody = RequestBody.create(MediaType.parse("text/plain"), String.valueOf(param)); + return requestBody; + } + + + /** + * 统一线程调度 + 生命周期管理 + * + * @param observable 接口Observable + * @param provider 生命周期管理者(Activity/Fragment) + * @return 处理后的Observable + */ + public Observable schedule(Observable observable, BehaviorSubject provider) { + return observable + // 子线程执行请求 + .subscribeOn(Schedulers.io()) + // 主线程回调 + .observeOn(AndroidSchedulers.mainThread()) + // 生命周期管理:页面销毁自动取消请求,防止内存泄漏 + .compose(RxLifecycle.bindUntilEvent(provider, ActivityEvent.DESTROY)); + } + + public Observable schedule(Observable observable) { + return observable + // 子线程执行请求 + .subscribeOn(Schedulers.io()) + // 主线程回调 + .observeOn(AndroidSchedulers.mainThread()); + } + + public Observable getSnRegisterObservable(BehaviorSubject provider) { + return schedule(mRetrofit.create(SnApi.class).register(SystemUtils.getSerial(), Build.MODEL), provider); + } + + public Observable> getUpdateHardwareInfoObservable(SnHardwareInfoReq snHardwareInfoReq) { + RequestBody body = convertToRequestBodyjson(snHardwareInfoReq); + return schedule(mRetrofit.create(SnApi.class).updateHardwareInfo(body)); + } + + public Observable getUploadScreenshotObservable(MultipartBody.Part part) { + return schedule(mRetrofit.create(SnApi.class).uploadScreenshot(part)); + } + + public Observable getUploadLocationObservable(SnLocationReq locationReq, BehaviorSubject provider) { + return schedule(mRetrofit.create(SnApi.class).uploadLocation(convertToRequestBodyjson(locationReq)), provider); + } + + public Observable getUploadLocationObservable(SnLocationReq locationReq) { + return schedule(mRetrofit.create(SnApi.class).uploadLocation(convertToRequestBodyjson(locationReq))); + } + + public Observable> getDeveloperOptionsObservable(BehaviorSubject provider) { + return schedule(mRetrofit.create(SnApi.class).getDeveloperOptions(), provider); + } + + public Observable> getDeveloperOptionsObservable() { + return schedule(mRetrofit.create(SnApi.class).getDeveloperOptions()); + } + + public Observable getUploadInstallApksObservable(RequestBody body) { + return schedule(mRetrofit.create(SnApi.class).uploadInstallApks(body)); + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/ttstd/dialer/network/RetryCallback.java b/app/src/main/java/com/ttstd/dialer/network/RetryCallback.java new file mode 100644 index 0000000..b3052f7 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/network/RetryCallback.java @@ -0,0 +1,92 @@ +package com.ttstd.dialer.network; + +import android.util.Log; + +import java.util.Timer; +import java.util.TimerTask; + +import io.reactivex.rxjava3.annotations.NonNull; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public abstract class RetryCallback implements Callback { + + private static final String TAG = "RetryCallback"; + + private int mRetryCount; + private long mRetryInterval; + + private int mCurrentRetryCount; + + private boolean isExecuting; + + private Call mCall; + + private Timer timer = new Timer(); + + public RetryCallback(Call call, int retryCount, long retryInterval) { + isExecuting = true; + mCall = call; + mRetryCount = retryCount; + mRetryInterval = retryInterval; + } + + @Override + public final void onResponse(@NonNull Call call, @NonNull Response response) { + Log.e(TAG, "onResponse"); + isExecuting = false; + if (!response.isSuccessful() && mCurrentRetryCount < mRetryCount) { + mCurrentRetryCount++; + retryRequest(call); + } else { + onRequestResponse(call, response); + } + } + + @Override + public final void onFailure(@NonNull Call call, @NonNull Throwable t) { + Log.e(TAG, "onFailure"); + isExecuting = false; + if (mCurrentRetryCount < mRetryCount) { + mCurrentRetryCount++; + Log.e(TAG, "onFailure: " + "RetryCount: " + mCurrentRetryCount); + Log.e(TAG, "onFailure: " + "RetryResponse: " + call.request()); + retryRequest(call); + } else { + onRequestFail(call, t); + } + } + + private void retryRequest(final Call call) { + Log.e(TAG, "retryRequest"); + onStartRetry(); + TimerTask timerTask = new TimerTask() { + @Override + public void run() { + synchronized (RetryCallback.this) { + mCall = call.clone(); + mCall.enqueue(RetryCallback.this); + isExecuting = true; + } + } + }; + timer.schedule(timerTask, mRetryInterval); + } + + public void cancelCall() { + synchronized (this) { + if (!isExecuting) { + timer.cancel(); + } else { + mCall.cancel(); + } + } + } + + public abstract void onRequestResponse(Call call, Response response); + + public abstract void onRequestFail(Call call, Throwable t); + + public abstract void onStartRetry(); +} \ No newline at end of file diff --git a/app/src/main/java/com/ttstd/dialer/network/SecurityUtils.java b/app/src/main/java/com/ttstd/dialer/network/SecurityUtils.java new file mode 100644 index 0000000..4a7aa9f --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/network/SecurityUtils.java @@ -0,0 +1,28 @@ +package com.ttstd.dialer.network; + +import android.os.Build; + +import com.ttstd.dialer.utils.HashUtils; + +import java.util.SortedMap; +import java.util.function.BiConsumer; + +public class SecurityUtils { + public static String generateSign(SortedMap params) { + StringBuilder sb = new StringBuilder(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + params.forEach(new BiConsumer() { + @Override + public void accept(String key, String value) { + sb.append(key).append("=").append(value).append("&"); + } + }); + } else { + for (String key : params.keySet()) { + sb.append(key).append("=").append(params.get(key)).append("&"); + } + } + sb.setLength(sb.length() - 1); + return HashUtils.getSHA256String(sb.toString()); + } +} diff --git a/app/src/main/java/com/ttstd/dialer/network/UrlAddress.java b/app/src/main/java/com/ttstd/dialer/network/UrlAddress.java deleted file mode 100644 index 9e6bea6..0000000 --- a/app/src/main/java/com/ttstd/dialer/network/UrlAddress.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.ttstd.dialer.network; - -public class UrlAddress { - -} diff --git a/app/src/main/java/com/ttstd/dialer/network/UrlConstants.java b/app/src/main/java/com/ttstd/dialer/network/UrlConstants.java new file mode 100644 index 0000000..a00a4b7 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/network/UrlConstants.java @@ -0,0 +1,19 @@ +package com.ttstd.dialer.network; + +public class UrlConstants { + public static final String ROOT_URL = "http://192.168.100.111:8000/api/v1/sn/"; + + //设备注册 + public static final String SN_REGISTER = "register"; + //上传硬件信息 + public static final String UPDATE_HARDWARE_INFO = "update_hardware_info"; + //上传屏幕截图 + public static final String UPLOAD_SCREENSHOT = "upload_screenshot"; + //上传定位地址 + public static final String UPLOAD_LOCATION = "upload_location"; + //获取开发者选项 + public static final String DEVELOPER_OPTIONS = "get_developer_options"; + /*上传设备已安装应用列表*/ + public static final String UPLOAD_INSTALL_APKS = "upload_install_apks"; + +} diff --git a/app/src/main/java/com/ttstd/dialer/network/api/SnApi.java b/app/src/main/java/com/ttstd/dialer/network/api/SnApi.java new file mode 100644 index 0000000..9daf590 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/network/api/SnApi.java @@ -0,0 +1,61 @@ +package com.ttstd.dialer.network.api; + +import com.ttstd.dialer.bean.BaseResponse; +import com.ttstd.dialer.bean.DeveloperOptions; +import com.ttstd.dialer.network.UrlConstants; + +import io.reactivex.rxjava3.core.Observable; +import okhttp3.MultipartBody; +import okhttp3.RequestBody; +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.Field; +import retrofit2.http.FormUrlEncoded; +import retrofit2.http.GET; +import retrofit2.http.Multipart; +import retrofit2.http.POST; +import retrofit2.http.Part; + +public interface SnApi { + @FormUrlEncoded + @POST(UrlConstants.SN_REGISTER) + Observable register( + @Field("sn") String sn, + @Field("model") String model + ); + + + @POST(UrlConstants.UPDATE_HARDWARE_INFO) + Observable> updateHardwareInfo( + @Body RequestBody body + ); + + @Multipart + @POST(UrlConstants.UPLOAD_SCREENSHOT) + Observable uploadScreenshot( + @Part MultipartBody.Part body + ); + + @Multipart + @POST(UrlConstants.UPLOAD_SCREENSHOT) + Call uploadScreenshotCall( + @Part MultipartBody.Part body + ); + + @POST(UrlConstants.UPLOAD_LOCATION) + Observable uploadLocation( + @Body RequestBody body + ); + + + @GET(UrlConstants.DEVELOPER_OPTIONS) + Observable> getDeveloperOptions( + + ); + + @POST(UrlConstants.UPLOAD_INSTALL_APKS) + Observable uploadInstallApks( + @Body RequestBody body + ); + +} diff --git a/app/src/main/java/com/ttstd/dialer/network/interceptor/AuthInterceptor.java b/app/src/main/java/com/ttstd/dialer/network/interceptor/AuthInterceptor.java new file mode 100644 index 0000000..0294dca --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/network/interceptor/AuthInterceptor.java @@ -0,0 +1,65 @@ +package com.ttstd.dialer.network.interceptor; + +import androidx.annotation.NonNull; + +import com.ttstd.dialer.mdm.DeviceManagerService; +import com.ttstd.dialer.network.SecurityUtils; +import com.ttstd.dialer.utils.NativeUtils; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; + +import okhttp3.Headers; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +/** + * 认证拦截器 + * 功能:为指定请求自动添加 Authorization Header,支持白名单过滤。 + */ +public class AuthInterceptor implements Interceptor { + + // 定义白名单路径(这些路径的请求将不添加Header) + private static final Set WHITE_LIST_PATHS = new HashSet<>(Arrays.asList( + "/api/v1/example" + // 可以继续添加其他白名单路径 + )); + + @NonNull + @Override + public Response intercept(@NonNull Chain chain) throws IOException { + Request originalRequest = chain.request(); + + // 1. 白名单检查:如果请求的URL路径在白名单中,则直接放行,不添加Header + String path = originalRequest.url().encodedPath(); + if (WHITE_LIST_PATHS.contains(path)) { + return chain.proceed(originalRequest); + } + + String sn = DeviceManagerService.getInstance().getSerial(); + long timestamp = System.currentTimeMillis(); + String nonce = NativeUtils.getNonce(); + + SortedMap headersMap = new TreeMap<>(); + headersMap.put("X-Device-SN", sn); + headersMap.put("X-Nonce", nonce); + headersMap.put("X-Timestamp", String.valueOf(timestamp)); + String sign = SecurityUtils.generateSign(headersMap); + headersMap.put("X-Sign", sign); + + // 2. 构建请求头(所有参数放header,防止URL被抓包泄露) + Headers headers = Headers.of(headersMap); + + Request newRequest = originalRequest.newBuilder() + .headers(headers) + .build(); + + // 3. 继续执行请求链 + return chain.proceed(newRequest); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ttstd/dialer/push/PushExecutor.java b/app/src/main/java/com/ttstd/dialer/push/PushExecutor.java new file mode 100644 index 0000000..0b2490f --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/push/PushExecutor.java @@ -0,0 +1,281 @@ +package com.ttstd.dialer.push; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Build; +import android.util.Log; + +import com.tencent.mmkv.MMKV; +import com.ttstd.dialer.BuildConfig; +import com.ttstd.dialer.bean.ApkInstalledInfo; +import com.ttstd.dialer.bean.BaseResponse; +import com.ttstd.dialer.bean.DeveloperOptions; +import com.ttstd.dialer.bean.PushMessage; +import com.ttstd.dialer.bean.req.SnHardwareInfoReq; +import com.ttstd.dialer.config.CommonConfig; +import com.ttstd.dialer.manager.AppManager; +import com.ttstd.dialer.manager.MapManager; +import com.ttstd.dialer.mdm.DeviceManagerService; +import com.ttstd.dialer.network.BaseObserver; +import com.ttstd.dialer.network.OkHttpManager; +import com.ttstd.dialer.utils.CmdUtil; +import com.ttstd.dialer.utils.Logger; +import com.ttstd.dialer.utils.RebootUtils; + +import java.io.File; +import java.util.List; +import java.util.concurrent.Callable; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.schedulers.Schedulers; +import okhttp3.MultipartBody; +import okhttp3.RequestBody; + +public class PushExecutor { + private static final String TAG = "PushExecutor"; + + private MMKV mMMKV = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE); + + @SuppressLint("StaticFieldLeak") + private static PushExecutor sInstance; + + private Context mContext; + private PackageManager mPackageManager; + + private static final String DEVICE_REFRESH = "1"; + private static final String DEVICE_SCREEN_SNAPSHOT = "2"; + private static final String DEVICE_REBOOT = "3"; + private static final String DEVICE_SHUTDOEN = "4"; + private static final String DEVICE_LOCATE = "5"; + private static final String DEVICE_RESTORE = "6"; + private static final String DEVICE_DEVELOPER = "7"; + + private PushExecutor(Context context) { + if (context == null) { + throw new RuntimeException("Context is NULL"); + } + this.mContext = context; + this.mPackageManager = mContext.getPackageManager(); + } + + public static void init(Context context) { + if (sInstance == null) { + synchronized (PushExecutor.class) { + if (sInstance == null) { + sInstance = new PushExecutor(context.getApplicationContext()); + } + } + } + } + + public static PushExecutor getInstance() { + if (sInstance == null) { + throw new IllegalStateException("You must first initialize the PushExecutor"); + } + return sInstance; + } + + /** + * @param pushMessage 将厂家推送消息转换为自定义消息 + * 方便以后更换推送sdk + */ + public void setPushMessage(PushMessage pushMessage) { + String contentType = pushMessage.contentType; + String title = pushMessage.title; + String content = pushMessage.message; + String extra = pushMessage.extra; + + switch (contentType) { + case DEVICE_REFRESH: + getDeviceSettings(); + uploadDeviceInfo(); + break; + case DEVICE_SCREEN_SNAPSHOT: + screenSnapshot(); + break; + case DEVICE_REBOOT: + RebootUtils.rebootDevice(mContext, "push"); + break; + case DEVICE_SHUTDOEN: + RebootUtils.shutdownDevice(mContext); + case DEVICE_LOCATE: + MapManager.getInstance().startLocation(); + break; + case DEVICE_RESTORE: + + break; + case DEVICE_DEVELOPER: + + break; + default: + Logger.e(TAG, "setPushMessage: default"); + } + } + + private void getDeviceSettings() { + OkHttpManager.getInstance().getDeveloperOptionsObservable() + .subscribe(new Observer>() { + @Override + public void onSubscribe(@NonNull Disposable d) { + Logger.e("getDeveloperOptions", "onSubscribe: "); + } + + @Override + public void onNext(@NonNull BaseResponse baseResponse) { + Logger.e("getDeveloperOptions", "onNext: " + baseResponse); + if (baseResponse.isSuccess()) { + DeveloperOptions developerOptions = baseResponse.getData(); + if (!BuildConfig.DEBUG) { + DeviceManagerService.getInstance().setDevelopmentOption(developerOptions.getDeveloperOptions() == 1); + DeviceManagerService.getInstance().setUsbDebugMode(developerOptions.getDeveloperOptions() == 1); + } + } + } + + @Override + public void onError(@NonNull Throwable e) { + Logger.e("getDeveloperOptions", "onError: " + e.getMessage()); + } + + @Override + public void onComplete() { + Logger.e("getDeveloperOptions", "onComplete: "); + } + }); + } + + private void uploadDeviceInfo() { + Observable + .fromCallable(new Callable>() { + @Override + public List call() throws Exception { + long time = System.currentTimeMillis(); + List apkInstalledInfos = AppManager.getInstance().getApkInstallInfos(); + Log.e(TAG, "call: get apkInstalledInfos time: " + (System.currentTimeMillis() - time) + "ms"); + //io 14738ms + //newThread 14351ms + + return apkInstalledInfos; + } + }) + .subscribeOn(Schedulers.computation()) + .observeOn(AndroidSchedulers.mainThread()) + .flatMap(new Function, Observable>() { + @Override + public Observable apply(List apkInstalledInfos) throws Throwable { + RequestBody body = OkHttpManager.convertToRequestBodyjson(apkInstalledInfos); + return OkHttpManager.getInstance().getUploadInstallApksObservable(body); + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer() { + @Override + public void onSubscribe(@NonNull Disposable d) { + Logger.e("uploadDeviceInfo", "onSubscribe: "); + } + + @Override + public void onNext(@NonNull BaseResponse baseResponse) { + Logger.e("uploadDeviceInfo", "onNext: " + baseResponse); + } + + @Override + public void onError(@NonNull Throwable e) { + Logger.e("uploadDeviceInfo", "onError: ", e); + } + + @Override + public void onComplete() { + Logger.e("uploadDeviceInfo", "onComplete: "); + } + }); + + + SnHardwareInfoReq snHardwareInfoReq = new SnHardwareInfoReq(); + snHardwareInfoReq.setImei(DeviceManagerService.getInstance().getImei()); + snHardwareInfoReq.setImsi(DeviceManagerService.getInstance().getImsi()); + snHardwareInfoReq.setWlanMac(DeviceManagerService.getInstance().getWlanMac()); + snHardwareInfoReq.setDeviceMac(DeviceManagerService.getInstance().getDeviceMac()); + snHardwareInfoReq.setBluetoothMac(DeviceManagerService.getInstance().getBluetoothMac()); + + snHardwareInfoReq.setModel(Build.MODEL); + snHardwareInfoReq.setBrand(Build.BRAND); + snHardwareInfoReq.setBoard(Build.BOARD); + snHardwareInfoReq.setAndroidApi(Build.VERSION.SDK_INT); + snHardwareInfoReq.setAndroidVersion(Build.VERSION.RELEASE); + snHardwareInfoReq.setBuildId(Build.ID); + snHardwareInfoReq.setBuildDisplayId(Build.DISPLAY); + + OkHttpManager.getInstance().getUpdateHardwareInfoObservable(snHardwareInfoReq) + .subscribe(new Observer>() { + @Override + public void onSubscribe(@NonNull Disposable d) { + Log.e("updateHardwareInfo", "onSubscribe: "); + } + + @Override + public void onNext(@NonNull BaseResponse voidBaseResponse) { + Log.e("updateHardwareInfo", "onNext: " + voidBaseResponse); + } + + @Override + public void onError(@NonNull Throwable e) { + Log.e("updateHardwareInfo", "onError: " + e.getMessage()); + } + + @Override + public void onComplete() { + Log.e("updateHardwareInfo", "onComplete: "); + } + }); + } + + private void screenSnapshot() { + String filepath = mContext.getExternalCacheDir().getAbsolutePath() + File.separator + "screenshot_" + System.currentTimeMillis() + ".png"; + + Observable.fromCallable(new Callable() { + @Override + public Integer call() throws Exception { + CmdUtil.Result result = CmdUtil.execute("screencap -p " + filepath); + Logger.e(TAG, "call: " + result.error); + return result.code; + } + }) + .subscribeOn(Schedulers.io()) // 在子线程执行截图 + .flatMap(new Function>() { + @Override + public Observable apply(Integer integer) throws Throwable { + if (integer == 0) { + // 截图成功,上传文件 + MultipartBody.Part part = OkHttpManager.getBodyPart("image/png", "file", new File(filepath)); + return OkHttpManager.getInstance().getUploadScreenshotObservable(part) + .subscribeOn(Schedulers.io()); // 确保上传操作在子线程执行 + } else { + // 截图失败,返回一个空的Observable + return Observable.empty(); + } + } + }) + .observeOn(AndroidSchedulers.mainThread()) // 在主线程回调 + .subscribe(new BaseObserver() { // 注意这里应该是BaseResponse,不是Observable + @Override + public void onSuccess(BaseResponse baseResponse) { + Log.e("screenSnapshot", "onSuccess: " + baseResponse.toString()); + } + + @Override + public void onFailure(Throwable e) { + Log.e("onFailure", "onFailure: " + e.getMessage()); + } + }); + } + + +} diff --git a/app/src/main/java/com/ttstd/dialer/push/jpush/PushMessageService.java b/app/src/main/java/com/ttstd/dialer/push/jpush/PushMessageService.java index e221ab4..6123553 100644 --- a/app/src/main/java/com/ttstd/dialer/push/jpush/PushMessageService.java +++ b/app/src/main/java/com/ttstd/dialer/push/jpush/PushMessageService.java @@ -1,30 +1,95 @@ package com.ttstd.dialer.push.jpush; import android.content.Context; -import android.util.Log; +import android.content.Intent; + +import com.ttstd.dialer.bean.PushMessage; +import com.ttstd.dialer.push.PushExecutor; +import com.ttstd.dialer.service.main.MainService; +import com.ttstd.dialer.utils.Logger; import cn.jpush.android.api.CustomMessage; +import cn.jpush.android.api.JPushInterface; import cn.jpush.android.api.JPushMessage; +import cn.jpush.android.api.NotificationMessage; import cn.jpush.android.service.JPushMessageReceiver; public class PushMessageService extends JPushMessageReceiver { private static final String TAG = "PushMessageService"; + @Override public void onMessage(Context context, CustomMessage customMessage) { super.onMessage(context, customMessage); + Logger.e(TAG, "onMessage: " + customMessage); + + PushMessage pushMessage = new PushMessage(customMessage); + PushExecutor.getInstance().setPushMessage(pushMessage); + + Intent intent = new Intent(context, MainService.class); + intent.putExtra("PushMessage", pushMessage); + context.sendBroadcast(intent); + + String contentType = customMessage.contentType; + String title = customMessage.title; + String content = customMessage.message; + String extra = customMessage.extra; + } @Override public void onAliasOperatorResult(Context context, JPushMessage jPushMessage) { super.onAliasOperatorResult(context, jPushMessage); int errorCode = jPushMessage.getErrorCode(); - Log.e(TAG, "onAliasOperatorResult: " + errorCode); + Logger.e(TAG, "onAliasOperatorResult: errorCode = " + errorCode); if (errorCode == 0) { - Log.e(TAG, "onAliasOperatorResult: " + jPushMessage.getAlias()); - + Logger.e(TAG, "onAliasOperatorResult: " + jPushMessage.getAlias()); + Logger.e(TAG, "onAliasOperatorResult: registrationId = " + JPushInterface.getRegistrationID(context)); } else { } } + + @Override + public void onTagOperatorResult(Context context, JPushMessage jPushMessage) { + super.onTagOperatorResult(context, jPushMessage); + Logger.e(TAG, "onTagOperatorResult: "); + } + + + @Override + public void onNotifyMessageOpened(Context context, NotificationMessage notificationMessage) { + super.onNotifyMessageOpened(context, notificationMessage); + Logger.e(TAG, "onNotifyMessageOpened: "); + } + + @Override + public void onNotifyMessageArrived(Context context, NotificationMessage notificationMessage) { + super.onNotifyMessageArrived(context, notificationMessage); + Logger.e(TAG, "onNotifyMessageArrived: "); + } + + @Override + public void onNotifyMessageUnShow(Context context, NotificationMessage notificationMessage) { + super.onNotifyMessageUnShow(context, notificationMessage); + Logger.e(TAG, "onNotifyMessageUnShow: "); + } + + @Override + public void onNotifyMessageDismiss(Context context, NotificationMessage notificationMessage) { + super.onNotifyMessageDismiss(context, notificationMessage); + Logger.e(TAG, "onNotifyMessageDismiss: "); + } + + @Override + public void onInAppMessageShow(Context context, NotificationMessage notificationMessage) { + super.onInAppMessageShow(context, notificationMessage); + Logger.e(TAG, "onInAppMessageShow: "); + } + + @Override + public void onInAppMessageClick(Context context, NotificationMessage notificationMessage) { + super.onInAppMessageClick(context, notificationMessage); + Logger.e(TAG, "onInAppMessageClick: "); + } } diff --git a/app/src/main/java/com/ttstd/dialer/receiver/AppChangedReceiver.java b/app/src/main/java/com/ttstd/dialer/receiver/AppChangedReceiver.java index aeb0a14..6b2c6ca 100644 --- a/app/src/main/java/com/ttstd/dialer/receiver/AppChangedReceiver.java +++ b/app/src/main/java/com/ttstd/dialer/receiver/AppChangedReceiver.java @@ -4,10 +4,10 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.text.TextUtils; -import android.util.Log; import com.tencent.mmkv.MMKV; import com.ttstd.dialer.config.CommonConfig; +import com.ttstd.dialer.utils.Logger; public class AppChangedReceiver extends BroadcastReceiver { private static final String TAG = "ApkInstallReceiver"; @@ -17,20 +17,20 @@ public class AppChangedReceiver extends BroadcastReceiver { @Override public void onReceive(final Context context, Intent intent) { String action = intent.getAction(); - Log.e(TAG, "onReceive: " + "action = " + action); + Logger.e(TAG, "onReceive: " + "action = " + action); if (TextUtils.isEmpty(action)) { return; } String packageName = intent.getDataString().replace("package:", ""); switch (action) { case Intent.ACTION_PACKAGE_ADDED: - Log.e(TAG, "onReceive: added " + packageName); + Logger.e(TAG, "onReceive: added " + packageName); break; case Intent.ACTION_PACKAGE_REPLACED: - Log.e(TAG, "onReceive: replaced " + packageName); + Logger.e(TAG, "onReceive: replaced " + packageName); break; case Intent.ACTION_PACKAGE_REMOVED: - Log.e(TAG, "onReceive: removed " + packageName); + Logger.e(TAG, "onReceive: removed " + packageName); break; default: break; diff --git a/app/src/main/java/com/ttstd/dialer/service/DialerAccessibilityService.java b/app/src/main/java/com/ttstd/dialer/service/DialerAccessibilityService.java index f4dc589..d37643c 100644 --- a/app/src/main/java/com/ttstd/dialer/service/DialerAccessibilityService.java +++ b/app/src/main/java/com/ttstd/dialer/service/DialerAccessibilityService.java @@ -13,9 +13,10 @@ import android.graphics.Rect; import android.os.Build; import android.os.Bundle; import android.os.Handler; +import android.os.Looper; +import android.os.Message; import android.text.TextUtils; import android.util.DisplayMetrics; -import android.util.Log; import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; @@ -26,31 +27,18 @@ import com.hjq.toast.Toaster; import com.tencent.mmkv.MMKV; import com.ttstd.dialer.config.CommonConfig; import com.ttstd.dialer.db.contact.ContactInfo; -import com.ttstd.dialer.utils.SystemUtils; +import com.ttstd.dialer.utils.Logger; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.Random; -import java.util.concurrent.TimeUnit; -import io.reactivex.rxjava3.annotations.NonNull; -import io.reactivex.rxjava3.core.Observable; -import io.reactivex.rxjava3.core.ObservableEmitter; -import io.reactivex.rxjava3.core.ObservableOnSubscribe; -import io.reactivex.rxjava3.functions.Consumer; - -/** - * 通过微信标签最高支持8.0.49,8.0.50 获取不到数据 - * 8.0.54 可以获取 - * 通过 {@link AccessibilityService#getWindows}和修改accessibility-service 配置能遍历屏幕元素 - */ public class DialerAccessibilityService extends AccessibilityService { private static final String TAG = "AccessibilityService"; private MMKV mMMKV = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE); - public static final int ACTION_VIDEO = 1; public static final int ACTION_AUDIO = 2; @@ -73,19 +61,27 @@ public class DialerAccessibilityService extends AccessibilityService { private static final String DIALER_HANDS_FREE_CLOSE_TEXT = "免提,已关闭"; private static final int WAIT_TIME = 1600; + private static final int DEFAULT_DELAY = 500; + private static final int RANDOM_DELAY_RANGE = 300; + + private static final int MSG_PROCESS_STEP = 1001; + private static final int MSG_DELAY_CLICK = 1002; + private static final int MSG_RETRY_STEP = 1003; private Handler mHandler; - private Step mCurrentStep = Step.WAITING; - private String mName = "";//微信昵称 - private String mTagName = "";//微信联系人标签名 + private String mName = ""; + private String mTagName = ""; private boolean mAutoAccept = false; private boolean mAutoHandsFree = false; private ContactInfo mContactInfo; private int mCallType = ACTION_VIDEO; + private int mFindCount = 0; + private static final int MAX_FIND_COUNT = 5; + private final Random mRandom = new Random(); public interface AccessibilityEventCallback { void onAccessibilityEventCallback(AccessibilityEvent accessibilityEvent); @@ -96,42 +92,77 @@ public class DialerAccessibilityService extends AccessibilityService { @Override public void onCreate() { super.onCreate(); - Log.e(TAG, "onCreate: "); + Logger.e(TAG, "onCreate: "); + initHandler(); registerSettingReceiver(); mAutoAccept = mMMKV.decodeBool(CommonConfig.WECHAT_AUTO_ACCEPT_CALL, false); mAutoHandsFree = mMMKV.decodeBool(CommonConfig.WECHAT_AUTO_HNADS_FREE, false); analysisAccessibilityEvent(); } - private void analysisAccessibilityEvent() { - Observable.create(new ObservableOnSubscribe() { + private void initHandler() { + mHandler = new Handler(Looper.getMainLooper()) { @Override - public void subscribe(@NonNull ObservableEmitter emitter) throws Throwable { - mAccessibilityEventCallback = emitter::onNext; + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_PROCESS_STEP: + processCurrentStep(); + break; + case MSG_DELAY_CLICK: + ClickInfo clickInfo = (ClickInfo) msg.obj; + performClick(clickInfo.node, clickInfo.simulate); + if (clickInfo.nextStep != null) { + mCurrentStep = clickInfo.nextStep; + sendProcessStepMessage(DEFAULT_DELAY); + } + break; + case MSG_RETRY_STEP: + mFindCount++; + if (mFindCount <= MAX_FIND_COUNT) { + processCurrentStep(); + } else { + Logger.e(TAG, "Max retry count reached, reset to waiting"); + mFindCount = 0; + mCurrentStep = Step.WAITING; + } + break; + } } - }).throttleLast(WAIT_TIME, TimeUnit.MILLISECONDS) - .subscribe(new Consumer() { - @Override - public void accept(AccessibilityEvent accessibilityEvent) throws Throwable { - Log.e(TAG, "analysisAccessibilityEvent accept: "); - _onAccessibilityEvent(accessibilityEvent); - } - }); - + }; } + private void sendProcessStepMessage(long delay) { + mHandler.removeMessages(MSG_PROCESS_STEP); + mHandler.sendEmptyMessageDelayed(MSG_PROCESS_STEP, delay); + } + + private void sendDelayClickMessage(AccessibilityNodeInfo node, boolean simulate, Step nextStep) { + Message msg = mHandler.obtainMessage(MSG_DELAY_CLICK); + msg.obj = new ClickInfo(node, simulate, nextStep); + long delay = DEFAULT_DELAY + mRandom.nextInt(RANDOM_DELAY_RANGE); + mHandler.sendMessageDelayed(msg, delay); + } + + private void sendRetryStepMessage(long delay) { + mHandler.removeMessages(MSG_RETRY_STEP); + mHandler.sendEmptyMessageDelayed(MSG_RETRY_STEP, delay); + } + + private void analysisAccessibilityEvent() { + } @Override public int onStartCommand(Intent intent, int flags, int startId) { - Log.e(TAG, "onStartCommand: "); + Logger.e(TAG, "onStartCommand: "); if (intent != null) { mContactInfo = (ContactInfo) intent.getSerializableExtra("ContactInfo"); - Log.e(TAG, "onStartCommand: contactInfo = " + mContactInfo); + Logger.e(TAG, "onStartCommand: contactInfo = " + mContactInfo); mCallType = intent.getIntExtra("call_type", ACTION_VIDEO); - mName = mContactInfo.getName(); + mName = mContactInfo != null ? mContactInfo.getName() : ""; mCurrentStep = Step.CLICK_HOME; if (mContactInfo != null) { startWeixin(); + sendProcessStepMessage(WAIT_TIME); } } return super.onStartCommand(intent, flags, startId); @@ -140,7 +171,10 @@ public class DialerAccessibilityService extends AccessibilityService { @Override public void onDestroy() { super.onDestroy(); - Log.e(TAG, "onDestroy: "); + Logger.e(TAG, "onDestroy: "); + if (mHandler != null) { + mHandler.removeCallbacksAndMessages(null); + } if (mSettingReceiver != null) { unregisterReceiver(mSettingReceiver); } @@ -148,31 +182,37 @@ public class DialerAccessibilityService extends AccessibilityService { @Override public void onAccessibilityEvent(AccessibilityEvent event) { - Log.v(TAG, "onAccessibilityEvent: event = " + event.toString()); + Logger.v(TAG, "onAccessibilityEvent: event = " + event.toString()); checkClassName(event); - mAccessibilityEventCallback.onAccessibilityEventCallback(event); + if (mAccessibilityEventCallback != null) { + mAccessibilityEventCallback.onAccessibilityEventCallback(event); + } + if (mCurrentStep != Step.WAITING) { + sendProcessStepMessage(100); + } + } + + @Override + public void onInterrupt() { + } private void checkClassName(AccessibilityEvent event) { - Log.e(TAG, "checkClassName: mCurrentStep = " + mCurrentStep); + Logger.e(TAG, "checkClassName: mCurrentStep = " + mCurrentStep); if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { - String currentPackageName = event.getPackageName().toString(); - String currentClassName = event.getClassName().toString(); + String currentPackageName = event.getPackageName() != null ? event.getPackageName().toString() : ""; + String currentClassName = event.getClassName() != null ? event.getClassName().toString() : ""; switch (mCurrentStep) { case WAITING: if (!TextUtils.isEmpty(currentPackageName) && "com.android.incallui".equals(currentPackageName)) { - Log.e(TAG, "checkClassName: to dialer hands free"); -// mCurrentStep = Step.DIALER_HANDS_FREE; + Logger.e(TAG, "checkClassName: to dialer hands free"); } break; default: if (!TextUtils.isEmpty(currentClassName)) { switch (currentClassName) { case "com.tencent.mm.ui.LauncherUI": -// if (mCurrentStep != Step.FIND_CONTACT) { -// mCurrentStep = Step.CLICK_CONTACT; -// } break; case "com.tencent.mm.plugin.account.ui.WelcomeActivity": case "com.tencent.mm.plugin.account.ui.LoginPasswordUI": @@ -180,7 +220,6 @@ public class DialerAccessibilityService extends AccessibilityService { mCurrentStep = Step.WAITING; break; case "com.tencent.mm.plugin.label.ui.ContactLabelManagerUI": - break; default: } @@ -189,134 +228,159 @@ public class DialerAccessibilityService extends AccessibilityService { } } - /** - * 1.在微信页面直接找到联系人拨打电话 - * 2.在联系人页面找到并拨打 - * 3.通过进入联系人-标签找到并拨打 - * - * @param event - */ - private void _onAccessibilityEvent(AccessibilityEvent event) { - Log.e(TAG, "_onAccessibilityEvent: " + mCurrentStep); + private void processCurrentStep() { + Logger.e(TAG, "processCurrentStep: " + mCurrentStep); switch (mCurrentStep) { case WAITING: mAutoAccept = mMMKV.decodeBool(CommonConfig.WECHAT_AUTO_ACCEPT_CALL, false); - Log.e(TAG, "_onAccessibilityEvent: mAutoAccept = " + mAutoAccept); + Logger.e(TAG, "processCurrentStep: mAutoAccept = " + mAutoAccept); if (mAutoAccept) { autoAccept(); } break; case WECHAT_HANDS_FREE: mAutoHandsFree = mMMKV.decodeInt(CommonConfig.WECHAT_AUTO_HNADS_FREE, 0) == 1; - Log.e(TAG, "_onAccessibilityEvent: mAutoHandsFree = " + mAutoHandsFree); + Logger.e(TAG, "processCurrentStep: mAutoHandsFree = " + mAutoHandsFree); if (mAutoHandsFree) { - handsFree(Property.DESCRIPTION, HANDS_FREE_TEXT); + handleHandsFree(Property.DESCRIPTION, HANDS_FREE_TEXT, true); } else { - Log.e(TAG, "_onAccessibilityEvent: not enable auto handsfree"); + Logger.e(TAG, "processCurrentStep: not enable auto handsfree"); + mCurrentStep = Step.WAITING; } + break; case DIALER_HANDS_FREE: - if (findHandsFree(Property.DESCRIPTION, DIALER_HANDS_FREE_CLOSE_TEXT)) { - dialerHandsFree(Property.TEXT, DIALER_HANDS_FREE_TEXT); + if (findNodeByProperty(Property.DESCRIPTION, DIALER_HANDS_FREE_CLOSE_TEXT, true) != null) { + handleHandsFree(Property.TEXT, DIALER_HANDS_FREE_TEXT, false); } else { mCurrentStep = Step.WAITING; } break; - case CLICK_HOME://主页能找到直接点击进去更多 - if (stepHome(Property.TEXT, mName)) { - Log.e(TAG, "_onAccessibilityEvent: not found contact in home"); + case CLICK_HOME: + if (findAndClick(Property.TEXT, mName, false, Step.CLICK_QUICK_WECHAT_CALL)) { + Logger.e(TAG, "processCurrentStep: found contact in home"); } else { clickViewById("com.tencent.mm:id/jha", Step.CLICK_SEARCH); -// step(Property.DESCRIPTION, SEARCH_TEXT, Step.CLICK_SEARCH); } break; case CLICK_SEARCH: putString(mName, Step.CLICK_SEARCH_CONTACT); break; case CLICK_SEARCH_CONTACT: - if (findSearchContact(Step.FIND_CONTACT)) { - findSearchContact(Property.TEXT, mName, Step.CLICK_QUICK_WECHAT_CALL); + if (findNodesByViewId("com.tencent.mm:id/gzf").size() > 0) { + findSearchContactAndClick("com.tencent.mm:id/odf", Step.CLICK_QUICK_WECHAT_CALL); } else { Toaster.show("没有找到联系人"); + mCurrentStep = Step.WAITING; } break; - case CLICK_QUICK_WECHAT_CALL://点击更多页面 + case CLICK_QUICK_WECHAT_CALL: clickViewById("com.tencent.mm:id/bjz", Step.CLICK_TARGET); -// step(Property.DESCRIPTION, MORE_NAME, Step.CLICK_TARGET); break; - case CLICK_TARGET://点击视频通话 + case CLICK_TARGET: stepCall(Property.TEXT, PARENT_VIDEO_TEXT); -// clickVideoCall(); break; - case CLICK_CALL://打视频或者电话 + case CLICK_CALL: if (mCallType == ACTION_AUDIO) { - step(Property.TEXT, VIDEO_TEXT, Step.WAITING); + findAndClick(Property.TEXT, VIDEO_TEXT, false, Step.WAITING); } else if (mCallType == ACTION_VIDEO) { - step(Property.TEXT, CALL_TEXT, Step.WAITING); + findAndClick(Property.TEXT, CALL_TEXT, false, Step.WAITING); } break; - - case CLICK_CONTACT://进入通讯录界面 - if (stepHome(Property.TEXT, CONTACT_TEXT, Step.FIND_TAG)) { - Log.e(TAG, "_onAccessibilityEvent: enter contact"); - } else { + case CLICK_CONTACT: + if (!findAndClick(Property.TEXT, CONTACT_TEXT, false, Step.FIND_TAG)) { touchContact(); } break; - case FIND_CONTACT://模拟滑动找到联系人 - findSearchContact(Property.TEXT, mName, Step.CLICK_QUICK_WECHAT_CALL); + case FIND_CONTACT: + findContactWithScroll(Property.TEXT, mName, Step.CLICK_QUICK_WECHAT_CALL); break; case FIND_TAG: - step(Property.TEXT, TAG_TEXT, Step.CLICK_TAG); + findAndClick(Property.TEXT, TAG_TEXT, false, Step.CLICK_TAG); break; case CLICK_TAG: - if (!step(Property.TEXT, mTagName, Step.CLICK_NAME)) { + if (!findAndClick(Property.TEXT, mTagName, false, Step.CLICK_NAME)) { Toaster.show("没有找到标签"); mCurrentStep = Step.WAITING; } break; - case CLICK_NAME://点击item - findContact(Property.TEXT, mName, Step.CLICK_INFO); + case CLICK_NAME: + findContactWithScroll(Property.TEXT, mName, Step.CLICK_INFO); break; - case CLICK_INFO://进入个人信息页面 - stepCallDialog(Property.TEXT, DIALER_TEXT, Step.CLICK_CALL); + case CLICK_INFO: + findAndClickWithScroll(Property.TEXT, DIALER_TEXT, Step.CLICK_CALL); break; - -// case CLICK_VIDEO_CALL: -// if (step(Property.TEXT, VIDEO_TEXT)) { -// Log.d(TAG, "finish, now: " + mCurrentStep); -// ToastUtils.show("成功发起视频聊天"); -// } -// break; default: } } - @Override - public void onInterrupt() { - Log.e(TAG, "onInterrupt: "); - + private boolean findAndClick(Property type, String text, boolean searchWindows, Step nextStep) { + AccessibilityNodeInfo node = findNodeByProperty(type, text, searchWindows); + if (node != null && isNodeVisible(node)) { + sendDelayClickMessage(node, false, nextStep); + return true; + } + return false; } - @Override - protected void onServiceConnected() { - super.onServiceConnected(); - Log.e(TAG, "onServiceConnected: "); + private void findAndClickWithScroll(Property type, String text, Step nextStep) { + AccessibilityNodeInfo node = findNodeByProperty(type, text, false); + if (node != null) { + if (node.isVisibleToUser()) { + sendDelayClickMessage(node, false, nextStep); + mFindCount = 0; + } else { + scrollDown(); + sendRetryStepMessage(WAIT_TIME); + } + } else { + handleNotFoundWithRetry("没有找到联系人", nextStep); + } } - /** - * 打开这个界面可以直接选择联系人拨打 - */ - private void openVoip() { - Intent intent = new Intent(); - ComponentName component = new ComponentName("com.tencent.mm", "com.tencent.mm.ui.contact.VoipAddressUI"); - intent.setComponent(component); - intent.putExtra("voip_video", false); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); + private void findContactWithScroll(Property type, String text, Step nextStep) { + AccessibilityNodeInfo node = findNodeByProperty(type, text, false); + if (node != null) { + sendDelayClickMessage(node, false, nextStep); + mFindCount = 0; + } else { + handleNotFoundWithRetry("没有找到联系人", nextStep); + } + } + + private void handleNotFoundWithRetry(String errorMsg, Step nextStep) { + if (mFindCount >= MAX_FIND_COUNT) { + Logger.e(TAG, "handleNotFoundWithRetry: max count reached"); + ToastUtils.showShort(errorMsg); + mCurrentStep = Step.WAITING; + mFindCount = 0; + } else { + Logger.e(TAG, "handleNotFoundWithRetry: not found, count=" + mFindCount); + mFindCount++; + scrollDown(); + sendRetryStepMessage(WAIT_TIME); + } + } + + private void handleHandsFree(Property type, String text, boolean isWechat) { + AccessibilityNodeInfo node = findNodeByProperty(type, text, true); + if (node != null) { + Point point = getPointByNode(node); + Logger.e(TAG, "handleHandsFree: " + point); + if (isWechat) { + clickByPoint(point.x, point.y - 50); + clickByPoint(point.x, point.y); + } else { + sendDelayClickMessage(node, false, Step.WAITING); + } + mCurrentStep = Step.WAITING; + } else { + Logger.e(TAG, "handleHandsFree: not found"); + mCurrentStep = Step.WAITING; + } } private void autoAccept() { - if (stepAnswer(Property.DESCRIPTION, RECEIVE_DESCRIPTION)) { + if (findAndClickAnswer(Property.DESCRIPTION, RECEIVE_DESCRIPTION)) { mCurrentStep = Step.WECHAT_HANDS_FREE; ToastUtils.showShort("已自动接听视频/语音"); } else if (clickNode("com.tencent.mm:id/kfp", false)) { @@ -324,171 +388,206 @@ public class DialerAccessibilityService extends AccessibilityService { ToastUtils.showShort("已自动接听视频/语音"); } else { mCurrentStep = Step.WAITING; -// clickAnswer(); } } - /** - * @param text 对应文本 - * @param simulate 是否通过坐标模拟点击 - * @return - */ - private boolean clickNode(String text, boolean simulate) { - findFloatWindowNode(text); - List nodeInfos = findNodesByViewId(text); + private boolean findAndClickAnswer(Property type, String text) { + AccessibilityNodeInfo node = findNodeByProperty(type, text, true); + if (node != null) { + Point point = getPointByNode(node); + Logger.e(TAG, "findAndClickAnswer: " + point); + clickByPoint(point.x, point.y - 50); + clickByPoint(point.x, point.y); + mCurrentStep = Step.WAITING; + return true; + } + return false; + } + + private boolean clickNode(String id, boolean simulate) { + findFloatWindowNode(id); + List nodeInfos = findNodesByViewId(id); Optional optional = nodeInfos.stream().findAny(); if (optional.isPresent()) { AccessibilityNodeInfo node = optional.get(); if (node.isClickable()) { boolean performAction = node.performAction(AccessibilityNodeInfo.ACTION_CLICK); - Log.e(TAG, "clickNode: performAction = " + performAction); + Logger.e(TAG, "clickNode: performAction = " + performAction); node.recycle(); return performAction; } else { if (simulate) { - Point point = getPointtByNode(node); - Log.e(TAG, "clickNode: " + point); + Point point = getPointByNode(node); + Logger.e(TAG, "clickNode: " + point); clickByPoint(point.x, point.y); - Log.e(TAG, "clickNode: mCurrentStep " + mCurrentStep + " done"); + Logger.e(TAG, "clickNode: mCurrentStep " + mCurrentStep + " done"); } else { - clickNode(findClickableNode(node)); + AccessibilityNodeInfo clickableNode = findClickableNode(node); + if (clickableNode != null) { + sendDelayClickMessage(clickableNode, false, null); + } } } return true; } else { - Log.e(TAG, "clickNode: not found"); + Logger.e(TAG, "clickNode: not found"); return false; } } + private void performClick(AccessibilityNodeInfo node, boolean simulate) { + if (node == null) { + Logger.e(TAG, "performClick: node is null"); + return; + } + try { + Logger.e(TAG, "performClick: getText = " + node.getText()); + Logger.e(TAG, "performClick: isClickable = " + node.isClickable()); + } catch (Exception e) { + Logger.e(TAG, "performClick: e = " + e.getMessage()); + } + if (node.isClickable()) { + boolean performAction = node.performAction(AccessibilityNodeInfo.ACTION_CLICK); + Logger.e(TAG, "performClick: performAction = " + performAction); + if (!performAction) { + Rect rect = new Rect(); + node.getBoundsInScreen(rect); + Logger.e(TAG, "performClick: rect = " + rect); + int centerX = (rect.left + rect.right) / 2; + int centerY = (rect.top + rect.bottom) / 2; + Logger.e(TAG, "performClick: clickByNode = " + clickByPoint(centerX, centerY)); + } + node.recycle(); + } else { + Rect rect = new Rect(); + node.getBoundsInScreen(rect); + Logger.e(TAG, "performClick: rect = " + rect); + int centerX = (rect.left + rect.right) / 2; + int centerY = (rect.top + rect.bottom) / 2; + Logger.e(TAG, "performClick: clickByNode = " + clickByPoint(centerX, centerY)); + } + } + private AccessibilityNodeInfo findClickableNode(AccessibilityNodeInfo node) { if (node == null) { - Log.e(TAG, "findClickableNode: node is null"); + Logger.e(TAG, "findClickableNode: node is null"); return null; } if (node.isClickable()) { return node; } else { - return findClickableNode(node); + AccessibilityNodeInfo parent = node.getParent(); + node.recycle(); + return parent != null ? findClickableNode(parent) : null; } } public void findFloatWindowNode(String id) { List windows = getWindows(); for (AccessibilityWindowInfo window : windows) { - // 筛选悬浮窗窗口 if (isFloatingWindow(window)) { AccessibilityNodeInfo rootNode = window.getRoot(); - // 处理悬浮窗节点 traverseNode(rootNode); - rootNode.recycle(); // 释放资源 + if (rootNode != null) { + rootNode.recycle(); + } } } - } private boolean isFloatingWindow(AccessibilityWindowInfo window) { - Log.e(TAG, "isFloatingWindow: " + window.getType()); - // 根据窗口类型或标题筛选 + Logger.e(TAG, "isFloatingWindow: " + window.getType()); return window.getType() == AccessibilityWindowInfo.TYPE_ACCESSIBILITY_OVERLAY; } private void traverseNode(AccessibilityNodeInfo node) { if (node == null) return; - // 提取节点信息(如文本、ID等) String text = node.getText() != null ? node.getText().toString() : ""; String id = node.getViewIdResourceName(); - // 递归遍历子节点 for (int i = 0; i < node.getChildCount(); i++) { traverseNode(node.getChild(i)); } } - @Deprecated - private void clickAnswer() { - String className = SystemUtils.getForegroundActivityClassName(DialerAccessibilityService.this); - Log.e(TAG, "clickAnswer: " + className); - if (!TextUtils.isEmpty(className)) { - if ("com.tencent.mm.plugin.voip.ui.VideoActivity".contentEquals(className)) { - boolean successful = clickByPoint(595, 1376); - Log.e(TAG, "clickAnswer: " + successful); - if (successful) { - ToastUtils.showShort("已自动接听视频/语音"); + private AccessibilityNodeInfo findNodeByProperty(Property type, String text, boolean searchWindows) { + if (searchWindows) { + return findNodeInWindows(type, text); + } else { + return findNodeInActiveWindow(getRootInActiveWindow(), type, text); + } + } + + private AccessibilityNodeInfo findNodeInWindows(Property type, String text) { + for (AccessibilityWindowInfo window : getWindows()) { + AccessibilityNodeInfo root = window.getRoot(); + AccessibilityNodeInfo node = findNodeInActiveWindow(root, type, text); + if (node != null) { + return node; + } + if (root != null) { + root.recycle(); + } + } + Logger.e(TAG, "findNodeInWindows: not found"); + return null; + } + + private AccessibilityNodeInfo findNodeInActiveWindow(AccessibilityNodeInfo root, Property type, String text) { + if (root == null) return null; + Logger.v(TAG, "findNodeInActiveWindow: getText = " + root.getText()); + Logger.v(TAG, "findNodeInActiveWindow: getClassName = " + root.getClassName()); + Logger.v(TAG, "findNodeInActiveWindow: getContentDescription = " + root.getContentDescription()); + boolean satisfied = checkPropertyMatch(root, type, text); + if (satisfied) { + return root; + } else { + for (int i = 0; i < root.getChildCount(); i++) { + AccessibilityNodeInfo result = findNodeInActiveWindow(root.getChild(i), type, text); + if (result != null) { + return result; } - } else { - Log.e(TAG, "clickAnswer: Not in the answering interface"); } } + root.recycle(); + return null; } - private boolean step(Property type, String text, Step nextStep) { - AccessibilityNodeInfo node = findNode(getRootInActiveWindow(), type, text); - if (node != null) { - Rect rect = new Rect(); - node.getBoundsInScreen(rect); - Log.e(TAG, "step: rect = " + rect); - if (rect.left < 0 || rect.top < 0 || rect.right < 0 || rect.bottom < 0) { + private boolean checkPropertyMatch(AccessibilityNodeInfo node, Property type, String text) { + switch (type) { + case TEXT: + return node.getText() != null && text.contentEquals(node.getText()); + case CLASS_NAME: + return node.getClassName() != null && text.contentEquals(node.getClassName()); + case DESCRIPTION: + return node.getContentDescription() != null && text.contentEquals(node.getContentDescription()); + default: return false; - } - clickNode(node); - Log.e(TAG, "step: mCurrentStep: " + mCurrentStep + " done"); - mCurrentStep = nextStep; - Log.e(TAG, "step: next: " + mCurrentStep); - return true; - } else { - return false; } } - // TODO: 2025/2/8 先把通讯录点击的换成node - private boolean stepHome(Property type, String text, Step nextStep) { - AccessibilityNodeInfo node = findNode(getWindows(), type, text); - if (node != null) { - Rect rect = new Rect(); - node.getBoundsInScreen(rect); - Log.e(TAG, "step: rect = " + rect); - if (rect.left < 0 || rect.top < 0 || rect.right < 0 || rect.bottom < 0) { - return false; - } - clickNode(node); - Log.e(TAG, "step: mCurrentStep: " + mCurrentStep + " done"); - mCurrentStep = nextStep; - Log.e(TAG, "step: next: " + mCurrentStep); - return true; - } else { - return false; - } + private boolean isNodeVisible(AccessibilityNodeInfo node) { + if (node == null) return false; + Rect rect = new Rect(); + node.getBoundsInScreen(rect); + return rect.left >= 0 && rect.top >= 0 && rect.right >= 0 && rect.bottom >= 0; } - @Deprecated - private void touchContact() { - boolean successful = clickByPoint(268, 1440); - if (successful) { - mCurrentStep = Step.FIND_TAG; - } else { - mCurrentStep = Step.WAITING; - Toaster.show("点击失败,请重试"); - } - } - - private boolean findSearchContact(Step nextStep) { - List nodeInfos = findNodesByViewId("com.tencent.mm:id/gzf"); - Log.e(TAG, "findSearchContact: " + nodeInfos); - Optional optional = nodeInfos.stream().findAny(); - return optional.isPresent(); - } - - private void findSearchContact(Property type, String text, Step nextStep) { - List nodeInfos = findNodesByViewId("com.tencent.mm:id/odf"); - Log.e(TAG, "findSearchContact: " + nodeInfos); + private void clickViewById(String id, Step nextStep) { + List nodeInfos = findNodesByViewId(id); Optional optional = nodeInfos.stream().findAny(); if (optional.isPresent()) { - AccessibilityNodeInfo nodeInfo = optional.get(); - clickNode(nodeInfo); - mCurrentStep = nextStep; + sendDelayClickMessage(optional.get(), false, nextStep); } else { - Toaster.show("没有找到联系人"); - mCurrentStep = Step.WAITING; + findAndClick(Property.DESCRIPTION, SEARCH_TEXT, false, Step.CLICK_SEARCH); + } + } + + private List findNodesByViewId(String id) { + AccessibilityNodeInfo nodeInfo = getRootInActiveWindow(); + if (nodeInfo != null) { + return nodeInfo.findAccessibilityNodeInfosByViewId(id); + } else { + return new ArrayList<>(); } } @@ -500,358 +599,89 @@ public class DialerAccessibilityService extends AccessibilityService { Bundle args = new Bundle(); args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text); nodeInfo.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args); - nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLEAR_FOCUS); // 确保焦点在输入框 + nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLEAR_FOCUS); if (Build.VERSION.SDK_INT >= ACTION_IME_ENTER_VERSION) { - //see https://developer.android.com/reference/android/view/accessibility/AccessibilityNodeInfo.AccessibilityAction#ACTION_IME_ENTER nodeInfo.performAction(ACTION_IME_ENTER_ID); } mCurrentStep = nextStep; + sendProcessStepMessage(WAIT_TIME); } else { Toaster.show("没有找到搜索框"); mCurrentStep = Step.WAITING; } } - private void clickViewById(String id, Step nextStep) { + private void findSearchContactAndClick(String id, Step nextStep) { List nodeInfos = findNodesByViewId(id); + Logger.e(TAG, "findSearchContactAndClick: " + nodeInfos); Optional optional = nodeInfos.stream().findAny(); if (optional.isPresent()) { - AccessibilityNodeInfo nodeInfo = optional.get(); - clickNode(nodeInfo); - mCurrentStep = nextStep; + sendDelayClickMessage(optional.get(), false, nextStep); } else { -// Toaster.show("没有找到搜索按钮"); - step(Property.DESCRIPTION, SEARCH_TEXT, Step.CLICK_SEARCH); + Toaster.show("没有找到联系人"); + mCurrentStep = Step.WAITING; } } - private List findNodesByViewId(String id) { - AccessibilityNodeInfo nodeInfo = getRootInActiveWindow(); - if (nodeInfo != null) { - List accessibilityNodeInfos = nodeInfo.findAccessibilityNodeInfosByViewId(id); - return accessibilityNodeInfos; - } else { - return new ArrayList<>(); - } - } - - private AccessibilityNodeInfo findNodeByText(AccessibilityNodeInfo root, String text) { - if (root == null) return null; - Log.e(TAG, "findNodeByText: getText = " + root.getText()); - Log.e(TAG, "findNodeByText: getContentDescription = " + root.getContentDescription()); - boolean found = root.getText() != null && text.contentEquals(root.getText()); - if (found) { - return root; - } else { - for (int i = 0; i < root.getChildCount(); i++) { - AccessibilityNodeInfo result = findNodeByText(root.getChild(i), text); - if (result != null) { - return result; - } - } - } - root.recycle(); - return null; - } - - private boolean stepCallDialog(Property type, String text, Step nextStep) { - AccessibilityNodeInfo node = findNode(getRootInActiveWindow(), type, text); - if (node != null) { - Log.e(TAG, "stepCallDialog: isVisibleToUser: " + node.isVisibleToUser()); - if (node.isVisibleToUser()) { - clickNode(node); - Log.e(TAG, "stepCallDialog: mCurrentStep: " + mCurrentStep + " done"); - mCurrentStep = nextStep; - Log.e(TAG, "stepCallDialog: next: " + mCurrentStep); - return true; - } else { - scrollDown(); - return false; - } - } else { - if (mFindCount == mMaxCount) { - Log.e("stepCallDialog", "mCurrentStep: max"); - ToastUtils.showShort("没有找到联系人"); - mCurrentStep = Step.WAITING; - mFindCount = 0; - return false; - } else { - Log.e("stepCallDialog", "mCurrentStep: not found"); - mFindCount++; - Log.e("stepCallDialog", "mCurrentStep: mFindCount = " + mFindCount); - scrollDown(); - return false; - } - } - } - - private boolean stepHome(Property type, String text) { - AccessibilityNodeInfo node = findNode(getRootInActiveWindow(), type, text); - if (node != null) { - clickNode(node); - Log.e(TAG, "stepHome: mCurrentStep: " + mCurrentStep + " done"); - mCurrentStep = Step.CLICK_QUICK_WECHAT_CALL; - Log.e(TAG, "stepHome: next: " + mCurrentStep); - return true; - } else { - mCurrentStep = Step.CLICK_SEARCH; - return false; - } - } - - private int mFindCount = 0; - private int mMaxCount = 5; - - - private boolean findContact(Property type, String text, Step nextStep) { - AccessibilityNodeInfo node = findNode(getRootInActiveWindow(), type, text); - if (node != null) { - clickNode(node); - Log.e("findContact", "mCurrentStep: " + mCurrentStep + " done"); - mCurrentStep = nextStep; - Log.e("findContact", "next: " + mCurrentStep); - mFindCount = 0; - return true; - } else { - if (mFindCount == mMaxCount) { - Log.e("findContact", "mCurrentStep: max"); - ToastUtils.showShort("没有找到联系人"); - mCurrentStep = Step.WAITING; - mFindCount = 0; - return false; - } else { - Log.e("findContact", "mCurrentStep: not found"); - mFindCount++; - Log.e("findContact", "mCurrentStep: mFindCount = " + mFindCount); - scrollDown(); - return false; - } - } - } - - - private AccessibilityNodeInfo findNode(AccessibilityNodeInfo root, Property type, String text) { - if (root == null) return null; -// Log.v(TAG, "findNode: getPackageName = " + root.getPackageName()); - Log.v(TAG, "findNode: getText = " + root.getText()); - Log.v(TAG, "findNode: getClassName = " + root.getClassName()); - Log.v(TAG, "findNode: getContentDescription = " + root.getContentDescription()); - boolean satisfied = false; - switch (type) { - case TEXT: - satisfied = root.getText() != null && text.contentEquals(root.getText()); - break; - case CLASS_NAME: - satisfied = root.getClassName() != null && text.contentEquals(root.getClassName()); - break; - case DESCRIPTION: - satisfied = root.getContentDescription() != null && text.contentEquals(root.getContentDescription()); - break; - default: - } - if (satisfied) { - return root; - } else { - for (int i = 0; i < root.getChildCount(); i++) { - AccessibilityNodeInfo result = findNode(root.getChild(i), type, text); - if (result != null) { - return result; - } - } - } - root.recycle(); - return null; - } - - private AccessibilityNodeInfo findNode(List windows, Property type, String text) { - for (AccessibilityWindowInfo accessibilityWindowInfo : windows) { - AccessibilityNodeInfo nodeInfo = findNode(accessibilityWindowInfo.getRoot(), type, text); - if (nodeInfo != null) { - return nodeInfo; - } - } - Log.e(TAG, "findNode windows: not found"); - return null; - } - - - private void clickNode(AccessibilityNodeInfo node) { - if (node == null) { - Log.e(TAG, "clickNode: node is null"); - return; - } - try { - Log.e(TAG, "clickNode: getText = " + node.getText()); - Log.e(TAG, "clickNode: isClickable = " + node.isClickable()); - } catch (Exception e) { - Log.e(TAG, "clickNode: e = " + e.getMessage()); - } - if (node.isClickable()) { - //防检测机制: - //添加随机延迟(避免高频操作) -// handler.postDelayed(new Runnable() { -// @Override -// public void run() { -// -// } -// }, 1000 + new Random().nextInt(100)); - boolean performAction = node.performAction(AccessibilityNodeInfo.ACTION_CLICK); - Log.e(TAG, "clickNode: performAction = " + performAction); - if (!performAction) { - Rect rect = new Rect(); - node.getBoundsInScreen(rect); - Log.e(TAG, "clickNode: rect = " + rect); - // 点击节点的中心位置 - int centerX = (rect.left + rect.right) / 2; - int centerY = (rect.top + rect.bottom) / 2; - Log.e(TAG, "clickNode: clickByNode = " + clickByPoint(centerX, centerY)); - } - node.recycle(); - } else { - Rect rect = new Rect(); - node.getBoundsInScreen(rect); - Log.e(TAG, "clickNode: rect = " + rect); - // 点击节点的中心位置 - int centerX = (rect.left + rect.right) / 2; - int centerY = (rect.top + rect.bottom) / 2; - Log.e(TAG, "clickNode: clickByNode = " + clickByPoint(centerX, centerY)); - } -// else { -// AccessibilityNodeInfo parent = node.getParent(); -// node.recycle(); -// clickNode(parent); -// } - } - - @Deprecated private boolean stepCall(Property type, String text) { - AccessibilityNodeInfo node = findNode(getRootInActiveWindow(), type, text); + AccessibilityNodeInfo node = findNodeByProperty(type, text, false); if (node != null) { - Point point = getPointtByNode(node); - Log.e(TAG, "stepCall: " + point); + Point point = getPointByNode(node); + Logger.e(TAG, "stepCall: " + point); clickByPoint(point.x, point.y); -// clickNode(node); - Log.e(TAG, "stepCall: mCurrentStep " + mCurrentStep + " done"); + Logger.e(TAG, "stepCall: mCurrentStep " + mCurrentStep + " done"); mCurrentStep = Step.CLICK_CALL; - Log.e(TAG, "stepCall: next " + mCurrentStep); + Logger.e(TAG, "stepCall: next " + mCurrentStep); + sendProcessStepMessage(WAIT_TIME); return true; } else { - Log.e(TAG, "stepCall: not found"); + Logger.e(TAG, "stepCall: not found"); return false; } } - private void clickVideoCall() { - List nodeInfos = findNodesByViewId("com.tencent.mm:id/a12"); - Optional accessibilityNodeInfo = nodeInfos.stream().findAny(); - if (accessibilityNodeInfo.isPresent()) { - AccessibilityNodeInfo nodeInfo = accessibilityNodeInfo.get(); - clickNode(nodeInfo); - mCurrentStep = Step.CLICK_CALL; + private void touchContact() { + boolean successful = clickByPoint(268, 1440); + if (successful) { + mCurrentStep = Step.FIND_TAG; + sendProcessStepMessage(WAIT_TIME); } else { - Toaster.show("没有找到通话按钮"); - } - } - - private boolean stepAnswer(Property type, String text) { - AccessibilityNodeInfo node = findNode(getWindows(), type, text); - if (node != null) { - Point point = getPointtByNode(node); - Log.e(TAG, "stepAnswer: " + point); - clickByPoint(point.x, point.y - 50); - clickByPoint(point.x, point.y); -// clickNode(node); - Log.e(TAG, "stepAnswer: mCurrentStep " + mCurrentStep + " done"); mCurrentStep = Step.WAITING; - Log.e(TAG, "stepAnswer: next " + mCurrentStep); - return true; - } else { - Log.e(TAG, "stepAnswer: not found"); - return false; + Toaster.show("点击失败,请重试"); } } - private boolean dialerHandsFree(Property type, String text) { - AccessibilityNodeInfo node = findNode(getWindows(), type, text); - if (node != null) { - Rect rect = new Rect(); - node.getBoundsInScreen(rect); - Log.e(TAG, "dialerHandsFree: rect = " + rect); - clickNode(node); - Log.e(TAG, "dialerHandsFree: mCurrentStep: " + mCurrentStep + " done"); - mCurrentStep = Step.WAITING; - Log.e(TAG, "dialerHandsFree: next: " + mCurrentStep); - return true; - } else { - return false; - } - } - - private boolean findHandsFree(Property type, String text) { - AccessibilityNodeInfo node = findNode(getWindows(), type, text); - if (node != null) { - Log.e(TAG, "findHandsFree: true"); - return true; - } else { - Log.e(TAG, "findHandsFree: false"); - return false; - } - } - - private boolean handsFree(Property type, String text) { - AccessibilityNodeInfo node = findNode(getWindows(), type, text); - if (node != null) { - Point point = getPointtByNode(node); - Log.e(TAG, "handsFree: " + point); - clickByPoint(point.x, point.y - 50); - clickByPoint(point.x, point.y); -// clickNode(node); - Log.e(TAG, "handsFree: mCurrentStep " + mCurrentStep + " done"); - mCurrentStep = Step.WAITING; - Log.e(TAG, "handsFree: next " + mCurrentStep); - return true; - } else { - Log.e(TAG, "handsFree: not found"); - mCurrentStep = Step.WAITING; - return false; - } - } - - //根据节点信息可获得对应的x,y坐标 - static Point getPointtByNode(AccessibilityNodeInfo node) { + static Point getPointByNode(AccessibilityNodeInfo node) { if (node == null) { return new Point(0, 0); } Rect rect = new Rect(); node.getBoundsInScreen(rect); - Point point = new Point(rect.centerX(), rect.centerY()); - return point; + return new Point(rect.centerX(), rect.centerY()); } - //实现对(x,y)坐标进行点击操作。 private boolean clickByPoint(int x, int y) { - Log.e(TAG, "clickByNode: x = " + x); - Log.e(TAG, "clickByNode: y = " + y); + Logger.e(TAG, "clickByPoint: x = " + x); + Logger.e(TAG, "clickByPoint: y = " + y); Point point = new Point(x, y); Path path = new Path(); path.moveTo(point.x, point.y); GestureDescription.Builder builder = new GestureDescription.Builder(); - //防检测机制: - //添加随机延迟(避免高频操作) - builder.addStroke(new GestureDescription.StrokeDescription(path, 0, 200 + new Random().nextInt(100))); + int duration = 200 + mRandom.nextInt(100); + builder.addStroke(new GestureDescription.StrokeDescription(path, 0, duration)); GestureDescription gesture = builder.build(); boolean dispatched = dispatchGesture(gesture, new GestureResultCallback() { @Override public void onCompleted(GestureDescription gestureDescription) { super.onCompleted(gestureDescription); - Log.e("clickByNode", "onCompleted: "); + Logger.e("clickByPoint", "onCompleted: "); } @Override public void onCancelled(GestureDescription gestureDescription) { super.onCancelled(gestureDescription); - Log.e("clickByNode", "onCompleted: "); + Logger.e("clickByPoint", "onCancelled: "); } }, null); return dispatched; @@ -861,35 +691,30 @@ public class DialerAccessibilityService extends AccessibilityService { WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE); DisplayMetrics dm = new DisplayMetrics(); wm.getDefaultDisplay().getRealMetrics(dm); - int width = dm.widthPixels; // 屏幕宽度(像素) - int height = dm.heightPixels; // 屏幕高度(像素) - float density = dm.density; // 屏幕密度(0.75 / 1.0 / 1.5) - int densityDpi = dm.densityDpi; // 屏幕密度dpi(120 / 160 / 240) - // 屏幕宽度算法:屏幕宽度(像素)/屏幕密度 -// int screenWidth = (int) (width / density); // 屏幕宽度(dp) -// int screenHeight = (int) (height / density);// 屏幕高度(dp) - Log.e(TAG, "scrollScreen: screenWidth = " + width); - Log.e(TAG, "scrollScreen: screenHeight = " + height); + int width = dm.widthPixels; + int height = dm.heightPixels; + Logger.e(TAG, "scrollScreen: screenWidth = " + width); + Logger.e(TAG, "scrollScreen: screenHeight = " + height); int center_X = width / 2; int center_Y = height / 2; - Log.e("scrollScreen", "center position:" + "(" + center_X + "," + center_Y + ")"); + Logger.e("scrollScreen", "center position:" + "(" + center_X + "," + center_Y + ")"); Path path = new Path(); - path.moveTo(center_X, (int) (center_Y * startY)); //起点坐标。 - path.lineTo(center_X, (int) (center_Y * endY)); //终点坐标。 + path.moveTo(center_X, (int) (center_Y * startY)); + path.lineTo(center_X, (int) (center_Y * endY)); GestureDescription.Builder builder = new GestureDescription.Builder(); GestureDescription gestureDescription = builder.addStroke(new GestureDescription.StrokeDescription(path, 0, 200)).build(); boolean dispatched = dispatchGesture(gestureDescription, new GestureResultCallback() { @Override public void onCompleted(GestureDescription gestureDescription) { super.onCompleted(gestureDescription); - Log.d("scrollScreen", "dispatchGesture ScrollUp onCompleted."); + Logger.d("scrollScreen", "dispatchGesture ScrollUp onCompleted."); path.close(); } @Override public void onCancelled(GestureDescription gestureDescription) { super.onCancelled(gestureDescription); - Log.d("scrollScreen", "dispatchGesture ScrollUp cancel."); + Logger.d("scrollScreen", "dispatchGesture ScrollUp cancel."); } }, null); return dispatched; @@ -905,39 +730,21 @@ public class DialerAccessibilityService extends AccessibilityService { private enum Step { WAITING, - //微信免提 WECHAT_HANDS_FREE, - //电话免提 DIALER_HANDS_FREE, - - //1-1微信主页找用户名 CLICK_HOME, - //2-2进入搜索界面 CLICK_SEARCH, - //2-3是否弹出了联系人列表 CLICK_SEARCH_CONTACT, - //1-2 2-4聊天界面+号 CLICK_QUICK_WECHAT_CALL, - //1-3 2-5更多里面视频通话 CLICK_TARGET, - /*1-4 语音通话*/ CLICK_CALL, - - - //主页点击导航栏通讯录 CLICK_CONTACT, FIND_CONTACT, - //通讯录页面点击标签 FIND_TAG, - //点击对应的标签名 CLICK_TAG, CLICK_NAME, CLICK_INFO, - CLICK_VIDEO_CALL; - - private Step next() { - return values()[(this.ordinal() + 1) % values().length]; - } + CLICK_VIDEO_CALL } private enum Property { @@ -946,6 +753,18 @@ public class DialerAccessibilityService extends AccessibilityService { DESCRIPTION } + private static class ClickInfo { + AccessibilityNodeInfo node; + boolean simulate; + Step nextStep; + + ClickInfo(AccessibilityNodeInfo node, boolean simulate, Step nextStep) { + this.node = node; + this.simulate = simulate; + this.nextStep = nextStep; + } + } + public static final String SETTING_CALL_TYPE_ACTION = "setting_call_type_action"; public static final String SETTING_AUTOMATIC_ANSWER_ACTION = "setting_automatic_answer_action"; @@ -965,18 +784,18 @@ public class DialerAccessibilityService extends AccessibilityService { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); - Log.e("SettingReceiver", "onReceive: " + action); + Logger.e("SettingReceiver", "onReceive: " + action); if (TextUtils.isEmpty(action)) return; switch (action) { case SETTING_CALL_TYPE_ACTION: int callType = intent.getIntExtra("call_type", ACTION_VIDEO); mCallType = callType; - Log.e("SettingReceiver", "onReceive: callType = " + callType); + Logger.e("SettingReceiver", "onReceive: callType = " + callType); break; case SETTING_AUTOMATIC_ANSWER_ACTION: boolean autoAnswer = intent.getBooleanExtra("auto_answer", false); mAutoAccept = autoAnswer; - Log.e("SettingReceiver", "onReceive: autoAnswer = " + autoAnswer); + Logger.e("SettingReceiver", "onReceive: autoAnswer = " + autoAnswer); break; default: } @@ -995,7 +814,7 @@ public class DialerAccessibilityService extends AccessibilityService { try { startActivity(intent); } catch (Exception e) { - Log.e(TAG, "startWeixin: " + e.getMessage()); + Logger.e(TAG, "startWeixin: " + e.getMessage()); } } } diff --git a/app/src/main/java/com/ttstd/dialer/service/main/MainService.java b/app/src/main/java/com/ttstd/dialer/service/main/MainService.java index 143c16d..c70828d 100644 --- a/app/src/main/java/com/ttstd/dialer/service/main/MainService.java +++ b/app/src/main/java/com/ttstd/dialer/service/main/MainService.java @@ -1,6 +1,5 @@ package com.ttstd.dialer.service.main; -import android.app.Service; import android.content.Intent; import android.graphics.PixelFormat; import android.os.Build; @@ -15,22 +14,27 @@ import android.view.View; import android.view.WindowManager; import androidx.annotation.Nullable; +import androidx.lifecycle.Observer; import com.hjq.toast.Toaster; +import com.jeremyliao.liveeventbus.LiveEventBus; import com.shehuan.niv.NiceImageView; import com.tencent.mmkv.MMKV; import com.ttstd.dialer.BuildConfig; import com.ttstd.dialer.R; 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.utils.ApkUtils; +import com.ttstd.dialer.utils.Logger; import com.ttstd.dialer.utils.SystemUtils; import java.util.HashSet; import java.util.List; import java.util.Set; -public class MainService extends Service { +public class MainService extends BaseService { private static final String TAG = "MainService"; public static final String SHOW_FLOAT_WINDOW_ACTION = "show_float_window"; @@ -38,6 +42,8 @@ public class MainService extends Service { private MMKV mMMKV = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE); + private MainServiceModel mViewModel; + private WindowManager mWindowManager; private View floatingView; @@ -53,12 +59,25 @@ public class MainService extends Service { @Override public void onCreate() { super.onCreate(); - Log.e(TAG, "onCreate: "); + Logger.e(TAG, "onCreate: "); + // 初始化ViewModel(绑定上下文+View回调) + mViewModel = new MainServiceModel(); + // 绑定Service生命周期(关键:让请求跟随生命周期) + mViewModel.setLifecycleSubject(getLifecycleSubject()); + + LiveEventBus.get(CommonConfig.LOCATION_CHANGED_KEY, SnLocationReq.class) + .observeSticky(this, new Observer() { + @Override + public void onChanged(SnLocationReq snLocationReq) { + Log.e(TAG, "onChanged: receive snLocationReq"); + mViewModel.uploadLocation(snLocationReq); + } + }); mWindowManager = (WindowManager) getSystemService(WINDOW_SERVICE); boolean enableFloatWindow = mMMKV.decodeInt(CommonConfig.FLOAT_WINDOW_ENABLE, 0) == 1; - Log.e(TAG, "onCreate: enableFloatWindow = " + enableFloatWindow); + Logger.e(TAG, "onCreate: enableFloatWindow = " + enableFloatWindow); if (enableFloatWindow) { showFloatWindow(); } @@ -67,9 +86,10 @@ public class MainService extends Service { @Override public int onStartCommand(Intent intent, int flags, int startId) { + super.onStartCommand(intent, flags, startId); if (intent != null) { String action = intent.getAction(); - Log.e(TAG, "onStartCommand: action = " + action); + Logger.e(TAG, "onStartCommand: action = " + action); if (!TextUtils.isEmpty(action)) { switch (action) { case SHOW_FLOAT_WINDOW_ACTION: @@ -85,7 +105,7 @@ public class MainService extends Service { } private void showFloatWindow() { - Log.e(TAG, "showFloatWindow: "); + Logger.e(TAG, "showFloatWindow: "); initFloatingView(); if (Settings.canDrawOverlays(this)) { @@ -94,14 +114,14 @@ public class MainService extends Service { mWindowManager.addView(floatingView, mLayoutParams); mFloatWindowShowing = true; } catch (Exception e) { - Log.e(TAG, "onCreate: addView = " + e.getMessage()); + Logger.e(TAG, "onCreate: addView = " + e.getMessage()); } } } private void initFloatingView() { - Log.e(TAG, "initFloatingView: "); + Logger.e(TAG, "initFloatingView: "); floatingView = LayoutInflater.from(this).inflate(R.layout.layout_floating_window, null); int layoutType; @@ -130,7 +150,7 @@ public class MainService extends Service { nvBack.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - Log.e(TAG, "onClick: "); + Logger.e(TAG, "onClick: "); // stopSelf(); // 停止服务,触发 onDestroy 移除悬浮窗 // mWindowManager.removeView(floatingView); // floatingView = null; @@ -159,7 +179,7 @@ public class MainService extends Service { @Override public boolean onTouch(View v, MotionEvent event) { - Log.e(TAG, "onTouch: "); + Logger.e(TAG, "onTouch: "); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // 记录初始位置 @@ -187,7 +207,7 @@ public class MainService extends Service { } private void hideFloatWindow() { - Log.e(TAG, "hideFloatWindow: "); + Logger.e(TAG, "hideFloatWindow: "); if (mFloatWindowShowing) { if (floatingView != null) { mWindowManager.removeView(floatingView); @@ -211,7 +231,7 @@ public class MainService extends Service { }}; private void killApp() { - Log.e(TAG, "killApp: "); + Logger.e(TAG, "killApp: "); List runningPackages = SystemUtils.getRunningTaskPackages(MainService.this); int i = 0; for (String pkg : runningPackages) { diff --git a/app/src/main/java/com/ttstd/dialer/service/main/MainServiceModel.java b/app/src/main/java/com/ttstd/dialer/service/main/MainServiceModel.java new file mode 100644 index 0000000..6c63764 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/service/main/MainServiceModel.java @@ -0,0 +1,60 @@ +package com.ttstd.dialer.service.main; + +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.network.BaseObserver; +import com.ttstd.dialer.network.OkHttpManager; + +import io.reactivex.rxjava3.subjects.BehaviorSubject; + +public class MainServiceModel extends ViewModel { + private static final String TAG = "MainServiceModel"; + + private BehaviorSubject lifecycleSubject; + + // 设置Service生命周期Subject + public void setLifecycleSubject(BehaviorSubject lifecycleSubject) { + this.lifecycleSubject = lifecycleSubject; + } + + // 绑定请求到Service销毁事件(自动取消请求) + private LifecycleTransformer bindToLifecycle() { + if (lifecycleSubject == null) { + throw new IllegalStateException("请先设置LifecycleSubject!"); + } + return RxLifecycle.bindUntilEvent(lifecycleSubject, ActivityEvent.DESTROY); + } + + @Override + protected void onCleared() { + super.onCleared(); + // 清空生命周期引用,防止内存泄漏 + this.lifecycleSubject = null; + } + + + public void uploadLocation(SnLocationReq locationReq) { + Log.e(TAG, "uploadLocation: "); + OkHttpManager.getInstance().getUploadLocationObservable(locationReq) + .compose(bindToLifecycle()) + .subscribe(new BaseObserver() { + @Override + public void onSuccess(BaseResponse baseResponse) { + Log.e("uploadLocation", "onSuccess: " + baseResponse); + } + + @Override + public void onFailure(Throwable e) { + Log.e("uploadLocation", "onFailure: " + e.getMessage()); + } + }); + + } +} diff --git a/app/src/main/java/com/ttstd/dialer/utils/AccessibilityServiceHelper.java b/app/src/main/java/com/ttstd/dialer/utils/AccessibilityServiceHelper.java index 2271c60..7ac9f58 100644 --- a/app/src/main/java/com/ttstd/dialer/utils/AccessibilityServiceHelper.java +++ b/app/src/main/java/com/ttstd/dialer/utils/AccessibilityServiceHelper.java @@ -6,7 +6,6 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.provider.Settings; -import android.util.Log; import android.view.accessibility.AccessibilityManager; import com.ttstd.dialer.service.DialerAccessibilityService; @@ -35,9 +34,9 @@ public class AccessibilityServiceHelper { // 构建你服务的完整组件标识符 String expectedServiceId = new ComponentName(context, serviceClass).flattenToShortString(); - Log.e(TAG, "isAccessibilityServiceEnabled: expectedServiceId = " + expectedServiceId); + Logger.e(TAG, "isAccessibilityServiceEnabled: expectedServiceId = " + expectedServiceId); for (AccessibilityServiceInfo serviceInfo : enabledServices) { - Log.e(TAG, "isAccessibilityServiceEnabled: serviceInfo.getId() = " + serviceInfo.getId()); + Logger.e(TAG, "isAccessibilityServiceEnabled: serviceInfo.getId() = " + serviceInfo.getId()); if (expectedServiceId.equals(serviceInfo.getId())) { return true; } diff --git a/app/src/main/java/com/ttstd/dialer/utils/ApkUtils.java b/app/src/main/java/com/ttstd/dialer/utils/ApkUtils.java index d4f3cce..32c6728 100644 --- a/app/src/main/java/com/ttstd/dialer/utils/ApkUtils.java +++ b/app/src/main/java/com/ttstd/dialer/utils/ApkUtils.java @@ -11,7 +11,6 @@ import android.content.pm.ResolveInfo; import android.graphics.drawable.Drawable; import android.net.Uri; import android.text.TextUtils; -import android.util.Log; import com.hjq.toast.Toaster; import com.ttstd.dialer.R; @@ -40,7 +39,7 @@ public class ApkUtils { pkgContext.startActivity(intent); return true; } - Log.e(TAG, "openPackage: can not open " + packageName); + Logger.e(TAG, "openPackage: can not open " + packageName); Toaster.show("打开失败"); return false; } @@ -96,7 +95,7 @@ public class ApkUtils { context.startActivity(intent); return true; } catch (Exception e) { - Log.e(TAG, "openApp: " + e.getMessage()); + Logger.e(TAG, "openApp: " + e.getMessage()); } return false; } @@ -166,7 +165,7 @@ public class ApkUtils { PackageManager pm = context.getPackageManager(); pi = pm.getPackageInfo(pkgName, 0); } catch (PackageManager.NameNotFoundException e) { - Log.e("isSystemApp: ", "NameNotFoundException:" + e.getMessage()); + Logger.e("isSystemApp: ", "NameNotFoundException:" + e.getMessage()); } // 是系统中已安装的应用 if (pi != null) { @@ -185,7 +184,7 @@ public class ApkUtils { try { context.startActivity(storeIntent); } catch (Exception e1) { - Log.e(TAG, "openWeixin storeIntent: " + e1.getMessage()); + Logger.e(TAG, "openWeixin storeIntent: " + e1.getMessage()); } } } diff --git a/app/src/main/java/com/ttstd/dialer/utils/CameraUtil.java b/app/src/main/java/com/ttstd/dialer/utils/CameraUtil.java new file mode 100644 index 0000000..a6824c9 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/utils/CameraUtil.java @@ -0,0 +1,567 @@ +package com.ttstd.dialer.utils; + +import android.Manifest; +import android.content.Context; +import android.content.pm.PackageManager; +import android.graphics.ImageFormat; +import android.graphics.SurfaceTexture; +import android.hardware.Camera; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CameraManager; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.TotalCaptureResult; +import android.hardware.camera2.params.StreamConfigurationMap; +import android.media.Image; +import android.media.ImageReader; +import android.media.MediaRecorder; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.DisplayMetrics; +import android.util.Size; + +import androidx.annotation.NonNull; +import androidx.core.app.ActivityCompat; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; + +/** + * @作者 Liushihua + * @创建时间 2021-2-3 10:54 + * @描述 + */ +public class CameraUtil { + private static final String TAG = "CameraUtil"; + + private Context context; + private CameraCallBack cameraCallBack; + private CameraManager cameraManager; + // 默认相机id是0 LENS_FACING_FRONT,LENS_FACING_BACK + private int cameraId = CameraCharacteristics.LENS_FACING_FRONT; + private CameraDevice mCameraDevice; + private String savePath; + + private CaptureRequest.Builder mPreviewRequestBuilder; + private CameraCaptureSession mCameraCaptureSession; + private CaptureRequest request; + private ExecutorService service; + private boolean isTakedPicture = false;//是否已经拍照 + + private int needSetOrientation = 0;// 设置默认的拍照方向 + private boolean isInitOk = false;// 是否初始化成功 + private boolean isSessionClosed = true;// captureSession是否被关闭 + private boolean isCameraDoing = false;// 是否正在使用相机 + private final long CAPTURE_DELAY_TIME_LONG = 1200;// 延时拍照——聚焦需要时间 + + private final int HANDLER_ERR = 3;// 拍照失败 + private final int HANDLER_TAKE_PHOTO_SUCCESS = 5;// 拍照成功 + private List enableCameraList;//可用摄像头列表 + private boolean mFlashSupported = false;//是否支持闪光灯 + private List recordSizeList;// 录制尺寸 + private Size mPreviewOutputSize;// 预览尺寸 + private List imgOutputSizes;// 拍照尺寸 + + private final int PREVIEW_TYPE_NORMAL = 0;// 默认预览 + private final int PREVIEW_TYPE_RECORD = 1;// 录屏预览 + private final int PREVIEW_TYPE_TAKE_PHOTO = 2;// 拍照预览 + private int previewType = 0;//默认预览 + + private boolean mTakePictureFinish = false; + + private long lastSaveFileTime = 0; + + public interface CameraCallBack { + void onErr(String msg); + + void onTakePhotoOk(String path); + } + + /** + * 处理静态图片的输出 + */ + private ImageReader imageReader; + + private Handler handler = new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(Message msg) { + super.handleMessage(msg); + switch (msg.what) { + case HANDLER_ERR: + cameraCallBack.onErr("" + msg.obj); + isCameraDoing = false; + break; + case HANDLER_TAKE_PHOTO_SUCCESS: + if (!mTakePictureFinish) { + cameraCallBack.onTakePhotoOk(savePath); + mTakePictureFinish = true; + } + break; + default: + } + } + }; + + /** + * 当相机设备的状态发生改变的时候,将会回调。 + */ + protected final CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() { + /** + * 当相机打开的时候,调用 + * @param cameraDevice + */ + @Override + public void onOpened(@NonNull CameraDevice cameraDevice) { + Logger.e(TAG, "onOpened"); + mCameraDevice = cameraDevice; + startPreview(); + } + + @Override + public void onDisconnected(@NonNull CameraDevice cameraDevice) { + Logger.e(TAG, "onDisconnected"); + cameraDevice.close(); + mCameraDevice = null; + Message message = new Message(); + message.what = HANDLER_ERR; + message.obj = "后台相机断开连接"; + handler.sendMessage(message); + } + + /** + * 发生异常的时候调用 + * + * 这里释放资源,然后关闭界面 + * @param cameraDevice + * @param error + */ + @Override + public void onError(@NonNull CameraDevice cameraDevice, int error) { + Logger.e(TAG, "onError 相机设备异常,请重启!"); + cameraDevice.close(); + mCameraDevice = null; + Message messagef = new Message(); + messagef.what = HANDLER_ERR; + messagef.obj = "相机设备异常,请重启!"; + handler.sendMessage(messagef); + } + + /** + *当相机被关闭的时候 + */ + @Override + public void onClosed(@NonNull CameraDevice camera) { + super.onClosed(camera); + Logger.e(TAG, "onClosed"); + mCameraDevice = null; + isCameraDoing = false; + } + }; + + /** + * 相机状态回调 + */ + private CameraManager.AvailabilityCallback callback = new CameraManager.AvailabilityCallback() { + @Override + public void onCameraAvailable(@NonNull String cameraId) {// 相机可用 + super.onCameraAvailable(cameraId); + Logger.e(TAG, "相机可用"); + } + + @Override + public void onCameraUnavailable(@NonNull String cameraId) {// 相机不可用 + super.onCameraUnavailable(cameraId); + Logger.e(TAG, "相机不可用"); + } + }; + + + /** + * 初始化 + * + * @param activity + * @param cameraCallBack 回调 + */ + public CameraUtil(Context activity, @NonNull CameraCallBack cameraCallBack) { + this.context = activity; + this.cameraCallBack = cameraCallBack; + cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); + // 对于静态图片,使用可用的最大值来拍摄。 + if (cameraManager != null) { + isInitOk = true; + cameraManager.registerAvailabilityCallback(callback, null); + getCameraInfo(); + setupImageReader(); + service = Executors.newSingleThreadExecutor(); + } + } + + private void setupImageReader() { +// Size size = getOutputSize(imgOutputSizes); + //前三个参数分别是需要的尺寸和格式,最后一个参数代表每次最多获取几帧数据,本例的3代表ImageReader中最多可以获取2帧图像流 + imageReader = ImageReader.newInstance(1600, 1200, ImageFormat.JPEG, 1); + //监听ImageReader的事件,当有图像流数据可用时会回调onImageAvailable方法,它的参数就是预览帧数据,可以对这帧数据进行处理 + imageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() { + @Override + public void onImageAvailable(ImageReader reader) { + Image image = reader.acquireLatestImage(); + //我们可以将这帧数据转成字节数组,类似于Camera1的PreviewCallback回调的预览帧数据 + ByteBuffer buffer = image.getPlanes()[0].getBuffer(); + byte[] data = new byte[buffer.remaining()]; + buffer.get(data); + image.close(); + saveFile(data, savePath); + } + }, null); + } + + /** + * 打开相机 + */ + private void openCamera() { + cameraId = getFrontCameraId(); + Logger.e(TAG, "openCamera: getFrontCameraId = " + getFrontCameraId()); + Logger.e(TAG, "openCamera:" + cameraId); + isCameraDoing = true; + // 设置TextureView的缓冲区大小 + // 获取Surface显示预览数据 + if (ActivityCompat.checkSelfPermission(context, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { + Message message = new Message(); + message.what = HANDLER_ERR; + message.obj = "权限不足"; + handler.sendMessage(message); + return; + } + try { + cameraManager.openCamera(Integer.toString(cameraId), stateCallback, null); + Logger.e(TAG, "打开相机成功!"); + } catch (Exception e) { + Logger.e(TAG, "打开相机失败-Exception:" + e.getMessage()); + e.printStackTrace(); + Message messagef = new Message(); + messagef.what = HANDLER_ERR; + messagef.obj = "打开相机失败"; + handler.sendMessage(messagef); + } + } + + private int getFrontCameraId() { + int numberOfCameras = Camera.getNumberOfCameras(); + for (int i = 0; i < numberOfCameras; i++) { + Camera.CameraInfo cameraInfo = new Camera.CameraInfo(); + Camera.getCameraInfo(i, cameraInfo); + if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { + return i; + } + } + return 0; + } + + /** + * 开始视频录制预览 + */ + private void startPreview() { + Logger.e(TAG, "startPreview"); + // CaptureRequest添加imageReaderSurface,不加的话就会导致ImageReader的onImageAvailable()方法不会回调 + // 创建CaptureSession时加上imageReaderSurface,如下,这样预览数据就会同时输出到previewSurface和imageReaderSurface了 + try { + // 创建CaptureRequestBuilder,TEMPLATE_PREVIEW比表示预览请求 + mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD); + mPreviewRequestBuilder.addTarget(imageReader.getSurface());// 设置Surface作为预览数据的显示界面 + // 创建相机捕获会话,第一个参数是捕获数据的输出Surface列表,第二个参数是CameraCaptureSession的状态回调接口,当它创建好后会回调onConfigured方法,第三个参数用来确定Callback在哪个线程执行,为null的话就在当前线程执行 + mCameraDevice.createCaptureSession(Arrays.asList(imageReader.getSurface()), captureSessionStateCallBack, null); + } catch (CameraAccessException e) { + e.printStackTrace(); + Message messagef = new Message(); + messagef.what = HANDLER_ERR; + messagef.obj = "捕获帧失败"; + handler.sendMessage(messagef); + Logger.e(TAG, "Camera获取成功,创建录制请求或捕获Session失败" + e.getMessage()); + } + } + + /** + * 捕获图片数据 + */ + private CameraCaptureSession.StateCallback captureSessionStateCallBack = new CameraCaptureSession.StateCallback() { + @Override + public void onConfigured(CameraCaptureSession session) { + try { + mCameraCaptureSession = session; + isSessionClosed = false; + request = mPreviewRequestBuilder.build(); + // 设置反复捕获数据的请求,这样预览界面就会一直有数据显示 + mCameraCaptureSession.setRepeatingRequest(request, null, null); + } catch (Exception e) { + e.printStackTrace(); + Message messagef = new Message(); + messagef.what = HANDLER_ERR; + messagef.obj = "开启图像预览失败"; + handler.sendMessage(messagef); + } + if (!isTakedPicture) { + isTakedPicture = true; + handler.postDelayed(() -> takePicture(), CAPTURE_DELAY_TIME_LONG); + } + } + + @Override + public void onConfigureFailed(CameraCaptureSession session) { + Logger.e(TAG, "onConfigureFailed"); + } + }; + + + public void startTakePicture(String savePath) { + Logger.e(TAG, "拍照:" + savePath); + this.savePath = savePath; + if (isCameraDoing) { + Logger.e(TAG, "相机使用中..."); + } else { + isTakedPicture = false; + openCamera(); + } + } + + /** + * 拍照 + */ + private void takePicture() { + Logger.e(TAG, "takePicture"); + try { + if (mCameraDevice == null || mPreviewRequestBuilder == null) return; + mPreviewRequestBuilder.addTarget(imageReader.getSurface()); + //设置拍照方向 + mPreviewRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION, this.needSetOrientation); + //这个回调接口用于拍照结束时重启预览,因为拍照会导致预览停止 + CameraCaptureSession.CaptureCallback mImageSavedCallback = new CameraCaptureSession.CaptureCallback() { + @Override + public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) { + Logger.e(TAG, "拍照完成"); + onStop(); + } + }; + //开始拍照,然后回调上面的接口重启预览,因为mCaptureBuilder设置ImageReader作为target,所以会自动回调ImageReader的onImageAvailable()方法保存图片 + mCameraCaptureSession.capture(mPreviewRequestBuilder.build(), mImageSavedCallback, null); + } catch (CameraAccessException e) { + Logger.e(TAG, "takePhoto CameraAccessException:" + e.getMessage()); + e.printStackTrace(); + Message messagef = new Message(); + messagef.what = HANDLER_ERR; + messagef.obj = "拍照失败"; + handler.sendMessage(messagef); + } + } + + /** + * 停止预览,释放资源 + */ + public void stopRecord() { + Logger.e(TAG, "停止预览,释放资源"); + try { + if (mCameraCaptureSession != null && !isSessionClosed) { + mCameraCaptureSession.stopRepeating(); + mCameraCaptureSession.abortCaptures(); + mCameraCaptureSession.close(); + isSessionClosed = true; + imageReader.close(); + imageReader = null; + } + if (mCameraDevice != null) + mCameraDevice.close(); + isCameraDoing = false; + } catch (Exception e) { + e.printStackTrace(); + Logger.e(TAG, "stopRecord-Exception:" + e.getMessage()); + } + } + + /** + * 重置后,开始预览 + */ + public void reset() { + previewType = PREVIEW_TYPE_NORMAL; + stopRecord(); + openCamera(); + } + + /** + * 在 activity,fragment的onStop中调用 + */ + public void onStop() { + stopRecord(); + } + + /** + * 注销 回调 + */ + public void onDestroy() { + this.cameraCallBack = null; + if (cameraManager != null) + cameraManager.unregisterAvailabilityCallback(callback); + } + + /** + * 获得可用的摄像头 + * + * @return SparseArray of available cameras ids。key为摄像头方位,见CameraCharacteristics#LENS_FACING,value为对应的摄像头id + */ + public void getCameras() { + if (cameraManager == null) return; + enableCameraList = new ArrayList<>(); + try { + String[] camerasAvailable = cameraManager.getCameraIdList(); + CameraCharacteristics cam; + Integer characteristic; + Logger.e(TAG, "-------------------------------------"); + for (String id : camerasAvailable) { + Logger.e(TAG, "getCameras:" + id); + try { + enableCameraList.add(Integer.parseInt(id)); + } catch (NumberFormatException e) { + e.printStackTrace(); + } + } + Logger.e(TAG, "-------------------------------------"); + } catch (CameraAccessException e) { + Logger.e(TAG, "getCameras CameraAccessException:" + e.getMessage()); + } + } + + /** + * 设置输出数据尺寸选择器,在selectCamera之前设置有效 + * (一般手机支持多种输出尺寸,请用户根据自身需要选择最合适的一种) + * 举例: + * SizeSelector maxPreview = SizeSelectors.and(SizeSelectors.maxWidth(720), SizeSelectors.maxHeight(480)); + * SizeSelector minPreview = SizeSelectors.and(SizeSelectors.minWidth(320), SizeSelectors.minHeight(240)); + * camera.setmOutputSizeSelector(SizeSelectors.or( + * SizeSelectors.and(maxPreview, minPreview)//先在最大和最小中寻找 + * , SizeSelectors.and(maxPreview, SizeSelectors.biggest())//找不到则按不超过最大尺寸的那个选择 + * )); + */ + public void getOutputSizeSelector() { + + } + + + /** + * 获取摄像头信息 + */ + public void getCameraInfo() { + if (enableCameraList == null) { + getCameras(); + } + try { + CameraCharacteristics mCameraCharacteristics = cameraManager.getCameraCharacteristics(String.valueOf(cameraId)); + // 设置是否支持闪光灯 + Boolean available = mCameraCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE); + mFlashSupported = available == null ? false : available; + StreamConfigurationMap map = mCameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); + if (map == null) { + Logger.e(TAG, "Could not get configuration map."); + return; + } + int mSensorOrientation = mCameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);//这个方法来获取CameraSensor的方向。 + Logger.e(TAG, "camera sensor orientation:" + mSensorOrientation + ",display rotation=" + context.getDisplay().getRotation()); + + int[] formats = map.getOutputFormats();//获得手机支持的输出格式,其中jpeg是一定会支持的,yuv_420_888是api21才开始支持的 + for (int format : formats) { + Logger.e(TAG, "手机格式支持: " + format); + } + Size[] yuvOutputSizes = map.getOutputSizes(ImageFormat.YUV_420_888); + Size[] mediaOutputSizes = map.getOutputSizes(MediaRecorder.class); + Size[] previewOutputSizes = map.getOutputSizes(SurfaceTexture.class); + Size[] jpegOutputSizes = map.getOutputSizes(ImageFormat.JPEG); + + recordSizeList = new ArrayList<>(); + imgOutputSizes = new ArrayList<>(); + + Logger.e(TAG, "---------------------------------------------------"); + for (Size size : mediaOutputSizes) { + recordSizeList.add(new Size(size.getWidth(), size.getHeight())); + Logger.e(TAG, "mediaOutputSizes: " + size.toString()); + } + for (Size size : jpegOutputSizes) { + imgOutputSizes.add(new Size(size.getWidth(), size.getHeight())); + Logger.e(TAG, "jpegOutputSizes: " + size.toString()); + } + for (Size size : previewOutputSizes) { + Logger.e(TAG, "previewOutputSizes: " + size.toString()); + } + for (Size size : yuvOutputSizes) { + Logger.e(TAG, "yuvOutputSizes: " + size.toString()); + } + Logger.e(TAG, "---------------------------------------------------"); + } catch (Exception e) { + Logger.e(TAG, "selectCamera Exception:" + e.getMessage()); + } + } + + private Size getOutputSize(List sizes) { + DisplayMetrics dm = context.getResources().getDisplayMetrics(); + int screenWidth = dm.widthPixels; + int screenHeight = dm.heightPixels; + Logger.e(TAG, "getOutputSize: screenWidth = " + screenWidth); + Logger.e(TAG, "getOutputSize: screenHeight = " + screenHeight); + List sorted = sizes.stream().sorted(new Comparator() { + @Override + public int compare(Size o1, Size o2) { + return Long.compare(o1.getHeight() * o1.getWidth(), o2.getHeight() * o2.getWidth()); + } + }).collect(Collectors.toList()); + for (Size size : sorted) { + if (size.getWidth() > screenHeight && size.getHeight() > screenWidth) { + return size; + } + } + return new Size(1600, 1200); + } + + //覆盖性保存 + private void saveFile(final byte[] data, final String savePath) { + if (data == null || data.length == 0) return; + if (System.currentTimeMillis() - lastSaveFileTime > 1000) + service.execute(() -> { + File file = new File(savePath); + File parent = file.getParentFile(); + if (parent != null && !parent.exists()) { + parent.mkdirs(); + } + if (file.exists()) { + file.delete(); + } + try { + file.createNewFile(); + FileOutputStream fos = new FileOutputStream(file); + fos.write(data); + fos.flush(); + fos.close(); + lastSaveFileTime = System.currentTimeMillis(); + Message message = new Message(); + message.what = HANDLER_TAKE_PHOTO_SUCCESS; + message.obj = savePath; + handler.sendMessage(message); + } catch (IOException e) { + e.printStackTrace(); + Logger.e(TAG, "图片保存-IOException:" + e.getMessage()); + Message messagef = new Message(); + messagef.what = HANDLER_ERR; + messagef.obj = "图片保存失败"; + handler.sendMessage(messagef); + } + }); + } +} + diff --git a/app/src/main/java/com/ttstd/dialer/utils/CmdUtil.java b/app/src/main/java/com/ttstd/dialer/utils/CmdUtil.java new file mode 100644 index 0000000..43433d5 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/utils/CmdUtil.java @@ -0,0 +1,144 @@ +package com.ttstd.dialer.utils; + +import android.text.TextUtils; + +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; + +public class CmdUtil { + private static final String TAG = "CmdUtil"; + + private static final String COMMAND_SH = "sh"; + private static final String COMMAND_EXIT = "exit\n"; + private static final String COMMAND_LINE_END = "\n"; + + + /** + * 运行命令 + * + * @param command 命令 + * @return 结果 + */ + public static Result execute(String command) { + Logger.i(TAG, "execute() command = " + command); + Result result = new Result(); + + if (TextUtils.isEmpty(command)) { + Logger.w(TAG, "WARNING: command should not be null or empty"); + return result; + } + + Process process = null; + DataOutputStream dos = null; + + try { + process = Runtime.getRuntime().exec(COMMAND_SH); + dos = new DataOutputStream(process.getOutputStream()); + dos.write(command.trim().getBytes()); + dos.writeBytes(COMMAND_LINE_END); + dos.flush(); + dos.writeBytes(COMMAND_EXIT); + dos.flush(); + result.code = process.waitFor(); + result.success = readBuffer(new BufferedReader(new InputStreamReader(process.getInputStream()))); + result.error = readBuffer(new BufferedReader(new InputStreamReader(process.getErrorStream()))); + Logger.i(TAG, "result = " + result); + } catch (IOException ioe) { + ioe.printStackTrace(); + Logger.e(TAG, ioe.getMessage()); + } catch (InterruptedException ie) { + ie.printStackTrace(); + Logger.e(TAG, ie.getMessage()); + } finally { + try { + if (null != dos) { + dos.close(); + } + } catch (IOException ioe) { + ioe.printStackTrace(); + Logger.e(TAG, ioe.getMessage()); + } + if (null != process) { + process.destroy(); + } + } + return result; + } + + private static String readBuffer(BufferedReader bufferedReader) throws IOException { + StringBuilder sb = new StringBuilder(); + String s; + while ((s = bufferedReader.readLine()) != null) { + sb.append(s); + } + return sb.toString(); + } + + + /** + * Command执行结果 + */ + public static final class Result { + + public static final int SUCCESS = 0; + public static final int ERROR = -1; + + public int code = ERROR; + public String error; + public String success; + + @Override + public String toString() { + return "Result{" + + "code=" + code + + ", error='" + error + '\'' + + ", success='" + success + '\'' + + '}'; + } + } + + + /** + * 执行 adb 命令 + * + * @param cmd 命令 + * @return + */ + public static StringBuffer shellExec(String cmd) { + Runtime mRuntime = Runtime.getRuntime(); //执行命令的方法 + try { + //Process中封装了返回的结果和执行错误的结果 + Process mProcess = mRuntime.exec(cmd); //加入参数 + //使用BufferReader缓冲各个字符,实现高效读取 + //InputStreamReader将执行命令后得到的字节流数据转化为字符流 + //mProcess.getInputStream()获取命令执行后的的字节流结果 + BufferedReader mReader = new BufferedReader(new InputStreamReader(mProcess.getInputStream())); + //实例化一个字符缓冲区 + StringBuffer mRespBuff = new StringBuffer(); + //实例化并初始化一个大小为1024的字符缓冲区,char类型 + char[] buff = new char[1024]; + int ch = 0; + //read()方法读取内容到buff缓冲区中,大小为buff的大小,返回一个整型值,即内容的长度 + //如果长度不为null + while ((ch = mReader.read(buff)) != -1) { + //就将缓冲区buff的内容填进字符缓冲区 + mRespBuff.append(buff, 0, ch); + } + //结束缓冲 + mReader.close(); + Logger.i("shell", "shellExec: " + mRespBuff); + //弹出结果 +// Logger.i("shell", "执行命令: " + cmd + "执行成功"); + return mRespBuff; + + } catch (IOException e) { + // 异常处理 + // TODO Auto-generated catch block + e.printStackTrace(); + } + return null; + } +} + diff --git a/app/src/main/java/com/ttstd/dialer/utils/FileUtils.java b/app/src/main/java/com/ttstd/dialer/utils/FileUtils.java new file mode 100644 index 0000000..ce475ac --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/utils/FileUtils.java @@ -0,0 +1,24 @@ +package com.ttstd.dialer.utils; + +import android.content.Context; +import android.os.Environment; + +public class FileUtils { + public static String getCacheDir(Context context) { + String cachePath; + if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) + || !Environment.isExternalStorageRemovable()) { + if (context.getExternalCacheDir() != null) { + cachePath = context.getExternalCacheDir().getPath(); + } else if (context.getExternalFilesDir("cache") != null) { + cachePath = context.getExternalFilesDir("cache").getPath(); + } else { + cachePath = context.getCacheDir().getPath(); + } + } else { + cachePath = context.getCacheDir().getPath(); + } + return cachePath; + } + +} diff --git a/app/src/main/java/com/ttstd/dialer/utils/HashUtils.java b/app/src/main/java/com/ttstd/dialer/utils/HashUtils.java new file mode 100644 index 0000000..6904d5f --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/utils/HashUtils.java @@ -0,0 +1,145 @@ +package com.ttstd.dialer.utils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; + +public class HashUtils { + + public static String getFileMd5(File file) { + if (file == null || !file.exists()) { + return null; + } + + MessageDigest digest; + InputStream inputStream = null; + + try { + digest = MessageDigest.getInstance("MD5"); + inputStream = new FileInputStream(file); + + byte[] buffer = new byte[8192]; + int len; + while ((len = inputStream.read(buffer)) != -1) { + digest.update(buffer, 0, len); + } + + return bytesToHex(digest.digest()); + } catch (Exception e) { + e.printStackTrace(); + return null; + } finally { + try { + if (inputStream != null) { + inputStream.close(); + } + } catch (Exception ignored) { + } + } + } + + /** + * 计算字符串的 SHA-256 哈希值 + * + * @param input 需要计算的字符串 + * @return 64位长度的十六进制哈希字符串 + */ + public static String getSHA256String(String input) { + if (input == null || input.isEmpty()) { + return null; + } + try { + // 获取 SHA-256 实例 (原生支持,无需外部依赖) + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + // 将字符串转换为字节数组并进行 Hash 计算 + byte[] hashBytes = digest.digest(input.getBytes(StandardCharsets.UTF_8)); + // 将字节数组转换为十六进制字符串 + return bytesToHex(hashBytes); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + return null; + } + } + + /** + * 计算文件的 SHA-256 哈希值 (常用于文件完整性校验) + * + * @param file 需要计算的文件 + * @return 64位长度的十六进制哈希字符串 + */ + public static String getFileSHA256(File file) { + if (file == null || !file.exists()) { + return null; + } + FileInputStream fis = null; + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + fis = new FileInputStream(file); + byte[] buffer = new byte[8192]; // 8KB 缓冲区 + int length; + // 分块读取文件并更新 Hash + while ((length = fis.read(buffer)) != -1) { + digest.update(buffer, 0, length); + } + return bytesToHex(digest.digest()); + } catch (NoSuchAlgorithmException | IOException e) { + e.printStackTrace(); + return null; + } finally { + if (fis != null) { + try { + fis.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + /** + * 将字节数组转换为小写的十六进制字符串 + */ + private static String bytesToHex(byte[] bytes) { + StringBuilder hexString = new StringBuilder(2 * bytes.length); + for (byte b : bytes) { + // 将 byte 转换为无符号整数并格式化为两位的十六进制数 + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + return hexString.toString(); + } + + + /** + * ⚠️ 特别提示:如果是为了“加密密码” + * 如果你的目的是在本地或传输前对用户密码进行 Hash,单纯的 SHA-256 是不够安全的(容易被彩虹表破解)。 + * 对于密码存储,建议使用加盐的迭代算法。 + * 在低版本 Android 上兼容性最好的原生方案是 PBKDF2WithHmacSHA1: + * + * @param password + * @param salt + * @return + */ + public static String hashPasswordPBKDF2(String password, byte[] salt) { + try { + // 迭代 10000 次,生成 256 位的密钥。PBKDF2WithHmacSHA1 兼容 API 10+ + PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 10000, 256); + SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + byte[] hash = skf.generateSecret(spec).getEncoded(); + return bytesToHex(hash); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ttstd/dialer/utils/Logger.java b/app/src/main/java/com/ttstd/dialer/utils/Logger.java index 948db68..9830cea 100644 --- a/app/src/main/java/com/ttstd/dialer/utils/Logger.java +++ b/app/src/main/java/com/ttstd/dialer/utils/Logger.java @@ -9,6 +9,8 @@ import com.ttstd.dialer.BuildConfig; import java.io.File; import java.io.FileWriter; import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; @@ -16,113 +18,228 @@ import java.util.Locale; public class Logger { // 日志级别枚举 public enum LogLevel { - DEBUG, INFO, WARN, ERROR + VERBOSE, DEBUG, INFO, WARN, ERROR } private static final String TAG = "AppLogger"; private static final String LOG_FILE_NAME = "app_log.txt"; private static File logFile; private static LogLevel logLevel = LogLevel.DEBUG; // 默认日志级别 - private static boolean isDebugMode = BuildConfig.DEBUG; // 默认关闭Debug输出 + private static boolean isDebugMode = BuildConfig.DEBUG; // Debug模式开关 // 初始化日志系统(需在应用启动时调用) public static void initialize(Context context, boolean debugMode) { + if (context == null) { + Log.e(TAG, "Logger initialize failed: context is null"); + return; + } isDebugMode = debugMode; try { File storageDir = context.getExternalFilesDir("log"); - logFile = new File(storageDir, LOG_FILE_NAME); + if (storageDir != null) { + // 确保日志目录存在 + if (!storageDir.exists()) { + boolean mkdirSuccess = storageDir.mkdirs(); + if (!mkdirSuccess) { + Log.e(TAG, "Create log directory failed"); + } + } + logFile = new File(storageDir, LOG_FILE_NAME); + } else { + Log.e(TAG, "Get external files dir failed"); + } } catch (Exception e) { - Log.e(TAG, "Log file init failed: " + e.getMessage()); + Log.e(TAG, "Log file init failed: " + e.getMessage(), e); } } // 设置全局日志级别 public static void setLogLevel(LogLevel level) { - logLevel = level; + if (level != null) { + logLevel = level; + } } - // 核心日志方法 - private static void log(LogLevel level, Object context, String name, String message) { - if (level.ordinal() < logLevel.ordinal()) return; // 低于设定级别不记录 - String tag; - if (TextUtils.isEmpty(context.getClass().getSimpleName())) { - tag = context.toString(); - } else { - tag = context.getClass().getSimpleName(); + // 核心日志方法(支持异常) + private static void log(LogLevel level, Object contextObj, String name, String message, Throwable throwable) { + // 级别过滤 + if (level == null || level.ordinal() < logLevel.ordinal()) { + return; } - String logMsg = formatLog(level, tag, name, message); - writeToFile(logMsg); // 始终写入文件 + // 处理Context TAG + String tag = getTagFromContext(contextObj); + // 格式化日志内容 + String logMsg = formatLog(level, tag, name, message, throwable); + + // 写入文件 + writeToFile(logMsg); + + // Debug模式输出到控制台 if (isDebugMode) { - outputToConsole(level, tag, name, message); // 仅Debug模式输出到控制台 + outputToConsole(level, tag, name, message, throwable); } } - // 格式化日志(含时间戳、线程信息) - private static String formatLog(LogLevel level, String tag, String name, String message) { + // 从Context对象获取TAG + private static String getTagFromContext(Object contextObj) { + if (contextObj == null) { + return TAG; + } + if (contextObj instanceof Class) { + return ((Class) contextObj).getSimpleName(); + } else if (contextObj instanceof String) { + return (String) contextObj; + } else { + String simpleName = contextObj.getClass().getSimpleName(); + return TextUtils.isEmpty(simpleName) ? contextObj.toString() : simpleName; + } + } + + // 格式化日志(含时间戳、线程信息、异常堆栈) + private static String formatLog(LogLevel level, String tag, String name, String message, Throwable throwable) { String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()).format(new Date()); String threadName = Thread.currentThread().getName(); - return String.format("%s [%s] %s/%s: %s %s\n", time, threadName, level.name(), tag, name, message); + + // 基础日志内容 + StringBuilder logBuilder = new StringBuilder(); + logBuilder.append(String.format("%s [%s] %s/%s: %s %s", + time, threadName, level.name(), tag, + TextUtils.isEmpty(name) ? "" : name, + TextUtils.isEmpty(message) ? "" : message)); + + // 追加异常堆栈 + if (throwable != null) { + logBuilder.append("\n").append(getStackTraceString(throwable)); + } + + logBuilder.append("\n"); + return logBuilder.toString(); + } + + // 将异常堆栈转为字符串 + private static String getStackTraceString(Throwable throwable) { + if (throwable == null) { + return ""; + } + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + throwable.printStackTrace(pw); + pw.flush(); + return sw.toString(); } // 写入日志文件 private static void writeToFile(String logMsg) { - if (logFile == null) return; + if (logFile == null || TextUtils.isEmpty(logMsg)) { + return; + } try (FileWriter writer = new FileWriter(logFile, true)) { writer.append(logMsg); } catch (IOException e) { - Log.e(TAG, "File write failed: " + e.getMessage()); + Log.e(TAG, "File write failed: " + e.getMessage(), e); } } // 输出到Android控制台(Logcat) - private static void outputToConsole(LogLevel level, String tag, String name, String message) { + private static void outputToConsole(LogLevel level, String tag, String name, String message, Throwable throwable) { + String logContent = TextUtils.isEmpty(name) ? message : (name + " : " + (message == null ? "" : message)); + switch (level) { + case VERBOSE: + Log.v(tag, logContent, throwable); + break; case DEBUG: - Log.d(tag, name + " : " + message); + Log.d(tag, logContent, throwable); break; case INFO: - Log.i(tag, name + " : " + message); + Log.i(tag, logContent, throwable); break; case WARN: - Log.w(tag, name + " : " + message); + Log.w(tag, logContent, throwable); break; case ERROR: - Log.e(tag, name + " : " + message); + Log.e(tag, logContent, throwable); + break; + default: + Log.d(tag, logContent, throwable); break; } } - // 快捷调用方法(自动使用类名作为TAG) + // ===================== 基础快捷方法 ===================== + public static void v(Object context, String name, String message) { + log(LogLevel.VERBOSE, context, name, message, null); + } + public static void d(Object context, String name, String message) { - log(LogLevel.DEBUG, context.getClass().getSimpleName(), name, message); + log(LogLevel.DEBUG, context, name, message, null); } public static void i(Object context, String name, String message) { - log(LogLevel.INFO, context.getClass().getSimpleName(), name, message); + log(LogLevel.INFO, context, name, message, null); } public static void w(Object context, String name, String message) { - log(LogLevel.WARN, context.getClass().getSimpleName(), name, message); + log(LogLevel.WARN, context, name, message, null); } public static void e(Object context, String name, String message) { - log(LogLevel.ERROR, context, name, message); + log(LogLevel.ERROR, context, name, message, null); + } + + // ===================== 支持异常的快捷方法 ===================== + public static void e(Object context, String name, String message, Throwable throwable) { + log(LogLevel.ERROR, context, name, message, throwable); + } + + public static void w(Object context, String name, String message, Throwable throwable) { + log(LogLevel.WARN, context, name, message, throwable); + } + + // ===================== 简化参数的快捷方法 ===================== + public static void v(Object context, String name) { + log(LogLevel.VERBOSE, context, name, "", null); } public static void d(Object context, String name) { - log(LogLevel.DEBUG, context.getClass().getSimpleName(), name, ""); + log(LogLevel.DEBUG, context, name, "", null); } public static void i(Object context, String name) { - log(LogLevel.INFO, context.getClass().getSimpleName(), name, ""); + log(LogLevel.INFO, context, name, "", null); } public static void w(Object context, String name) { - log(LogLevel.WARN, context.getClass().getSimpleName(), name, ""); + log(LogLevel.WARN, context, name, "", null); } public static void e(Object context, String name) { - log(LogLevel.ERROR, context.getClass().getSimpleName(), name, ""); + log(LogLevel.ERROR, context, name, "", null); } -} \ No newline at end of file + + // ===================== 兼容原生Log调用的静态方法 ===================== + public static void v(String tag, String msg) { + if (isDebugMode) Log.v(tag, msg); + } + + public static void d(String tag, String msg) { + if (isDebugMode) Log.d(tag, msg); + } + + public static void i(String tag, String msg) { + if (isDebugMode) Log.i(tag, msg); + } + + public static void w(String tag, String msg) { + if (isDebugMode) Log.w(tag, msg); + } + + public static void e(String tag, String msg) { + if (isDebugMode) Log.e(tag, msg); + } + + public static void e(String tag, String msg, Throwable tr) { + if (isDebugMode) Log.e(tag, msg, tr); + } +} diff --git a/app/src/main/java/com/ttstd/dialer/utils/NativeUtils.java b/app/src/main/java/com/ttstd/dialer/utils/NativeUtils.java index 7bf4a85..7377eb0 100644 --- a/app/src/main/java/com/ttstd/dialer/utils/NativeUtils.java +++ b/app/src/main/java/com/ttstd/dialer/utils/NativeUtils.java @@ -11,7 +11,7 @@ public class NativeUtils { * * @return */ - public static native String getKey(); + public static native String getHashKey(); /** * 获取 获取绑定状态的code @@ -20,6 +20,21 @@ public class NativeUtils { */ public static native String getBindStatuSigCode(); + + /** + * 获取app接口请求安全密钥 + * + * @return + */ + public static native String getAppSecureSecretKey(); + + /** + * 获取随机字符串 + * + * @return + */ + public static native String getNonce(); + /** * 和风天气API Host * diff --git a/app/src/main/java/com/ttstd/dialer/utils/RebootUtils.java b/app/src/main/java/com/ttstd/dialer/utils/RebootUtils.java new file mode 100644 index 0000000..20a8479 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/utils/RebootUtils.java @@ -0,0 +1,165 @@ +package com.ttstd.dialer.utils; + +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.PowerManager; +import android.util.Log; + +import androidx.annotation.RequiresApi; + +import java.io.DataOutputStream; +import java.lang.reflect.Method; + +public class RebootUtils { + + /** + * 重启设备(兼容高低版本) + * + * @param context 上下文 + * @param reason 重启原因(可为空) + */ + public static void rebootDevice(Context context, String reason) { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + // Android 9.0+ 使用PowerManager.reboot() + rebootWithPowerManager(context, reason); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + // Android 6.0+ 尝试多种方式 + rebootWithMultipleMethods(context); + } else { + // 低版本使用传统方式 + rebootLegacy(context); + } + } catch (Exception e) { + Log.e("RebootUtils", "重启失败: " + e.getMessage()); + // 尝试最终的回退方案 + rebootFallback(); + } + } + + /** + * Android 9.0+ 推荐方式 + */ + @RequiresApi(api = Build.VERSION_CODES.P) + private static void rebootWithPowerManager(Context context, String reason) { + PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + if (powerManager != null) { + powerManager.reboot(reason); + } + } + + /** + * Android 6.0+ 多方法尝试 + */ + private static void rebootWithMultipleMethods(Context context) { + // 方法1: 使用Intent.ACTION_REBOOT + try { + Intent rebootIntent = new Intent(Intent.ACTION_REBOOT); + rebootIntent.putExtra("nowait", 1); + rebootIntent.putExtra("interval", 1); + rebootIntent.putExtra("window", 0); + context.sendBroadcast(rebootIntent); + } catch (SecurityException e) { + // 方法2: 使用su命令(需要root权限) + rebootWithSuCommand(); + } + } + + /** + * 低版本传统方式 + */ + private static void rebootLegacy(Context context) { + try { + // 尝试发送重启广播 + Intent intent = new Intent("android.intent.action.REBOOT"); + intent.putExtra("nowait", 1); + context.sendBroadcast(intent); + } catch (Exception e) { + rebootWithSuCommand(); + } + } + + /** + * 使用su命令重启(需要root权限) + */ + private static void rebootWithSuCommand() { + try { + Process process = Runtime.getRuntime().exec(new String[]{"su", "-c", "reboot"}); + process.waitFor(); + } catch (Exception e) { + Log.e("RebootUtils", "su命令执行失败: " + e.getMessage()); + } + } + + /** + * 最终回退方案 + */ + private static void rebootFallback() { + try { + // 尝试直接执行reboot命令 + Runtime.getRuntime().exec("reboot"); + } catch (Exception e) { + Log.e("RebootUtils", "所有重启方法都失败了"); + } + } + + + public static void shutdownDevice(Context context) { + try { + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + if (pm != null) { + Method method = pm.getClass().getMethod("shutdown", + boolean.class, String.class, boolean.class); + method.invoke(pm, false, null, false); + } + } catch (Exception e) { + e.printStackTrace(); + // 备用方案 + fallbackShutdown(context); + } + } + + private static void fallbackShutdown(Context context) { + try { + String action; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // Android 8.1+ 使用新的action + action = "com.android.internal.intent.action.REQUEST_SHUTDOWN"; + } else { + // Android 8.1以下使用旧的action + action = "android.intent.action.ACTION_REQUEST_SHUTDOWN"; + } + + Intent shutdown = new Intent(action); + shutdown.putExtra("android.intent.extra.KEY_CONFIRM", false); + shutdown.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(shutdown); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private static boolean shutdownWithRoot() { + try { + Process process = Runtime.getRuntime().exec("su"); + DataOutputStream os = new DataOutputStream(process.getOutputStream()); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + // Android 5.0+ + os.writeBytes("svc power shutdown\n"); + } else { + // 旧版本 + os.writeBytes("reboot -p\n"); + } + + os.writeBytes("exit\n"); + os.flush(); + int exitCode = process.waitFor(); + return exitCode == 0; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ttstd/dialer/utils/ScreenUtils.java b/app/src/main/java/com/ttstd/dialer/utils/ScreenUtils.java index dba8a28..13d43c4 100644 --- a/app/src/main/java/com/ttstd/dialer/utils/ScreenUtils.java +++ b/app/src/main/java/com/ttstd/dialer/utils/ScreenUtils.java @@ -3,7 +3,6 @@ package com.ttstd.dialer.utils; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; -import android.util.Log; import java.lang.reflect.Method; @@ -47,19 +46,19 @@ public class ScreenUtils { Method getMethod = systemPropertiesClass.getDeclaredMethod("get", String.class); // 3. 调用方法获取属性值 String characteristics = (String) getMethod.invoke(null, "ro.build.characteristics"); - Log.e(TAG, "isTablet: " + characteristics); + Logger.e(TAG, "isTablet: " + characteristics); // 4. 判断是否包含 "tablet" 标识 return characteristics != null && characteristics.contains("tablet"); } catch (Exception e) { // 反射失败时的处理(如属性不存在或权限问题) - Log.e(TAG, "Reflection failed: " + e.getMessage()); + Logger.e(TAG, "Reflection failed: " + e.getMessage()); return false; } } public static boolean isTablet(Context context) { boolean isTablet = (context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_LARGE; - Log.e(TAG, "isTablet: " + isTablet); + Logger.e(TAG, "isTablet: " + isTablet); return isTablet; } diff --git a/app/src/main/java/com/ttstd/dialer/utils/SystemUtils.java b/app/src/main/java/com/ttstd/dialer/utils/SystemUtils.java index dae14a0..27d549a 100644 --- a/app/src/main/java/com/ttstd/dialer/utils/SystemUtils.java +++ b/app/src/main/java/com/ttstd/dialer/utils/SystemUtils.java @@ -5,7 +5,9 @@ import android.app.ActivityManager; import android.app.ActivityManagerNative; import android.app.ActivityTaskManager; import android.app.role.RoleManager; +import android.bluetooth.BluetoothAdapter; import android.content.ComponentName; +import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -14,61 +16,55 @@ import android.content.pm.ResolveInfo; import android.content.pm.UserInfo; import android.graphics.Bitmap; import android.graphics.Rect; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; import android.os.Build; +import android.os.Environment; import android.os.IBinder; +import android.os.Process; import android.os.RemoteException; import android.os.UserHandle; +import android.provider.MediaStore; +import android.provider.Settings; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.DisplayMetrics; import android.util.Log; -import android.os.Process; import android.view.SurfaceControl; import android.view.WindowManager; import com.ttstd.dialer.BuildConfig; +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.io.OutputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Method; +import java.net.NetworkInterface; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Optional; +import java.util.concurrent.Callable; import java.util.concurrent.Executor; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; + import static android.app.ActivityManager.RECENT_IGNORE_UNAVAILABLE; public class SystemUtils { private static final String TAG = "SystemUtils"; - /** - * 获取设备序列号 - * - * @return - */ - @SuppressLint("MissingPermission") - public static String getSerial() { - String serial = "unknow"; - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {//9.0+ - serial = Build.getSerial(); - } else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {//8.0+ - serial = Build.SERIAL; - } else {//8.0- - Class c = Class.forName("android.os.SystemProperties"); - Method get = c.getMethod("get", String.class); - serial = (String) get.invoke(c, "ro.serialno"); - } - } catch (Exception e) { - e.printStackTrace(); - Log.e("e", "读取设备序列号异常:" + e.toString()); - } - return serial; - } - /** * 获取系统配置信息 * @@ -81,7 +77,7 @@ public class SystemUtils { try { Class c = Class.forName("android.os.SystemProperties"); Method get = c.getMethod("get", String.class, String.class); - value = (String) (get.invoke(c, key, "unknown")); + value = (String) (get.invoke(c, key, Build.UNKNOWN)); } catch (Exception e) { e.printStackTrace(); } finally { @@ -89,6 +85,251 @@ public class SystemUtils { } } + /** + * 获取设备序列号 + * + * @return + */ + @SuppressLint("MissingPermission") + public static String getSerial() { + String serial = Build.UNKNOWN; + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {//9.0+ + serial = Build.getSerial(); + } else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {//8.0+ + serial = Build.SERIAL; + } else {//8.0- +// Class c = Class.forName("android.os.SystemProperties"); +// Method get = c.getMethod("get", String.class); +// serial = (String) get.invoke(c, "ro.serialno"); + getProperty("ro.serialno", serial); + } + } catch (Exception e) { + e.printStackTrace(); + Logger.e("e", "读取设备序列号异常:" + e.toString()); + } + return serial; + } + + /** + * 获取卡槽 0 (主卡) 的 IMEI + * + * @param context 上下文 + * @return IMEI 字符串,如果获取失败则返回空字符串 + */ + @SuppressLint("MissingPermission") // 系统应用已通过 manifest 赋予权限,此处屏蔽 Lint 警告 + public static String getDefaultImei(Context context) { + return getImei(context, 0); + } + + /** + * 根据卡槽 ID 获取对应的 IMEI + * 该方法适用于 Android 6.0 (API 23) 及以上的所有版本。 + * 在 Android 10+ 上,只有系统应用能成功返回数据。 + * + * @param context 上下文 + * @param slotIndex 卡槽索引 (0 代表卡槽1, 1 代表卡槽2) + * @return IMEI 字符串,如果获取失败则返回空字符串 + */ + @SuppressLint("MissingPermission") + public static String getImei(Context context, int slotIndex) { + String imei = ""; + try { + TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + if (tm != null) { + // 优先使用标准的 getImei(int slotIndex) 方法 (API 23+) + // 对于低版本系统,可以使用反射调用,但现代系统推荐直接调用 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + imei = tm.getImei(slotIndex); + } else { + // 针对极少数的 Android 6.0 以下的老旧系统兜底 + imei = tm.getDeviceId(slotIndex); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + + return imei == null ? "" : imei; + } + + /** + * 获取所有卡槽的 IMEI 数组 + * + * @param context 上下文 + * @return 包含各个卡槽 IMEI 的字符串数组 + */ + @SuppressLint("MissingPermission") + public static String[] getAllImeis(Context context) { + String[] imeis = new String[2]; // 假设最多两个卡槽 + imeis[0] = getImei(context, 0); + imeis[1] = getImei(context, 1); + return imeis; + } + + public static String getDefaultImSi(Context context) { + return getImsiBySubId(context, 0); + } + + public static String getImsiBySubId(Context context, int count) { + SubscriptionManager subscriptionManager = SubscriptionManager.from(context); + SubscriptionInfo subInfo = subscriptionManager.getActiveSubscriptionInfoForSimSlotIndex(count); + if (subInfo != null) { + TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + int subId = subInfo.getSubscriptionId(); + TelephonyManager tm = telephonyManager.createForSubscriptionId(subId); //关键点,拿到对应subId的tm + String mImsi = tm.getSubscriberId(subId); //imsi?not subid? + Log.e(TAG, "getSubInfo: mImsi = " + mImsi); + return mImsi; + } else { + return Build.UNKNOWN; + } + } + + /** + * 获取MAC地址 + * + * @param context + * @return + */ + public static String getWlanMacAddress(Context context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return getMacDefault(context); + } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + return getMacAddressM(); + } else { + return getMacFromHardware(); + } + } + + /** + * Android 6.0 之前(不包括6.0) + * + * @param context + * @return + */ + private static String getMacDefault(Context context) { + String mac = "未获取到设备Mac地址"; + if (context == null) { + return mac; + } + WifiManager wifi = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + WifiInfo info = null; + try { + info = wifi.getConnectionInfo(); + } catch (Exception e) { + e.printStackTrace(); + } + if (info == null) { + return mac; + } + mac = info.getMacAddress(); + if (!TextUtils.isEmpty(mac)) { + mac = mac.toUpperCase(Locale.ENGLISH); + return mac; + } else { + return "02:00:00:00:00:00"; + } + } + + /** + * Android 6.0(包括) - Android 7.0(不包括) + * + * @return + */ + private static String getMacAddressM() { + String mac = "未获取到设备Mac地址"; + + try { + mac = new BufferedReader(new FileReader("/sys/class/net/wlan0/address")).readLine(); + } catch (IOException e) { + e.printStackTrace(); + } + return mac; + } + + /** + * 遍历循环所有的网络接口,找到接口是 wlan0 + * + * @return + */ + private static String getMacFromHardware() { + try { + List all = Collections.list(NetworkInterface.getNetworkInterfaces()); + + for (NetworkInterface nif : all) { + if (!nif.getName().equalsIgnoreCase("wlan0")) { + continue; + } + byte[] macBytes = nif.getHardwareAddress(); + StringBuilder res1 = new StringBuilder(); + for (byte b : macBytes) { + res1.append(String.format("%02X:", b)); + } + if (res1 != null) { + res1.deleteCharAt(res1.length() - 1); + } + return res1.toString(); + } + } catch (Exception e) { + e.printStackTrace(); + } + return "未获取到设备Mac地址"; + } + + public static String getFactoryMacAddresses(Context context) { + WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + String[] factoryMacAddresses = wifiManager.getFactoryMacAddresses(); + String str = (factoryMacAddresses == null || factoryMacAddresses.length <= 0) ? Build.UNKNOWN : factoryMacAddresses[0]; + return str.toUpperCase(Locale.ENGLISH); + } + + public static String getBluetoothMacAddress(Context context) { + String mac = Settings.Secure.getString( + context.getContentResolver(), + "bluetooth_address" + ); + return mac; + } + + @Deprecated + public static String getBtAddressByReflection() { + String def = "02:00:00:00:00:00"; + BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + if (bluetoothAdapter == null) { + return def; + } + return bluetoothAdapter.getAddress(); + +// Field field; +// try { +// field = BluetoothAdapter.class.getDeclaredField("mService"); +// field.setAccessible(true); +// Object bluetoothManagerService = field.get(bluetoothAdapter); +// if (bluetoothManagerService == null) { +// return def; +// } +// Method method = bluetoothManagerService.getClass().getMethod("getAddress"); +// if (method != null) { +// Object obj = method.invoke(bluetoothManagerService); +// if (obj != null) { +// return obj.toString(); +// } +// } +// } catch (NoSuchFieldException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { +// e.printStackTrace(); +// } +// return def; + } + + public static String getBuildId() { + return getProperty(Build.ID, Build.UNKNOWN); + } + + public static String getBuildDisplayId() { + return getProperty(Build.DISPLAY, Build.UNKNOWN); + } + public static boolean isMainProcessName(Context cxt, int pid) { String packageName = cxt.getPackageName(); ActivityManager am = (ActivityManager) cxt.getSystemService(Context.ACTIVITY_SERVICE); @@ -147,7 +388,7 @@ public class SystemUtils { } public static void killBackgroundProcesses(Context context, String processName) { - Log.e(TAG, "killBackgroundProcesses: " + processName); + Logger.e(TAG, "killBackgroundProcesses: " + processName); ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); String packageName; try { @@ -170,7 +411,7 @@ public class SystemUtils { } public static void removeTask(Context context, String packageName) { - Log.e(TAG, "removeTask: " + packageName); + Logger.e(TAG, "removeTask: " + packageName); // if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { List list = getRecentTasks(ActivityManager.getMaxRecentTasksStatic(), getCurrentUserId()); HashMap taskMap = new HashMap<>(); @@ -181,9 +422,9 @@ public class SystemUtils { ActivityManagerNative.getDefault().removeTask(taskMap.get(packageName)); } catch (RemoteException e) { e.printStackTrace(); - Log.e(TAG, "removeTask: " + e.getMessage()); + Logger.e(TAG, "removeTask: " + e.getMessage()); } catch (NullPointerException e) { - Log.e(TAG, "removeTask: " + e.getMessage()); + Logger.e(TAG, "removeTask: " + e.getMessage()); } // } else { // ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); @@ -200,7 +441,7 @@ public class SystemUtils { return ActivityTaskManager.getService().getRecentTasks(numTasks, RECENT_IGNORE_UNAVAILABLE, userId).getList(); } catch (RemoteException e) { - Log.e(TAG, "Failed to get recent tasks " + e); + Logger.e(TAG, "Failed to get recent tasks " + e); return new ArrayList<>(); } } @@ -221,13 +462,13 @@ public class SystemUtils { public static boolean isDefaultLauncher(Context context, Class launcherActivityClass) { ComponentName componentName = new ComponentName(context, launcherActivityClass); - Log.e(TAG, "isDefaultLauncher: componentName = " + componentName); + Logger.e(TAG, "isDefaultLauncher: componentName = " + componentName); Intent intent = new Intent(Intent.ACTION_MAIN); intent.addCategory(Intent.CATEGORY_HOME); ResolveInfo resolveInfo = context.getPackageManager().resolveActivity(intent, 0); if (resolveInfo != null && resolveInfo.activityInfo != null) { ComponentName defaultComponentName = resolveInfo.getComponentInfo().getComponentName(); - Log.e(TAG, "isDefaultLauncher: defaultComponentName = " + defaultComponentName); + Logger.e(TAG, "isDefaultLauncher: defaultComponentName = " + defaultComponentName); return componentName.equals(defaultComponentName); } return false; @@ -269,7 +510,7 @@ public class SystemUtils { try { // 注意:API 29 (Android 10) 中此方法对第三方应用废弃,但对系统签名应用依然有效 pm.replacePreferredActivity(filter, bestMatch, set, myLauncher); - Log.i("LauncherHelper", "成功设置为默认桌面"); + Logger.i("LauncherHelper", "成功设置为默认桌面"); // 可选:发送一个回到桌面的 Intent 来验证 // Intent startHome = new Intent(Intent.ACTION_MAIN); @@ -278,9 +519,9 @@ public class SystemUtils { // context.startActivity(startHome); } catch (SecurityException e) { - Log.e("LauncherHelper", "缺少权限或未生效,请检查系统签名是否正确", e); + Logger.e("LauncherHelper", "缺少权限或未生效,请检查系统签名是否正确", e); } catch (Exception e) { - Log.e("LauncherHelper", "设置默认桌面失败", e); + Logger.e("LauncherHelper", "设置默认桌面失败", e); } } @@ -314,7 +555,7 @@ public class SystemUtils { */ public static void addRoleHolderAsUser(Context context, String packageName) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - Log.e(TAG, "RoleManager requires Android 10 or higher."); + Logger.e(TAG, "RoleManager requires Android 10 or higher."); return; } @@ -326,7 +567,7 @@ public class SystemUtils { // 1. 检查是否已经是当前角色持有者,避免重复调用 if (roleManager.isRoleHeld(roleName)) { // 注意:这里最好再判断一下持有的包名是否为目标包名 - Log.i(TAG, "Role " + roleName + " is already held by some package."); + Logger.i(TAG, "Role " + roleName + " is already held by some package."); // 如果已经是自己,直接返回 } @@ -337,9 +578,9 @@ public class SystemUtils { // 定义回调 Consumer callback = successful -> { if (successful) { - Log.i(TAG, "Successfully set " + packageName + " as " + roleName); + Logger.i(TAG, "Successfully set " + packageName + " as " + roleName); } else { - Log.e(TAG, "Failed to set " + packageName + " as " + roleName + ". Check system logs."); + Logger.e(TAG, "Failed to set " + packageName + " as " + roleName + ". Check system logs."); } }; @@ -348,15 +589,15 @@ public class SystemUtils { Method method = RoleManager.class.getMethod("addRoleHolderAsUser", String.class, String.class, int.class, UserHandle.class, Executor.class, Consumer.class); - Log.d(TAG, "Invoking addRoleHolderAsUser for package: " + packageName); + Logger.d(TAG, "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, "Method not found. Is this a non-standard ROM?", e); + Logger.e(TAG, "Method not found. Is this a non-standard ROM?", e); } catch (Exception e) { - Log.e(TAG, "Error invoking addRoleHolderAsUser", e); + Logger.e(TAG, "Error invoking addRoleHolderAsUser", e); } } @@ -398,8 +639,8 @@ public class SystemUtils { int screenWidth = metrics.widthPixels; int screenHeight = metrics.heightPixels; - Log.e(TAG, "takeFullScreenshot: screenWidth " + screenWidth); - Log.e(TAG, "takeFullScreenshot: screenHeight " + screenHeight); + Logger.e(TAG, "takeFullScreenshot: screenWidth " + screenWidth); + Logger.e(TAG, "takeFullScreenshot: screenHeight " + screenHeight); try { // 注意:不同 Android 版本的 SurfaceControl.screenshot 方法签名可能不同 @@ -425,6 +666,7 @@ public class SystemUtils { return null; } } + public static Bitmap takeScreenshotHighVersion() { try { // 1. 获取主屏幕的 Token (Internal Display) @@ -465,4 +707,43 @@ public class SystemUtils { return null; } + /** + * 保存Bitmap到公共图库(Android 10+ 无需存储权限) + * + * @param context 上下文 + * @param bitmap 截图Bitmap + * @return 保存成功返回true + */ + public static boolean saveScreenshotToGallery(Context context, Bitmap bitmap) { + if (bitmap == null) return false; + + // 使用MediaStore保存到Pictures目录(Android 10+ 推荐) + ContentValues values = new android.content.ContentValues(); + values.put(MediaStore.Images.Media.MIME_TYPE, "image/png"); + values.put(MediaStore.Images.Media.DISPLAY_NAME, "Screenshot_" + System.currentTimeMillis() + ".png"); + values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/SystemScreenshots"); + + android.net.Uri uri = context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); + if (uri == null) return false; + + try (OutputStream outputStream = context.getContentResolver().openOutputStream(uri)) { + bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream); + return true; + } catch (java.io.IOException e) { + e.printStackTrace(); + return false; + } + } + + public static void screenshotCmd(String filepath, Observer observer) { + Observable.fromCallable(new Callable() { + @Override + public Integer call() throws Exception { + CmdUtil.Result result = CmdUtil.execute("screencap -p " + filepath); + Logger.e(TAG, "call: " + result.error); + return result.code; + } + }).subscribe(observer); + } + } diff --git a/app/src/main/java/com/ttstd/dialer/view/ToggleButton.java b/app/src/main/java/com/ttstd/dialer/view/ToggleButton.java index 64a1526..6ed2085 100644 --- a/app/src/main/java/com/ttstd/dialer/view/ToggleButton.java +++ b/app/src/main/java/com/ttstd/dialer/view/ToggleButton.java @@ -8,7 +8,6 @@ import android.graphics.Color; import android.graphics.Paint; import android.graphics.RectF; import android.util.AttributeSet; -import android.util.Log; import android.util.TypedValue; import android.view.View; @@ -18,6 +17,7 @@ import com.facebook.rebound.SpringConfig; import com.facebook.rebound.SpringSystem; import com.facebook.rebound.SpringUtil; import com.ttstd.dialer.R; +import com.ttstd.dialer.utils.Logger; public class ToggleButton extends View { private static final String TAG = "ToggleButton"; @@ -172,7 +172,7 @@ public class ToggleButton extends View { public void toggle(boolean animate) { toggleOn = !toggleOn; - Log.e(TAG, "toggle: toggleOn = " + toggleOn); + Logger.e(TAG, "toggle: toggleOn = " + toggleOn); takeEffect(animate); if (listener != null) { listener.onToggle(toggleOn); @@ -239,12 +239,12 @@ public class ToggleButton extends View { public int getToggleOnStatu() { - Log.e(TAG, "getToggleOnStatu: " + toggleOn); + Logger.e(TAG, "getToggleOnStatu: " + toggleOn); return toggleOn ? 1 : 0; } public boolean isToggleOn() { - Log.e(TAG, "isToggleOn: " + toggleOn); + Logger.e(TAG, "isToggleOn: " + toggleOn); return toggleOn; } diff --git a/app/src/main/jni/ttstd.cpp b/app/src/main/jni/ttstd.cpp index 76e931f..ee81bf4 100644 --- a/app/src/main/jni/ttstd.cpp +++ b/app/src/main/jni/ttstd.cpp @@ -3,13 +3,14 @@ #include // 日志打印 #include +#include #define LOG_TAG "TAG_LOG" #define LOGI(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) extern "C" JNIEXPORT jstring JNICALL -Java_com_ttstd_dialer_utils_NativeUtils_getKey(JNIEnv *env, jclass clazz) { +Java_com_ttstd_dialer_utils_NativeUtils_getHashKey(JNIEnv *env, jclass clazz) { std::string key = "Ls0HSh9fNfeZ8Qu5"; return env->NewStringUTF(key.c_str()); } @@ -47,4 +48,38 @@ JNIEXPORT jstring JNICALL Java_com_ttstd_dialer_utils_NativeUtils_getQWeatherCredentialId(JNIEnv *env, jclass clazz) { std::string key = "KB5C8PK5TY"; return env->NewStringUTF(key.c_str()); +} + +extern "C" +JNIEXPORT jstring JNICALL +Java_com_ttstd_dialer_utils_NativeUtils_getAppSecureSecretKey(JNIEnv *env, jclass clazz) { + +} + +// 生成指定长度的随机字符串 +std::string generateRandomString(int length) { + const char charset[] = + "0123456789" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz"; + + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution<> dis(0, sizeof(charset) - 2); // 减2是因为包含结束符 + + std::string result; + result.reserve(length); + + for (int i = 0; i < length; ++i) { + result += charset[dis(gen)]; + } + + return result; +} + +extern "C" +JNIEXPORT jstring JNICALL +Java_com_ttstd_dialer_utils_NativeUtils_getNonce(JNIEnv *env, jclass clazz) { + std::string randomStr = generateRandomString(8); // 生成长度为32的随机字符串 + return env->NewStringUTF(randomStr.c_str()); } \ No newline at end of file diff --git a/app/src/main/res/layout/activity_settings_utils.xml b/app/src/main/res/layout/activity_settings_utils.xml index ce51acc..398768a 100644 --- a/app/src/main/res/layout/activity_settings_utils.xml +++ b/app/src/main/res/layout/activity_settings_utils.xml @@ -116,6 +116,7 @@ android:text="截图" android:onClick="@{click::screenshotSnap}" app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@+id/iv_snap" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> @@ -129,6 +130,30 @@ + + + + + + + + diff --git a/iconloader/src/main/java/com/ttstd/iconloader/IconCacheManager.java b/iconloader/src/main/java/com/ttstd/iconloader/IconCacheManager.java index a5ad624..e56b973 100644 --- a/iconloader/src/main/java/com/ttstd/iconloader/IconCacheManager.java +++ b/iconloader/src/main/java/com/ttstd/iconloader/IconCacheManager.java @@ -11,7 +11,6 @@ import android.graphics.Canvas; import android.graphics.drawable.AdaptiveIconDrawable; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; -import android.graphics.drawable.LayerDrawable; import android.os.Build; import android.util.Log; @@ -44,7 +43,7 @@ public class IconCacheManager { public static IconCacheManager getInstance() { if (INSTANCE == null) { - throw new IllegalStateException("You must be init IconCacheManager first"); + throw new IllegalStateException("You must first initialize the IconCacheManager"); } return INSTANCE; }