优化天气,增加悬浮窗,增加快捷拨号,增加设置默认桌面
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
package com.ttstd.dialer.activity.app;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
@@ -84,6 +86,8 @@ public class AppListActivity extends BaseMvvmActivity<AppListViewModel, Activity
|
||||
|
||||
|
||||
public class BtnClick {
|
||||
|
||||
public void exit(View view) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,10 @@ public class ContactAddActivity extends BaseMvvmActivity<ContactAddViewModel, Ac
|
||||
}
|
||||
|
||||
public class BtnClick {
|
||||
public void exit(View view) {
|
||||
finish();
|
||||
}
|
||||
|
||||
public void save(View view) {
|
||||
String name = mViewDataBinding.etName.getText().toString();
|
||||
String phone = mViewDataBinding.etPhone.getText().toString();
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.ttstd.dialer.activity.contact.edit;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import com.ttstd.dialer.R;
|
||||
import com.ttstd.dialer.base.mvvm.BaseMvvmActivity;
|
||||
import com.ttstd.dialer.databinding.ActivityContactEditBinding;
|
||||
@@ -37,7 +39,9 @@ public class ContactEditActivity extends BaseMvvmActivity<ContactEditViewModel,
|
||||
|
||||
|
||||
public class BtnClick{
|
||||
|
||||
public void exit(View view) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -136,6 +136,10 @@ public class ContactListActivity extends BaseMvvmActivity<ContactListViewModel,
|
||||
}
|
||||
|
||||
public class BtnClick {
|
||||
public void exit(View view) {
|
||||
finish();
|
||||
}
|
||||
|
||||
public void addContact(View view) {
|
||||
startActivity(new Intent(ContactListActivity.this, ContactAddActivity.class));
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.ttstd.dialer.fragment.app.AppFragment;
|
||||
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.view.ApkPagerAdapter;
|
||||
import com.ttstd.dialer.view.ScaleCircleNavigator;
|
||||
|
||||
@@ -150,6 +151,9 @@ public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBi
|
||||
Log.e(TAG, "initView: mDefaultIndex = " + mDefaultIndex);
|
||||
Log.e(TAG, "initView: mFragmentSize = " + mFragmentSize);
|
||||
|
||||
Intent intent = new Intent(this, MainService.class);
|
||||
startService(intent);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.ttstd.dialer.activity.settings.call;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import com.ttstd.dialer.R;
|
||||
import com.ttstd.dialer.activity.settings.home.SettingsViewModel;
|
||||
import com.ttstd.dialer.base.mvvm.BaseMvvmActivity;
|
||||
import com.ttstd.dialer.databinding.ActivitySettingsBinding;
|
||||
import com.ttstd.dialer.databinding.ActivitySettingsCallBinding;
|
||||
|
||||
public class SettingsCallActivity extends BaseMvvmActivity<SettingsCallViewModel, ActivitySettingsCallBinding> {
|
||||
private static final String TAG = "SettingsActivity";
|
||||
|
||||
@Override
|
||||
public boolean setfitWindow() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setNightMode() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getLayoutId() {
|
||||
return R.layout.activity_settings_call;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initDataBinding() {
|
||||
mViewModel.setContext(this);
|
||||
mViewModel.setVDBinding(mViewDataBinding);
|
||||
mViewModel.setLifecycle(getLifecycleSubject());
|
||||
mViewDataBinding.setClick(new BtnClick());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initView() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initData() {
|
||||
|
||||
}
|
||||
|
||||
public class BtnClick {
|
||||
public void exit(View view) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.ttstd.dialer.activity.settings.call;
|
||||
|
||||
import com.trello.rxlifecycle4.android.ActivityEvent;
|
||||
import com.ttstd.dialer.base.mvvm.BaseViewModel;
|
||||
import com.ttstd.dialer.databinding.ActivitySettingsCallBinding;
|
||||
|
||||
public class SettingsCallViewModel extends BaseViewModel<ActivitySettingsCallBinding, ActivityEvent> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.ttstd.dialer.activity.settings.home;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.view.View;
|
||||
|
||||
import com.ttstd.dialer.R;
|
||||
import com.ttstd.dialer.activity.settings.call.SettingsCallActivity;
|
||||
import com.ttstd.dialer.activity.settings.utils.SettingsUtilsActivity;
|
||||
import com.ttstd.dialer.base.mvvm.BaseMvvmActivity;
|
||||
import com.ttstd.dialer.databinding.ActivitySettingsBinding;
|
||||
|
||||
public class SettingsActivity extends BaseMvvmActivity<SettingsViewModel, ActivitySettingsBinding> {
|
||||
private static final String TAG = "SettingsActivity";
|
||||
|
||||
@Override
|
||||
public boolean setfitWindow() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setNightMode() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getLayoutId() {
|
||||
return R.layout.activity_settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initDataBinding() {
|
||||
mViewModel.setContext(this);
|
||||
mViewModel.setVDBinding(mViewDataBinding);
|
||||
mViewModel.setLifecycle(getLifecycleSubject());
|
||||
mViewDataBinding.setClick(new BtnClick());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initView() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initData() {
|
||||
|
||||
}
|
||||
|
||||
public class BtnClick {
|
||||
public void exit(View view) {
|
||||
finish();
|
||||
}
|
||||
|
||||
public void openCallSettings(View view) {
|
||||
startActivity(new Intent(SettingsActivity.this, SettingsCallActivity.class));
|
||||
}
|
||||
|
||||
public void openUtilsSettings(View view) {
|
||||
startActivity(new Intent(SettingsActivity.this, SettingsUtilsActivity.class));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.ttstd.dialer.activity.settings.home;
|
||||
|
||||
import com.trello.rxlifecycle4.android.ActivityEvent;
|
||||
import com.ttstd.dialer.base.mvvm.BaseViewModel;
|
||||
import com.ttstd.dialer.databinding.ActivitySettingsBinding;
|
||||
import com.ttstd.dialer.databinding.ActivityTemplateBinding;
|
||||
|
||||
public class SettingsViewModel extends BaseViewModel<ActivitySettingsBinding, ActivityEvent> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package com.ttstd.dialer.activity.settings.utils;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
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.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.SystemUtils;
|
||||
import com.ttstd.dialer.view.ToggleButton;
|
||||
|
||||
public class SettingsUtilsActivity extends BaseMvvmActivity<SettingsUtilsViewModel, ActivitySettingsUtilsBinding> {
|
||||
private static final String TAG = "SettingsActivity";
|
||||
|
||||
private MMKV mMMKV = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE);
|
||||
|
||||
@Override
|
||||
public boolean setfitWindow() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setNightMode() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getLayoutId() {
|
||||
return R.layout.activity_settings_utils;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initDataBinding() {
|
||||
mViewModel.setContext(this);
|
||||
mViewModel.setVDBinding(mViewDataBinding);
|
||||
mViewModel.setLifecycle(getLifecycleSubject());
|
||||
mViewDataBinding.setClick(new BtnClick());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initView() {
|
||||
|
||||
mViewDataBinding.siFloatWindow.setOnToggleChanged(new ToggleButton.OnToggleChanged() {
|
||||
@Override
|
||||
public void onToggle(boolean on) {
|
||||
Intent intent = new Intent(SettingsUtilsActivity.this, MainService.class);
|
||||
if (on) {
|
||||
intent.setAction(MainService.SHOW_FLOAT_WINDOW_ACTION);
|
||||
mMMKV.encode(CommonConfig.FLOAT_WINDOW_ENABLE, 1);
|
||||
} else {
|
||||
intent.setAction(MainService.HIDE_FLOAT_WINDOW_ACTION);
|
||||
mMMKV.encode(CommonConfig.FLOAT_WINDOW_ENABLE, 0);
|
||||
}
|
||||
startService(intent);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
mViewDataBinding.siFloatWindowKill.setOnToggleChanged(new ToggleButton.OnToggleChanged() {
|
||||
@Override
|
||||
public void onToggle(boolean on) {
|
||||
if (on) {
|
||||
mMMKV.encode(CommonConfig.FLOAT_WINDOW_KILL_APP, 1);
|
||||
} else {
|
||||
mMMKV.encode(CommonConfig.FLOAT_WINDOW_KILL_APP, 0);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
mViewDataBinding.siDefaultLauncher.setOnToggleChanged(new ToggleButton.OnToggleChanged() {
|
||||
@Override
|
||||
public void onToggle(boolean on) {
|
||||
if (on) {
|
||||
// SystemUtils.setDefaultLauncher(SettingsUtilsActivity.this, MainActivity.class);
|
||||
// SystemUtils.setDefaultLauncher(SettingsUtilsActivity.this);
|
||||
SystemUtils.addRoleHolderAsUser(SettingsUtilsActivity.this, BuildConfig.APPLICATION_ID);
|
||||
} else {
|
||||
SystemUtils.setOtherDefaultLauncher(SettingsUtilsActivity.this);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initData() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
int enableFloatWindow = mMMKV.decodeInt(CommonConfig.FLOAT_WINDOW_ENABLE, 0);
|
||||
Log.e(TAG, "initView: enableFloatWindow = " + enableFloatWindow);
|
||||
mViewDataBinding.siFloatWindow.setToggleStatu(enableFloatWindow == 1);
|
||||
|
||||
int floatWindowKillApp = mMMKV.decodeInt(CommonConfig.FLOAT_WINDOW_KILL_APP, 0);
|
||||
Log.e(TAG, "initView: floatWindowKillApp = " + floatWindowKillApp);
|
||||
mViewDataBinding.siFloatWindowKill.setToggleStatu(floatWindowKillApp == 1);
|
||||
|
||||
boolean defaultLauncher = SystemUtils.isDefaultLauncher(SettingsUtilsActivity.this, MainActivity.class);
|
||||
Log.e(TAG, "initView: defaultLauncher = " + defaultLauncher);
|
||||
mViewDataBinding.siDefaultLauncher.setToggleStatu(defaultLauncher);
|
||||
}
|
||||
|
||||
public class BtnClick {
|
||||
public void exit(View view) {
|
||||
finish();
|
||||
}
|
||||
|
||||
public void screenshotSnap(View view) {
|
||||
// Bitmap bitmap = SystemUtils.takeFullScreenshot(SettingsUtilsActivity.this);
|
||||
Bitmap bitmap = SystemUtils.takeScreenshotHighVersion();
|
||||
if (bitmap != null) {
|
||||
mViewDataBinding.ivSnap.setImageBitmap(bitmap);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.ttstd.dialer.activity.settings.utils;
|
||||
|
||||
import com.trello.rxlifecycle4.android.ActivityEvent;
|
||||
import com.ttstd.dialer.base.mvvm.BaseViewModel;
|
||||
import com.ttstd.dialer.databinding.ActivitySettingsCallBinding;
|
||||
import com.ttstd.dialer.databinding.ActivitySettingsUtilsBinding;
|
||||
|
||||
public class SettingsUtilsViewModel extends BaseViewModel<ActivitySettingsUtilsBinding, ActivityEvent> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.ttstd.dialer.activity.template;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import com.ttstd.dialer.R;
|
||||
import com.ttstd.dialer.base.mvvm.BaseMvvmActivity;
|
||||
import com.ttstd.dialer.databinding.ActivityTemplateBinding;
|
||||
|
||||
public class TemplateActivity extends BaseMvvmActivity<TemplateViewModel, ActivityTemplateBinding> {
|
||||
private static final String TAG = "TemplateActivity";
|
||||
|
||||
|
||||
@Override
|
||||
public boolean setfitWindow() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getLayoutId() {
|
||||
return R.layout.activity_template;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initDataBinding() {
|
||||
mViewModel.setContext(this);
|
||||
mViewModel.setVDBinding(mViewDataBinding);
|
||||
mViewModel.setLifecycle(getLifecycleSubject());
|
||||
mViewDataBinding.setClick(new BtnClick());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initView() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initData() {
|
||||
|
||||
}
|
||||
|
||||
public class BtnClick {
|
||||
public void exit(View view) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.ttstd.dialer.activity.template;
|
||||
|
||||
import com.trello.rxlifecycle4.android.ActivityEvent;
|
||||
import com.ttstd.dialer.base.mvvm.BaseViewModel;
|
||||
import com.ttstd.dialer.databinding.ActivityTemplateBinding;
|
||||
|
||||
public class TemplateViewModel extends BaseViewModel<ActivityTemplateBinding, ActivityEvent> {
|
||||
|
||||
}
|
||||
@@ -35,6 +35,12 @@ public class WeatherMainActivity extends BaseMvvmActivity<WeatherMainViewModel,
|
||||
// return true;
|
||||
// }
|
||||
|
||||
|
||||
@Override
|
||||
public boolean setNightMode() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getLayoutId() {
|
||||
return R.layout.activity_weather_main;
|
||||
|
||||
@@ -44,8 +44,8 @@ public class HourlyWeatherAdapter extends RecyclerView.Adapter<HourlyWeatherAdap
|
||||
mContext = (FragmentActivity) parent.getContext();
|
||||
requestBuilder = GlideApp.with(mContext)
|
||||
.as(PictureDrawable.class)
|
||||
.placeholder(R.drawable.image_loading)
|
||||
.error(R.drawable.not_applicable)
|
||||
// .placeholder(R.drawable.image_loading)
|
||||
.error(R.drawable.ic_not_applicable)
|
||||
.transition(withCrossFade())
|
||||
.listener(new SvgSoftwareLayerSetter());
|
||||
return new HourlyWeather(LayoutInflater.from(mContext).inflate(R.layout.item_hourly_weather, parent, false));
|
||||
@@ -53,31 +53,34 @@ public class HourlyWeatherAdapter extends RecyclerView.Adapter<HourlyWeatherAdap
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull HourlyWeather holder, int position) {
|
||||
WeatherHourly weatherHourly = mWeatherHourlyList.get(position);
|
||||
String fxTime = weatherHourly.getFxTime();
|
||||
holder.tv_time.setText(TimeUtils.formatToHourDescription(fxTime));
|
||||
String icon = weatherHourly.getIcon();
|
||||
// 获取资源ID
|
||||
String fileName = "qweather_" + icon; // 替换为你的文件名
|
||||
int resId = mContext.getResources().getIdentifier(fileName, "raw", mContext.getPackageName());
|
||||
if (resId != 0) {
|
||||
// 使用Glide加载,假设已配置SVG支持
|
||||
Glide.with(mContext)
|
||||
.as(PictureDrawable.class)
|
||||
.load(resId) // 加载raw资源ID
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE) // raw资源通常不缓存
|
||||
.into(holder.iv_icon);
|
||||
} else {
|
||||
// 处理错误
|
||||
Log.e("GlideLoad", "Raw resource not found: " + fileName);
|
||||
}
|
||||
if (mWeatherHourlyList != null && !mWeatherHourlyList.isEmpty()) {
|
||||
WeatherHourly weatherHourly = mWeatherHourlyList.get(position);
|
||||
String fxTime = weatherHourly.getFxTime();
|
||||
holder.tv_time.setText(TimeUtils.formatToHourDescription(fxTime));
|
||||
String icon = weatherHourly.getIcon();
|
||||
// 获取资源ID
|
||||
String fileName = "qweather_" + icon; // 替换为你的文件名
|
||||
int resId = mContext.getResources().getIdentifier(fileName, "raw", mContext.getPackageName());
|
||||
if (resId != 0) {
|
||||
// 使用Glide加载,假设已配置SVG支持
|
||||
// Glide.with(mContext)
|
||||
// .as(PictureDrawable.class)
|
||||
// .load(resId) // 加载raw资源ID
|
||||
// .diskCacheStrategy(DiskCacheStrategy.NONE) // raw资源通常不缓存
|
||||
// .into(holder.iv_icon);
|
||||
requestBuilder.load(resId).into(holder.iv_icon);
|
||||
} else {
|
||||
// 处理错误
|
||||
Log.e("GlideLoad", "Raw resource not found: " + fileName);
|
||||
}
|
||||
|
||||
holder.tv_temp.setText(weatherHourly.getTemp() + "°");
|
||||
holder.tv_temp.setText(weatherHourly.getTemp() + "°");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mWeatherHourlyList == null ? 0 : mWeatherHourlyList.size();
|
||||
return mWeatherHourlyList == null ? 27 : mWeatherHourlyList.size();
|
||||
}
|
||||
|
||||
public class HourlyWeather extends RecyclerView.ViewHolder {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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;
|
||||
@@ -9,15 +11,20 @@ import android.widget.TextView;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.bumptech.glide.RequestBuilder;
|
||||
import com.qweather.sdk.response.weather.WeatherDaily;
|
||||
import com.tencent.mmkv.MMKV;
|
||||
import com.ttstd.dialer.R;
|
||||
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 java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade;
|
||||
|
||||
public class WeatherAdapter extends RecyclerView.Adapter<WeatherAdapter.WeatherHolder> {
|
||||
private static final String TAG = "WeatherAdapter";
|
||||
|
||||
@@ -25,6 +32,8 @@ public class WeatherAdapter extends RecyclerView.Adapter<WeatherAdapter.WeatherH
|
||||
|
||||
private FragmentActivity mContext;
|
||||
private List<WeatherDaily> mWeatherDailyList;
|
||||
private RequestBuilder<PictureDrawable> requestBuilder;
|
||||
|
||||
|
||||
public void setWeatherDailyList(List<WeatherDaily> weatherDailyList) {
|
||||
mWeatherDailyList = weatherDailyList;
|
||||
@@ -34,31 +43,54 @@ public class WeatherAdapter extends RecyclerView.Adapter<WeatherAdapter.WeatherH
|
||||
@Override
|
||||
public WeatherHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
mContext = (FragmentActivity) parent.getContext();
|
||||
requestBuilder = GlideApp.with(mContext)
|
||||
.as(PictureDrawable.class)
|
||||
// .placeholder(R.drawable.image_loading)
|
||||
.error(R.drawable.ic_not_applicable)
|
||||
.transition(withCrossFade())
|
||||
.listener(new SvgSoftwareLayerSetter());
|
||||
return new WeatherHolder(LayoutInflater.from(mContext).inflate(R.layout.item_weather, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(WeatherHolder holder, int position) {
|
||||
WeatherDaily weatherDaily = mWeatherDailyList.get(position);
|
||||
String date = weatherDaily.getFxDate();
|
||||
if (DateUtil.isTodayAll(date)) {
|
||||
holder.tv_date.setText("今天");
|
||||
} else {
|
||||
String week = DateUtil.convertToChineseWeekdayAll(date);
|
||||
holder.tv_date.setText(week);
|
||||
if (mWeatherDailyList != null && !mWeatherDailyList.isEmpty()) {
|
||||
WeatherDaily weatherDaily = mWeatherDailyList.get(position);
|
||||
String date = weatherDaily.getFxDate();
|
||||
if (DateUtil.isTodayAll(date)) {
|
||||
holder.tv_date.setText("今天");
|
||||
} else {
|
||||
String week = DateUtil.convertToChineseWeekdayAll(date);
|
||||
holder.tv_date.setText(week);
|
||||
}
|
||||
|
||||
String tempMin = weatherDaily.getTempMin();
|
||||
holder.tv_temp_min.setText(tempMin + "°");
|
||||
String tempMax = weatherDaily.getTempMax();
|
||||
holder.tv_temp_max.setText(tempMax + "°");
|
||||
|
||||
String iconDay = weatherDaily.getIconDay();
|
||||
// 获取资源ID
|
||||
String fileName = "qweather_" + iconDay; // 替换为你的文件名
|
||||
int resId = mContext.getResources().getIdentifier(fileName, "raw", mContext.getPackageName());
|
||||
if (resId != 0) {
|
||||
// 使用Glide加载,假设已配置SVG支持
|
||||
// Glide.with(mContext)
|
||||
// .as(PictureDrawable.class)
|
||||
// .load(resId) // 加载raw资源ID
|
||||
// .diskCacheStrategy(DiskCacheStrategy.NONE) // raw资源通常不缓存
|
||||
// .into(holder.iv_icon);
|
||||
requestBuilder.load(resId).into(holder.iv_icon);
|
||||
} else {
|
||||
// 处理错误
|
||||
Log.e("GlideLoad", "Raw resource not found: " + fileName);
|
||||
}
|
||||
}
|
||||
|
||||
String tempMin = weatherDaily.getTempMin();
|
||||
holder.tv_temp_min.setText(tempMin + "°");
|
||||
String tempMax = weatherDaily.getTempMax();
|
||||
holder.tv_temp_max.setText(tempMax + "°");
|
||||
|
||||
String iconDay = weatherDaily.getIconDay();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mWeatherDailyList == null ? 0 : mWeatherDailyList.size();
|
||||
return mWeatherDailyList == null ? 10 : mWeatherDailyList.size();
|
||||
}
|
||||
|
||||
public class WeatherHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
@@ -5,6 +5,14 @@ public class CommonConfig {
|
||||
|
||||
public static final String CONTACT_HOME_PAGE = "contact_home_page_key";
|
||||
|
||||
public static final String FLOAT_WINDOW_X = "float_window_x_key";
|
||||
public static final String FLOAT_WINDOW_Y = "float_window_y_key";
|
||||
public static final String FLOAT_WINDOW_ENABLE = "float_window_enable_key";
|
||||
public static final String FLOAT_WINDOW_KILL_APP = "float_window_kill_app_key";
|
||||
|
||||
public static final String WECHAT_AUTO_ACCEPT_CALL= "wechat_auto_accept_call_key";
|
||||
public static final String WECHAT_AUTO_HNADS_FREE= "wechat_auto_hands_free_key";
|
||||
|
||||
public static final String MANUALLY_SELECT_LOCATION = "manually_select_location_key";
|
||||
|
||||
/*地址改变时发送*/
|
||||
|
||||
@@ -38,6 +38,9 @@ public class AppFragment extends BaseMvvmFragment<AppViewModel, FragmentAppBindi
|
||||
// }
|
||||
|
||||
|
||||
public AppFragment() {
|
||||
}
|
||||
|
||||
public AppFragment(List<AppInfo> appInfos) {
|
||||
mAppInfos = appInfos;
|
||||
Log.e(TAG, "AppFragment: mAppInfos = " + mAppInfos.hashCode());
|
||||
|
||||
@@ -26,6 +26,8 @@ public class ContactFragment extends BaseMvvmFragment<ContactViewModel, Fragment
|
||||
|
||||
private static final int REQUEST_CODE_CALL = 7897;
|
||||
|
||||
public ContactFragment() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getLayoutId() {
|
||||
|
||||
@@ -38,6 +38,9 @@ public class CallFragment extends BaseMvvmDialogFragment<CallViewModel, Fragment
|
||||
private Activity mContext;
|
||||
private ContactInfo mContactInfo;
|
||||
|
||||
public CallFragment() {
|
||||
}
|
||||
|
||||
public CallFragment(ContactInfo contactInfo) {
|
||||
mContactInfo = contactInfo;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,9 @@ public class PermissionDialogFragment extends BaseDialogFragment {
|
||||
private DialogFragmentPermissionsBinding mBinding;
|
||||
private String mContent;
|
||||
|
||||
public PermissionDialogFragment() {
|
||||
}
|
||||
|
||||
public PermissionDialogFragment(String content) {
|
||||
this.mContent = content;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,9 @@ public class ShortcutDialogFagment extends BaseMvvmDialogFragment<ShortcutViewMo
|
||||
private String mNegativeText;
|
||||
private String mPositiveText;
|
||||
|
||||
public ShortcutDialogFagment() {
|
||||
}
|
||||
|
||||
public ShortcutDialogFagment(AppInfo appInfo) {
|
||||
mAppInfo = appInfo;
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ package com.ttstd.dialer.fragment.settings;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import com.ttstd.dialer.R;
|
||||
import com.ttstd.dialer.activity.app.AppListActivity;
|
||||
import com.ttstd.dialer.activity.settings.home.SettingsActivity;
|
||||
import com.ttstd.dialer.base.mvvm.fragment.BaseMvvmFragment;
|
||||
import com.ttstd.dialer.databinding.FragmentSettingsBinding;
|
||||
|
||||
@@ -17,6 +17,9 @@ public class SettingsFragment extends BaseMvvmFragment<SettingsViewModel, Fragme
|
||||
|
||||
private Activity mContext;
|
||||
|
||||
public SettingsFragment() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getLayoutId() {
|
||||
return R.layout.fragment_settings;
|
||||
@@ -48,7 +51,7 @@ public class SettingsFragment extends BaseMvvmFragment<SettingsViewModel, Fragme
|
||||
|
||||
public class BtnClick {
|
||||
public void openSettings(View view) {
|
||||
Intent intent = new Intent(Settings.ACTION_SETTINGS);
|
||||
Intent intent = new Intent(mContext, SettingsActivity.class);
|
||||
try {
|
||||
startActivity(intent);
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
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<String> packages) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getInstallPackageTrustList() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,45 +1,135 @@
|
||||
package com.ttstd.dialer.service;
|
||||
|
||||
import android.accessibilityservice.AccessibilityService;
|
||||
import android.accessibilityservice.GestureDescription;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
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;
|
||||
import android.view.accessibility.AccessibilityWindowInfo;
|
||||
|
||||
import com.blankj.utilcode.util.ToastUtils;
|
||||
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.ApkUtils;
|
||||
import com.ttstd.dialer.utils.SystemUtils;
|
||||
|
||||
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;
|
||||
|
||||
private ContactInfo mContactInfo;
|
||||
private static final int ACTION_IME_ENTER_VERSION = 30;
|
||||
private static final int ACTION_IME_ENTER_ID = 16908372;
|
||||
|
||||
private static final String DIALER_TEXT = "音视频通话";
|
||||
private static final String CONTACT_TEXT = "通讯录";
|
||||
private static final String SEARCH_TEXT = "搜索";
|
||||
private static final String TAG_TEXT = "标签";
|
||||
private static final String MORE_NAME = "更多功能按钮,已折叠";
|
||||
private static final String PARENT_VIDEO_TEXT = "视频通话";
|
||||
|
||||
private static final String VIDEO_TEXT = "视频通话";
|
||||
private static final String CALL_TEXT = "语音通话";
|
||||
|
||||
private static final String RECEIVE_DESCRIPTION = "接听";
|
||||
private static final String HANDS_FREE_TEXT = "扬声器已关";
|
||||
private static final String DIALER_HANDS_FREE_TEXT = "免提";
|
||||
private static final String DIALER_HANDS_FREE_CLOSE_TEXT = "免提,已关闭";
|
||||
|
||||
private static final int WAIT_TIME = 1600;
|
||||
|
||||
private Handler mHandler;
|
||||
|
||||
private Step mCurrentStep = Step.WAITING;
|
||||
private String mName = "";//微信昵称
|
||||
private String mTagName = "";//微信联系人标签名
|
||||
|
||||
private boolean mAutoAccept = false;
|
||||
private boolean mAutoHandsFree = false;
|
||||
|
||||
private ContactInfo mContactInfo;
|
||||
private int mCallType = ACTION_VIDEO;
|
||||
|
||||
@Override
|
||||
public void onAccessibilityEvent(AccessibilityEvent event) {
|
||||
|
||||
public interface AccessibilityEventCallback {
|
||||
void onAccessibilityEventCallback(AccessibilityEvent accessibilityEvent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInterrupt() {
|
||||
|
||||
}
|
||||
private AccessibilityEventCallback mAccessibilityEventCallback;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
Log.e(TAG, "onCreate: ");
|
||||
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<AccessibilityEvent>() {
|
||||
@Override
|
||||
public void subscribe(@NonNull ObservableEmitter<AccessibilityEvent> emitter) throws Throwable {
|
||||
mAccessibilityEventCallback = emitter::onNext;
|
||||
}
|
||||
}).throttleLast(WAIT_TIME, TimeUnit.MILLISECONDS)
|
||||
.subscribe(new Consumer<AccessibilityEvent>() {
|
||||
@Override
|
||||
public void accept(AccessibilityEvent accessibilityEvent) throws Throwable {
|
||||
Log.e(TAG, "analysisAccessibilityEvent accept: ");
|
||||
_onAccessibilityEvent(accessibilityEvent);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
Log.e(TAG, "onStartCommand: ");
|
||||
if (intent != null) {
|
||||
mContactInfo = (ContactInfo) intent.getSerializableExtra("ContactInfo");
|
||||
mCallType = intent.getIntExtra("callType", 1);
|
||||
Log.e(TAG, "onStartCommand: contactInfo = " + mContactInfo);
|
||||
mCallType = intent.getIntExtra("call_type", ACTION_VIDEO);
|
||||
mName = mContactInfo.getName();
|
||||
mCurrentStep = Step.CLICK_HOME;
|
||||
if (mContactInfo != null) {
|
||||
startWeixin();
|
||||
}
|
||||
@@ -50,6 +140,847 @@ public class DialerAccessibilityService extends AccessibilityService {
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
Log.e(TAG, "onDestroy: ");
|
||||
if (mSettingReceiver != null) {
|
||||
unregisterReceiver(mSettingReceiver);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAccessibilityEvent(AccessibilityEvent event) {
|
||||
Log.v(TAG, "onAccessibilityEvent: event = " + event.toString());
|
||||
checkClassName(event);
|
||||
mAccessibilityEventCallback.onAccessibilityEventCallback(event);
|
||||
}
|
||||
|
||||
private void checkClassName(AccessibilityEvent event) {
|
||||
Log.e(TAG, "checkClassName: mCurrentStep = " + mCurrentStep);
|
||||
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
|
||||
String currentPackageName = event.getPackageName().toString();
|
||||
String currentClassName = 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;
|
||||
}
|
||||
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":
|
||||
Toaster.showLong("请先登录微信");
|
||||
mCurrentStep = Step.WAITING;
|
||||
break;
|
||||
case "com.tencent.mm.plugin.label.ui.ContactLabelManagerUI":
|
||||
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 1.在微信页面直接找到联系人拨打电话
|
||||
* 2.在联系人页面找到并拨打
|
||||
* 3.通过进入联系人-标签找到并拨打
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
private void _onAccessibilityEvent(AccessibilityEvent event) {
|
||||
Log.e(TAG, "_onAccessibilityEvent: " + mCurrentStep);
|
||||
switch (mCurrentStep) {
|
||||
case WAITING:
|
||||
mAutoAccept = mMMKV.decodeBool(CommonConfig.WECHAT_AUTO_ACCEPT_CALL, false);
|
||||
Log.e(TAG, "_onAccessibilityEvent: 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);
|
||||
if (mAutoHandsFree) {
|
||||
handsFree(Property.DESCRIPTION, HANDS_FREE_TEXT);
|
||||
} else {
|
||||
Log.e(TAG, "_onAccessibilityEvent: not enable auto handsfree");
|
||||
}
|
||||
case DIALER_HANDS_FREE:
|
||||
if (findHandsFree(Property.DESCRIPTION, DIALER_HANDS_FREE_CLOSE_TEXT)) {
|
||||
dialerHandsFree(Property.TEXT, DIALER_HANDS_FREE_TEXT);
|
||||
} else {
|
||||
mCurrentStep = Step.WAITING;
|
||||
}
|
||||
break;
|
||||
case CLICK_HOME://主页能找到直接点击进去更多
|
||||
if (stepHome(Property.TEXT, mName)) {
|
||||
Log.e(TAG, "_onAccessibilityEvent: not 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);
|
||||
} else {
|
||||
Toaster.show("没有找到联系人");
|
||||
}
|
||||
break;
|
||||
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://点击视频通话
|
||||
stepCall(Property.TEXT, PARENT_VIDEO_TEXT);
|
||||
// clickVideoCall();
|
||||
break;
|
||||
case CLICK_CALL://打视频或者电话
|
||||
if (mCallType == ACTION_AUDIO) {
|
||||
step(Property.TEXT, VIDEO_TEXT, Step.WAITING);
|
||||
} else if (mCallType == ACTION_VIDEO) {
|
||||
step(Property.TEXT, CALL_TEXT, Step.WAITING);
|
||||
}
|
||||
break;
|
||||
|
||||
case CLICK_CONTACT://进入通讯录界面
|
||||
if (stepHome(Property.TEXT, CONTACT_TEXT, Step.FIND_TAG)) {
|
||||
Log.e(TAG, "_onAccessibilityEvent: enter contact");
|
||||
} else {
|
||||
touchContact();
|
||||
}
|
||||
break;
|
||||
case FIND_CONTACT://模拟滑动找到联系人
|
||||
findSearchContact(Property.TEXT, mName, Step.CLICK_QUICK_WECHAT_CALL);
|
||||
break;
|
||||
case FIND_TAG:
|
||||
step(Property.TEXT, TAG_TEXT, Step.CLICK_TAG);
|
||||
break;
|
||||
case CLICK_TAG:
|
||||
if (!step(Property.TEXT, mTagName, Step.CLICK_NAME)) {
|
||||
Toaster.show("没有找到标签");
|
||||
mCurrentStep = Step.WAITING;
|
||||
}
|
||||
break;
|
||||
case CLICK_NAME://点击item
|
||||
findContact(Property.TEXT, mName, Step.CLICK_INFO);
|
||||
break;
|
||||
case CLICK_INFO://进入个人信息页面
|
||||
stepCallDialog(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: ");
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onServiceConnected() {
|
||||
super.onServiceConnected();
|
||||
Log.e(TAG, "onServiceConnected: ");
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开这个界面可以直接选择联系人拨打
|
||||
*/
|
||||
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 autoAccept() {
|
||||
if (stepAnswer(Property.DESCRIPTION, RECEIVE_DESCRIPTION)) {
|
||||
mCurrentStep = Step.WECHAT_HANDS_FREE;
|
||||
ToastUtils.showShort("已自动接听视频/语音");
|
||||
} else if (clickNode("com.tencent.mm:id/kfp", false)) {
|
||||
mCurrentStep = Step.WECHAT_HANDS_FREE;
|
||||
ToastUtils.showShort("已自动接听视频/语音");
|
||||
} else {
|
||||
mCurrentStep = Step.WAITING;
|
||||
// clickAnswer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param text 对应文本
|
||||
* @param simulate 是否通过坐标模拟点击
|
||||
* @return
|
||||
*/
|
||||
private boolean clickNode(String text, boolean simulate) {
|
||||
findFloatWindowNode(text);
|
||||
List<AccessibilityNodeInfo> nodeInfos = findNodesByViewId(text);
|
||||
Optional<AccessibilityNodeInfo> 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);
|
||||
node.recycle();
|
||||
return performAction;
|
||||
} else {
|
||||
if (simulate) {
|
||||
Point point = getPointtByNode(node);
|
||||
Log.e(TAG, "clickNode: " + point);
|
||||
clickByPoint(point.x, point.y);
|
||||
Log.e(TAG, "clickNode: mCurrentStep " + mCurrentStep + " done");
|
||||
} else {
|
||||
clickNode(findClickableNode(node));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
Log.e(TAG, "clickNode: not found");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private AccessibilityNodeInfo findClickableNode(AccessibilityNodeInfo node) {
|
||||
if (node == null) {
|
||||
Log.e(TAG, "findClickableNode: node is null");
|
||||
return null;
|
||||
}
|
||||
if (node.isClickable()) {
|
||||
return node;
|
||||
} else {
|
||||
return findClickableNode(node);
|
||||
}
|
||||
}
|
||||
|
||||
public void findFloatWindowNode(String id) {
|
||||
List<AccessibilityWindowInfo> windows = getWindows();
|
||||
for (AccessibilityWindowInfo window : windows) {
|
||||
// 筛选悬浮窗窗口
|
||||
if (isFloatingWindow(window)) {
|
||||
AccessibilityNodeInfo rootNode = window.getRoot();
|
||||
// 处理悬浮窗节点
|
||||
traverseNode(rootNode);
|
||||
rootNode.recycle(); // 释放资源
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private boolean isFloatingWindow(AccessibilityWindowInfo window) {
|
||||
Log.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("已自动接听视频/语音");
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "clickAnswer: Not in the answering interface");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@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<AccessibilityNodeInfo> nodeInfos = findNodesByViewId("com.tencent.mm:id/gzf");
|
||||
Log.e(TAG, "findSearchContact: " + nodeInfos);
|
||||
Optional<AccessibilityNodeInfo> optional = nodeInfos.stream().findAny();
|
||||
return optional.isPresent();
|
||||
}
|
||||
|
||||
private void findSearchContact(Property type, String text, Step nextStep) {
|
||||
List<AccessibilityNodeInfo> nodeInfos = findNodesByViewId("com.tencent.mm:id/odf");
|
||||
Log.e(TAG, "findSearchContact: " + nodeInfos);
|
||||
Optional<AccessibilityNodeInfo> optional = nodeInfos.stream().findAny();
|
||||
if (optional.isPresent()) {
|
||||
AccessibilityNodeInfo nodeInfo = optional.get();
|
||||
clickNode(nodeInfo);
|
||||
mCurrentStep = nextStep;
|
||||
} else {
|
||||
Toaster.show("没有找到联系人");
|
||||
mCurrentStep = Step.WAITING;
|
||||
}
|
||||
}
|
||||
|
||||
private void putString(String text, Step nextStep) {
|
||||
List<AccessibilityNodeInfo> nodeInfos = findNodesByViewId("com.tencent.mm:id/d98");
|
||||
Optional<AccessibilityNodeInfo> optional = nodeInfos.stream().findAny();
|
||||
if (optional.isPresent()) {
|
||||
AccessibilityNodeInfo nodeInfo = optional.get();
|
||||
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); // 确保焦点在输入框
|
||||
|
||||
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;
|
||||
} else {
|
||||
Toaster.show("没有找到搜索框");
|
||||
mCurrentStep = Step.WAITING;
|
||||
}
|
||||
}
|
||||
|
||||
private void clickViewById(String id, Step nextStep) {
|
||||
List<AccessibilityNodeInfo> nodeInfos = findNodesByViewId(id);
|
||||
Optional<AccessibilityNodeInfo> optional = nodeInfos.stream().findAny();
|
||||
if (optional.isPresent()) {
|
||||
AccessibilityNodeInfo nodeInfo = optional.get();
|
||||
clickNode(nodeInfo);
|
||||
mCurrentStep = nextStep;
|
||||
} else {
|
||||
// Toaster.show("没有找到搜索按钮");
|
||||
step(Property.DESCRIPTION, SEARCH_TEXT, Step.CLICK_SEARCH);
|
||||
}
|
||||
}
|
||||
|
||||
private List<AccessibilityNodeInfo> findNodesByViewId(String id) {
|
||||
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
|
||||
if (nodeInfo != null) {
|
||||
List<AccessibilityNodeInfo> 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<AccessibilityWindowInfo> 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);
|
||||
if (node != null) {
|
||||
Point point = getPointtByNode(node);
|
||||
Log.e(TAG, "stepCall: " + point);
|
||||
clickByPoint(point.x, point.y);
|
||||
// clickNode(node);
|
||||
Log.e(TAG, "stepCall: mCurrentStep " + mCurrentStep + " done");
|
||||
mCurrentStep = Step.CLICK_CALL;
|
||||
Log.e(TAG, "stepCall: next " + mCurrentStep);
|
||||
return true;
|
||||
} else {
|
||||
Log.e(TAG, "stepCall: not found");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void clickVideoCall() {
|
||||
List<AccessibilityNodeInfo> nodeInfos = findNodesByViewId("com.tencent.mm:id/a12");
|
||||
Optional<AccessibilityNodeInfo> accessibilityNodeInfo = nodeInfos.stream().findAny();
|
||||
if (accessibilityNodeInfo.isPresent()) {
|
||||
AccessibilityNodeInfo nodeInfo = accessibilityNodeInfo.get();
|
||||
clickNode(nodeInfo);
|
||||
mCurrentStep = Step.CLICK_CALL;
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
//实现对(x,y)坐标进行点击操作。
|
||||
private boolean clickByPoint(int x, int y) {
|
||||
Log.e(TAG, "clickByNode: x = " + x);
|
||||
Log.e(TAG, "clickByNode: 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)));
|
||||
GestureDescription gesture = builder.build();
|
||||
boolean dispatched = dispatchGesture(gesture, new GestureResultCallback() {
|
||||
@Override
|
||||
public void onCompleted(GestureDescription gestureDescription) {
|
||||
super.onCompleted(gestureDescription);
|
||||
Log.e("clickByNode", "onCompleted: ");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancelled(GestureDescription gestureDescription) {
|
||||
super.onCancelled(gestureDescription);
|
||||
Log.e("clickByNode", "onCompleted: ");
|
||||
}
|
||||
}, null);
|
||||
return dispatched;
|
||||
}
|
||||
|
||||
private boolean scrollScreen(double startY, double endY) {
|
||||
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 center_X = width / 2;
|
||||
int center_Y = height / 2;
|
||||
Log.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)); //终点坐标。
|
||||
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.");
|
||||
path.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancelled(GestureDescription gestureDescription) {
|
||||
super.onCancelled(gestureDescription);
|
||||
Log.d("scrollScreen", "dispatchGesture ScrollUp cancel.");
|
||||
}
|
||||
}, null);
|
||||
return dispatched;
|
||||
}
|
||||
|
||||
private boolean scrollDown() {
|
||||
return scrollScreen(1.5, 0.5);
|
||||
}
|
||||
|
||||
private boolean scrollUp() {
|
||||
return scrollScreen(0.5, 1.5);
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
private enum Property {
|
||||
TEXT,
|
||||
CLASS_NAME,
|
||||
DESCRIPTION
|
||||
}
|
||||
|
||||
public static final String SETTING_CALL_TYPE_ACTION = "setting_call_type_action";
|
||||
public static final String SETTING_AUTOMATIC_ANSWER_ACTION = "setting_automatic_answer_action";
|
||||
|
||||
private SettingReceiver mSettingReceiver;
|
||||
|
||||
private void registerSettingReceiver() {
|
||||
if (mSettingReceiver == null) {
|
||||
mSettingReceiver = new SettingReceiver();
|
||||
}
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(SETTING_CALL_TYPE_ACTION);
|
||||
filter.addAction(SETTING_AUTOMATIC_ANSWER_ACTION);
|
||||
registerReceiver(mSettingReceiver, filter);
|
||||
}
|
||||
|
||||
private class SettingReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
Log.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);
|
||||
break;
|
||||
case SETTING_AUTOMATIC_ANSWER_ACTION:
|
||||
boolean autoAnswer = intent.getBooleanExtra("auto_answer", false);
|
||||
mAutoAccept = autoAnswer;
|
||||
Log.e("SettingReceiver", "onReceive: autoAnswer = " + autoAnswer);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void startWeixin() {
|
||||
@@ -64,7 +995,7 @@ public class DialerAccessibilityService extends AccessibilityService {
|
||||
try {
|
||||
startActivity(intent);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "launchWeChat: " + e.getMessage());
|
||||
Log.e(TAG, "startWeixin: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
258
app/src/main/java/com/ttstd/dialer/service/main/MainService.java
Normal file
258
app/src/main/java/com/ttstd/dialer/service/main/MainService.java
Normal file
@@ -0,0 +1,258 @@
|
||||
package com.ttstd.dialer.service.main;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.provider.Settings;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hjq.toast.Toaster;
|
||||
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.config.CommonConfig;
|
||||
import com.ttstd.dialer.utils.ApkUtils;
|
||||
import com.ttstd.dialer.utils.SystemUtils;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class MainService extends Service {
|
||||
private static final String TAG = "MainService";
|
||||
|
||||
public static final String SHOW_FLOAT_WINDOW_ACTION = "show_float_window";
|
||||
public static final String HIDE_FLOAT_WINDOW_ACTION = "hide_float_window";
|
||||
|
||||
private MMKV mMMKV = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE);
|
||||
|
||||
private WindowManager mWindowManager;
|
||||
|
||||
private View floatingView;
|
||||
private WindowManager.LayoutParams mLayoutParams;
|
||||
private boolean mFloatWindowShowing = false;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
Log.e(TAG, "onCreate: ");
|
||||
|
||||
mWindowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
|
||||
|
||||
boolean enableFloatWindow = mMMKV.decodeInt(CommonConfig.FLOAT_WINDOW_ENABLE, 0) == 1;
|
||||
Log.e(TAG, "onCreate: enableFloatWindow = " + enableFloatWindow);
|
||||
if (enableFloatWindow) {
|
||||
showFloatWindow();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if (intent != null) {
|
||||
String action = intent.getAction();
|
||||
Log.e(TAG, "onStartCommand: action = " + action);
|
||||
if (!TextUtils.isEmpty(action)) {
|
||||
switch (action) {
|
||||
case SHOW_FLOAT_WINDOW_ACTION:
|
||||
showFloatWindow();
|
||||
break;
|
||||
case HIDE_FLOAT_WINDOW_ACTION:
|
||||
hideFloatWindow();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
private void showFloatWindow() {
|
||||
Log.e(TAG, "showFloatWindow: ");
|
||||
initFloatingView();
|
||||
|
||||
if (Settings.canDrawOverlays(this)) {
|
||||
// 4. 将 View 添加到 WindowManager
|
||||
try {
|
||||
mWindowManager.addView(floatingView, mLayoutParams);
|
||||
mFloatWindowShowing = true;
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "onCreate: addView = " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void initFloatingView() {
|
||||
Log.e(TAG, "initFloatingView: ");
|
||||
floatingView = LayoutInflater.from(this).inflate(R.layout.layout_floating_window, null);
|
||||
|
||||
int layoutType;
|
||||
// Android 8.0 (API 26) 及以上需要使用 TYPE_APPLICATION_OVERLAY
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
layoutType = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
|
||||
} else {
|
||||
layoutType = WindowManager.LayoutParams.TYPE_PHONE;
|
||||
}
|
||||
|
||||
mLayoutParams = new WindowManager.LayoutParams(
|
||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||
layoutType,
|
||||
// FLAG_NOT_FOCUSABLE 确保悬浮窗不会拦截原本属于背后应用/桌面的按键事件
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
||||
PixelFormat.TRANSLUCENT
|
||||
);
|
||||
|
||||
// 初始化位置
|
||||
mLayoutParams.gravity = Gravity.TOP | Gravity.START;
|
||||
mLayoutParams.x = mMMKV.decodeInt(CommonConfig.FLOAT_WINDOW_X, 0);
|
||||
mLayoutParams.y = mMMKV.decodeInt(CommonConfig.FLOAT_WINDOW_Y, 0);
|
||||
|
||||
NiceImageView nvBack = floatingView.findViewById(R.id.nv_back);
|
||||
nvBack.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Log.e(TAG, "onClick: ");
|
||||
// stopSelf(); // 停止服务,触发 onDestroy 移除悬浮窗
|
||||
// mWindowManager.removeView(floatingView);
|
||||
// floatingView = null;
|
||||
// mFloatWindowShowing = false;
|
||||
String foreground = SystemUtils.getForegroundActivityPackageName(MainService.this);
|
||||
if (BuildConfig.APPLICATION_ID.equals(foreground)) {
|
||||
int floatWindowKillApp = mMMKV.decodeInt(CommonConfig.FLOAT_WINDOW_KILL_APP, 0);
|
||||
if (floatWindowKillApp == 1) {
|
||||
killApp();
|
||||
} else {
|
||||
|
||||
}
|
||||
} else {
|
||||
Intent intent = new Intent(MainService.this, MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
nvBack.setOnTouchListener(new View.OnTouchListener() {
|
||||
private int initialX;
|
||||
private int initialY;
|
||||
private float initialTouchX;
|
||||
private float initialTouchY;
|
||||
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
Log.e(TAG, "onTouch: ");
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
// 记录初始位置
|
||||
initialX = mLayoutParams.x;
|
||||
initialY = mLayoutParams.y;
|
||||
initialTouchX = event.getRawX();
|
||||
initialTouchY = event.getRawY();
|
||||
break;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
// 计算偏移量并更新位置
|
||||
mLayoutParams.x = initialX + (int) (event.getRawX() - initialTouchX);
|
||||
mLayoutParams.y = initialY + (int) (event.getRawY() - initialTouchY);
|
||||
mMMKV.encode(CommonConfig.FLOAT_WINDOW_X, mLayoutParams.x);
|
||||
mMMKV.encode(CommonConfig.FLOAT_WINDOW_Y, mLayoutParams.y);
|
||||
mWindowManager.updateViewLayout(floatingView, mLayoutParams);
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
break;
|
||||
default:
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private void hideFloatWindow() {
|
||||
Log.e(TAG, "hideFloatWindow: ");
|
||||
if (mFloatWindowShowing) {
|
||||
if (floatingView != null) {
|
||||
mWindowManager.removeView(floatingView);
|
||||
floatingView = null;
|
||||
}
|
||||
mFloatWindowShowing = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static final Set<String> mWhiteRunningAppSets = new HashSet<String>() {{
|
||||
this.add(BuildConfig.APPLICATION_ID);
|
||||
this.add("com.tencent.mm");
|
||||
this.add("com.ss.android.ugc.aweme");
|
||||
|
||||
this.add("com.tencent.wetype");
|
||||
this.add("com.tencent.qqpinyin");
|
||||
this.add("com.google.android.inputmethod.pinyin");
|
||||
this.add("com.sohu.inputmethod.sogou");
|
||||
this.add("com.iflytek.inputmethod");
|
||||
this.add("com.baidu.input");
|
||||
}};
|
||||
|
||||
private void killApp() {
|
||||
Log.e(TAG, "killApp: ");
|
||||
List<String> runningPackages = SystemUtils.getRunningTaskPackages(MainService.this);
|
||||
int i = 0;
|
||||
for (String pkg : runningPackages) {
|
||||
if (mWhiteRunningAppSets.contains(pkg)) {
|
||||
continue;
|
||||
}
|
||||
if (ApkUtils.isSystemApp(MainService.this, pkg)) {
|
||||
continue;
|
||||
}
|
||||
i++;
|
||||
SystemUtils.killBackgroundProcesses(MainService.this, pkg);
|
||||
}
|
||||
Toaster.show(String.format(getString(R.string.kill_app_number), i));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
// 务必在服务销毁时移除 View,防止内存泄漏或系统崩溃
|
||||
if (floatingView != null) {
|
||||
mWindowManager.removeView(floatingView);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLowMemory() {
|
||||
super.onLowMemory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTrimMemory(int level) {
|
||||
super.onTrimMemory(level);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onUnbind(Intent intent) {
|
||||
return super.onUnbind(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRebind(Intent intent) {
|
||||
super.onRebind(intent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.ttstd.dialer.utils;
|
||||
|
||||
public class BitmapUtils {
|
||||
}
|
||||
@@ -2,14 +2,47 @@ package com.ttstd.dialer.utils;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.ActivityManager;
|
||||
import android.app.ActivityManagerNative;
|
||||
import android.app.ActivityTaskManager;
|
||||
import android.app.role.RoleManager;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.content.pm.UserInfo;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.os.UserHandle;
|
||||
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.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
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 static android.app.ActivityManager.RECENT_IGNORE_UNAVAILABLE;
|
||||
|
||||
public class SystemUtils {
|
||||
private static final String TAG = "SystemUtils";
|
||||
|
||||
/**
|
||||
* 获取设备序列号
|
||||
@@ -72,4 +105,364 @@ public class SystemUtils {
|
||||
}
|
||||
|
||||
|
||||
public static String getForegroundActivityPackageName(Context context) {
|
||||
ActivityManager activityManager = (ActivityManager) context.getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE);
|
||||
List<ActivityManager.RunningTaskInfo> runningTaskInfos = activityManager.getRunningTasks(1);
|
||||
if (runningTaskInfos != null && !runningTaskInfos.isEmpty()) {
|
||||
ComponentName componentName = runningTaskInfos.get(0).topActivity;
|
||||
if (componentName != null) {
|
||||
String currentPackageName = componentName.getPackageName();
|
||||
return currentPackageName;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取栈顶的应用包名
|
||||
*/
|
||||
public static String getForegroundActivityClassName(Context context) {
|
||||
ActivityManager manager = (ActivityManager) context.getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE);
|
||||
String currentClassName = manager.getRunningTasks(1).get(0).topActivity.getClassName();
|
||||
return currentClassName;
|
||||
}
|
||||
|
||||
public static List<String> getRunningTaskPackages(Context context) {
|
||||
ActivityManager activityManager = (ActivityManager) context.getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE);
|
||||
List<ActivityManager.RunningTaskInfo> runningTaskInfos = activityManager.getRunningTasks(Integer.MAX_VALUE);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
return runningTaskInfos.stream().map(new Function<ActivityManager.RunningTaskInfo, String>() {
|
||||
@Override
|
||||
public String apply(ActivityManager.RunningTaskInfo runningTaskInfo) {
|
||||
return runningTaskInfo.topActivity.getPackageName();
|
||||
}
|
||||
}).collect(Collectors.toList());
|
||||
} else {
|
||||
List<String> packageNames = new ArrayList<>();
|
||||
for (ActivityManager.RunningTaskInfo runningTaskInfo : runningTaskInfos) {
|
||||
packageNames.add(runningTaskInfo.topActivity.getPackageName());
|
||||
}
|
||||
return packageNames;
|
||||
}
|
||||
}
|
||||
|
||||
public static void killBackgroundProcesses(Context context, String processName) {
|
||||
Log.e(TAG, "killBackgroundProcesses: " + processName);
|
||||
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
||||
String packageName;
|
||||
try {
|
||||
if (!processName.contains(":")) {
|
||||
packageName = processName;
|
||||
} else {
|
||||
packageName = processName.split(":")[0];
|
||||
}
|
||||
activityManager.killBackgroundProcesses(packageName);
|
||||
activityManager.forceStopPackage(packageName);
|
||||
// removeTask(context, processName);
|
||||
|
||||
// Method forceStopPackage = activityManager.getClass()
|
||||
// .getDeclaredMethod("forceStopPackage", String.class);
|
||||
// forceStopPackage.setAccessible(true);
|
||||
// forceStopPackage.invoke(activityManager, packageName);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeTask(Context context, String packageName) {
|
||||
Log.e(TAG, "removeTask: " + packageName);
|
||||
// if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
|
||||
List<ActivityManager.RecentTaskInfo> list = getRecentTasks(ActivityManager.getMaxRecentTasksStatic(), getCurrentUserId());
|
||||
HashMap<String, Integer> taskMap = new HashMap<>();
|
||||
for (ActivityManager.RecentTaskInfo info : list) {
|
||||
taskMap.put(info.realActivity.getPackageName(), info.id);
|
||||
}
|
||||
try {
|
||||
ActivityManagerNative.getDefault().removeTask(taskMap.get(packageName));
|
||||
} catch (RemoteException e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "removeTask: " + e.getMessage());
|
||||
} catch (NullPointerException e) {
|
||||
Log.e(TAG, "removeTask: " + e.getMessage());
|
||||
}
|
||||
// } else {
|
||||
// ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
||||
//
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a list of the recents tasks.
|
||||
* 获取近期任务列表
|
||||
*/
|
||||
public static List<ActivityManager.RecentTaskInfo> getRecentTasks(int numTasks, int userId) {
|
||||
try {
|
||||
return ActivityTaskManager.getService().getRecentTasks(numTasks,
|
||||
RECENT_IGNORE_UNAVAILABLE, userId).getList();
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Failed to get recent tasks " + e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the current user's id.
|
||||
* 获取userId
|
||||
*/
|
||||
public static int getCurrentUserId() {
|
||||
UserInfo ui;
|
||||
try {
|
||||
ui = ActivityManager.getService().getCurrentUser();
|
||||
return ui != null ? ui.id : 0;
|
||||
} catch (RemoteException e) {
|
||||
throw e.rethrowFromSystemServer();
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isDefaultLauncher(Context context, Class<?> launcherActivityClass) {
|
||||
ComponentName componentName = new ComponentName(context, launcherActivityClass);
|
||||
Log.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);
|
||||
return componentName.equals(defaultComponentName);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将指定的 Activity 设置为系统默认桌面(需要系统签名)
|
||||
*
|
||||
* @param context 上下文
|
||||
* @param launcherActivityClass 你的桌面 Activity 类,例如 MyLauncherActivity.class
|
||||
*/
|
||||
public static void setDefaultLauncher(Context context, Class<?> launcherActivityClass) {
|
||||
PackageManager pm = context.getPackageManager();
|
||||
|
||||
// 1. 创建桌面过滤的 IntentFilter
|
||||
IntentFilter filter = new IntentFilter(Intent.ACTION_MAIN);
|
||||
filter.addCategory(Intent.CATEGORY_HOME);
|
||||
filter.addCategory(Intent.CATEGORY_DEFAULT);
|
||||
|
||||
// 2. 查询当前系统中所有的桌面应用(用来构建 ComponentName 数组)
|
||||
Intent homeIntent = new Intent(Intent.ACTION_MAIN);
|
||||
homeIntent.addCategory(Intent.CATEGORY_HOME);
|
||||
List<ResolveInfo> resolveInfos = pm.queryIntentActivities(homeIntent, PackageManager.MATCH_DEFAULT_ONLY);
|
||||
|
||||
int bestMatch = 0;
|
||||
ComponentName[] set = new ComponentName[resolveInfos.size()];
|
||||
for (int i = 0; i < resolveInfos.size(); i++) {
|
||||
ResolveInfo info = resolveInfos.get(i);
|
||||
set[i] = new ComponentName(info.activityInfo.packageName, info.activityInfo.name);
|
||||
if (info.match > bestMatch) {
|
||||
bestMatch = info.match; // 获取最匹配的值
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 构建你自己的桌面 ComponentName
|
||||
ComponentName myLauncher = new ComponentName(context, launcherActivityClass);
|
||||
|
||||
// 4. 替换首选 Activity(核心步骤)
|
||||
try {
|
||||
// 注意:API 29 (Android 10) 中此方法对第三方应用废弃,但对系统签名应用依然有效
|
||||
pm.replacePreferredActivity(filter, bestMatch, set, myLauncher);
|
||||
Log.i("LauncherHelper", "成功设置为默认桌面");
|
||||
|
||||
// 可选:发送一个回到桌面的 Intent 来验证
|
||||
// Intent startHome = new Intent(Intent.ACTION_MAIN);
|
||||
// startHome.addCategory(Intent.CATEGORY_HOME);
|
||||
// startHome.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
// context.startActivity(startHome);
|
||||
|
||||
} catch (SecurityException e) {
|
||||
Log.e("LauncherHelper", "缺少权限或未生效,请检查系统签名是否正确", e);
|
||||
} catch (Exception e) {
|
||||
Log.e("LauncherHelper", "设置默认桌面失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setDefaultLauncher(Context context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
RoleManager roleManager = (RoleManager) context.getSystemService(Context.ROLE_SERVICE);
|
||||
|
||||
// 检查当前应用是否已经是桌面角色持有者
|
||||
if (roleManager != null && !roleManager.isRoleHeld(RoleManager.ROLE_HOME)) {
|
||||
|
||||
// 检查该角色是否可用
|
||||
if (roleManager.isRoleAvailable(RoleManager.ROLE_HOME)) {
|
||||
// 创建请求意图
|
||||
Intent roleRequestIntent = roleManager.createRequestRoleIntent(RoleManager.ROLE_HOME);
|
||||
// 注意:在普通应用中,这会弹出系统选择框
|
||||
// 在系统签名应用中,这通常能直接提升优先级或简化流程
|
||||
context.startActivity(roleRequestIntent);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Android 10 以下的传统做法:清除当前默认并弹出选择
|
||||
Intent intent = new Intent(Intent.ACTION_MAIN);
|
||||
intent.addCategory(Intent.CATEGORY_HOME);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将指定包名设置为默认桌面 (需要系统签名)
|
||||
*/
|
||||
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.");
|
||||
return;
|
||||
}
|
||||
|
||||
RoleManager roleManager = context.getSystemService(RoleManager.class);
|
||||
if (roleManager == null) return;
|
||||
|
||||
String roleName = RoleManager.ROLE_HOME;
|
||||
|
||||
// 1. 检查是否已经是当前角色持有者,避免重复调用
|
||||
if (roleManager.isRoleHeld(roleName)) {
|
||||
// 注意:这里最好再判断一下持有的包名是否为目标包名
|
||||
Log.i(TAG, "Role " + roleName + " is already held by some package.");
|
||||
// 如果已经是自己,直接返回
|
||||
}
|
||||
|
||||
try {
|
||||
UserHandle user = Process.myUserHandle();
|
||||
Executor executor = context.getMainExecutor();
|
||||
|
||||
// 定义回调
|
||||
Consumer<Boolean> callback = successful -> {
|
||||
if (successful) {
|
||||
Log.i(TAG, "Successfully set " + packageName + " as " + roleName);
|
||||
} else {
|
||||
Log.e(TAG, "Failed to set " + packageName + " as " + roleName + ". Check system logs.");
|
||||
}
|
||||
};
|
||||
|
||||
// 2. 使用反射调用隐藏方法 addRoleHolderAsUser
|
||||
// 方法签名: addRoleHolderAsUser(String, String, int, UserHandle, Executor, Consumer<Boolean>)
|
||||
Method method = RoleManager.class.getMethod("addRoleHolderAsUser",
|
||||
String.class, String.class, int.class, UserHandle.class, Executor.class, Consumer.class);
|
||||
|
||||
Log.d(TAG, "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);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error invoking addRoleHolderAsUser", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setOtherDefaultLauncher(Context context) {
|
||||
PackageManager pm = context.getPackageManager();
|
||||
|
||||
IntentFilter filter = new IntentFilter(Intent.ACTION_MAIN);
|
||||
filter.addCategory(Intent.CATEGORY_HOME);
|
||||
filter.addCategory(Intent.CATEGORY_DEFAULT);
|
||||
|
||||
Intent homeIntent = new Intent(Intent.ACTION_MAIN);
|
||||
homeIntent.addCategory(Intent.CATEGORY_HOME);
|
||||
List<ResolveInfo> resolveInfos = pm.queryIntentActivities(homeIntent, PackageManager.MATCH_DEFAULT_ONLY);
|
||||
Optional<ResolveInfo> systemLauncher = resolveInfos.stream().filter(new Predicate<ResolveInfo>() {
|
||||
@Override
|
||||
public boolean test(ResolveInfo resolveInfo) {
|
||||
return !BuildConfig.APPLICATION_ID.equals(resolveInfo.activityInfo.packageName);
|
||||
}
|
||||
}).filter(new Predicate<ResolveInfo>() {
|
||||
@Override
|
||||
public boolean test(ResolveInfo resolveInfo) {
|
||||
return ApkUtils.isSystemApp(context, resolveInfo.activityInfo.packageName);
|
||||
}
|
||||
}).findAny();
|
||||
systemLauncher.ifPresent(resolveInfo -> addRoleHolderAsUser(context, resolveInfo.activityInfo.packageName));
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统签名应用专用:静默获取全屏截图Bitmap
|
||||
*
|
||||
* @param context 上下文
|
||||
* @return 全屏截图Bitmap,失败返回null
|
||||
*/
|
||||
public static Bitmap takeFullScreenshot(Context context) {
|
||||
// 获取屏幕真实宽高(包含状态栏、导航栏)
|
||||
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
|
||||
DisplayMetrics metrics = new DisplayMetrics();
|
||||
wm.getDefaultDisplay().getRealMetrics(metrics);
|
||||
int screenWidth = metrics.widthPixels;
|
||||
int screenHeight = metrics.heightPixels;
|
||||
|
||||
Log.e(TAG, "takeFullScreenshot: screenWidth " + screenWidth);
|
||||
Log.e(TAG, "takeFullScreenshot: screenHeight " + screenHeight);
|
||||
|
||||
try {
|
||||
// 注意:不同 Android 版本的 SurfaceControl.screenshot 方法签名可能不同
|
||||
// 以下代码适用于 Android 9.0 及以上版本常见的隐藏 API 调用方式
|
||||
|
||||
// 1. 获取屏幕显示的 IBinder (通常为 DisplayControl 或 SurfaceControl 的内部方法)
|
||||
// 在较高版本中,可能需要通过 SurfaceControl.getInternalDisplayToken() 获取
|
||||
|
||||
// 2. 反射调用 screenshot 方法
|
||||
Class<?> surfaceControlClass = Class.forName("android.view.SurfaceControl");
|
||||
|
||||
// Android 10+ 建议使用新的反射路径,这里以通用逻辑为例:
|
||||
// 对于 Android 11+,Google 引入了 SurfaceControl.LayerCaptureArgs 等内部类
|
||||
|
||||
// 这是一个针对 Android 9/10 的简化逻辑参考:
|
||||
Bitmap bitmap = (Bitmap) surfaceControlClass.getDeclaredMethod("screenshot",
|
||||
Rect.class, Integer.TYPE, Integer.TYPE, Integer.TYPE)
|
||||
.invoke(null, new Rect(), screenWidth, screenHeight, 0);
|
||||
|
||||
return bitmap;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
public static Bitmap takeScreenshotHighVersion() {
|
||||
try {
|
||||
// 1. 获取主屏幕的 Token (Internal Display)
|
||||
// 反射调用 SurfaceControl.getInternalDisplayToken()
|
||||
Method getInternalDisplayTokenMethod = SurfaceControl.class.getDeclaredMethod("getInternalDisplayToken");
|
||||
getInternalDisplayTokenMethod.setAccessible(true);
|
||||
IBinder displayToken = (IBinder) getInternalDisplayTokenMethod.invoke(null);
|
||||
|
||||
if (displayToken == null) return null;
|
||||
|
||||
// 2. 构造 DisplayCaptureArgs.Builder (Android 11+ 的新包装类)
|
||||
Class<?> builderClass = Class.forName("android.view.SurfaceControl$DisplayCaptureArgs$Builder");
|
||||
Constructor<?> builderConstructor = builderClass.getConstructor(IBinder.class);
|
||||
Object builder = builderConstructor.newInstance(displayToken);
|
||||
|
||||
// 可以通过 Builder 设置缩放、格式等,这里直接 build()
|
||||
Method buildMethod = builderClass.getDeclaredMethod("build");
|
||||
Object captureArgs = buildMethod.invoke(builder);
|
||||
|
||||
// 3. 调用 SurfaceControl.screenshot(DisplayCaptureArgs)
|
||||
// 返回值是一个 ScreenshotHardwareBuffer 对象
|
||||
Class<?> captureArgsClass = Class.forName("android.view.SurfaceControl$DisplayCaptureArgs");
|
||||
Method screenshotMethod = SurfaceControl.class.getDeclaredMethod("screenshot", captureArgsClass);
|
||||
screenshotMethod.setAccessible(true);
|
||||
|
||||
Object screenshotBuffer = screenshotMethod.invoke(null, captureArgs);
|
||||
|
||||
if (screenshotBuffer == null) return null;
|
||||
|
||||
// 4. 从 ScreenshotHardwareBuffer 中提取 Bitmap
|
||||
Method asBitmapMethod = screenshotBuffer.getClass().getDeclaredMethod("asBitmap");
|
||||
asBitmapMethod.setAccessible(true);
|
||||
return (Bitmap) asBitmapMethod.invoke(screenshotBuffer);
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
230
app/src/main/java/com/ttstd/dialer/view/SettingItem.java
Normal file
230
app/src/main/java/com/ttstd/dialer/view/SettingItem.java
Normal file
@@ -0,0 +1,230 @@
|
||||
package com.ttstd.dialer.view;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
|
||||
|
||||
import com.ttstd.dialer.R;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class SettingItem extends ConstraintLayout {
|
||||
|
||||
private OnClickListener mRootOnClickListener;
|
||||
private ToggleButton.OnToggleChanged mOnToggleChanged;
|
||||
|
||||
private static final String DefaultOptionsText = "设置选项";
|
||||
private static final String DefaultEnableText = "开启描述";
|
||||
private static final String DefaultDisableText = "关闭描述";
|
||||
|
||||
private String mOptionsText = "";
|
||||
private String mEnableText = "";
|
||||
private String mDisableText = "";
|
||||
|
||||
private int mOptionsTextColor = 0xFF000000;
|
||||
private int mHintTextColor = 0xFF9D9D9D;
|
||||
// private boolean mRootClick = false;
|
||||
private boolean mShowHintText = true;
|
||||
private boolean mShowToggle = true;
|
||||
private boolean mShowMore = false;
|
||||
private boolean mLinkage = true;
|
||||
private boolean mShowDivider = true;
|
||||
|
||||
private ConstraintLayout cl_root;
|
||||
private TextView tv_options, tv_hint;
|
||||
private ToggleButton tb;
|
||||
private ImageView iv_more;
|
||||
private View dividerLine;
|
||||
|
||||
public void setRootOnClickListener(OnClickListener rootOnClickListener) {
|
||||
mRootOnClickListener = rootOnClickListener;
|
||||
}
|
||||
|
||||
public void setOnToggleChanged(ToggleButton.OnToggleChanged onToggleChanged) {
|
||||
mOnToggleChanged = onToggleChanged;
|
||||
tb.setOnToggleChanged(mOnToggleChanged);
|
||||
}
|
||||
|
||||
public void setToggleStatu(boolean on) {
|
||||
tb.setToggleStatu(on);
|
||||
if (on) {
|
||||
if (!TextUtils.isEmpty(mEnableText)) {
|
||||
tv_hint.setText(mEnableText);
|
||||
} else {
|
||||
tv_hint.setText(DefaultEnableText);
|
||||
}
|
||||
} else {
|
||||
if (!TextUtils.isEmpty(mDisableText)) {
|
||||
tv_hint.setText(mDisableText);
|
||||
} else {
|
||||
tv_hint.setText(DefaultDisableText);
|
||||
}
|
||||
}
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
public SettingItem(@NonNull @NotNull Context context) {
|
||||
super(context);
|
||||
init(context, null);
|
||||
}
|
||||
|
||||
public SettingItem(@NonNull @NotNull Context context, @Nullable @org.jetbrains.annotations.Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context, attrs);
|
||||
}
|
||||
|
||||
public SettingItem(@NonNull @NotNull Context context, @Nullable @org.jetbrains.annotations.Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init(context, attrs);
|
||||
}
|
||||
|
||||
public SettingItem(@NonNull @NotNull Context context, @Nullable @org.jetbrains.annotations.Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
init(context, attrs);
|
||||
}
|
||||
|
||||
private void init(Context context, AttributeSet attrs) {
|
||||
LayoutInflater inflater = LayoutInflater.from(context);
|
||||
inflater.inflate(R.layout.layout_setting_content_item, this, true);
|
||||
|
||||
cl_root = findViewById(R.id.cl_root);
|
||||
tv_options = findViewById(R.id.tv_options);
|
||||
tv_hint = findViewById(R.id.tv_hint);
|
||||
tb = findViewById(R.id.tb);
|
||||
iv_more = findViewById(R.id.iv_more);
|
||||
dividerLine = findViewById(R.id.divider);
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.SettingItem);
|
||||
try {
|
||||
mOptionsText = typedArray.getString(R.styleable.SettingItem_optionsText);
|
||||
if (!TextUtils.isEmpty(mOptionsText)) {
|
||||
tv_options.setText(mOptionsText);
|
||||
} else {
|
||||
tv_options.setText(DefaultOptionsText);
|
||||
}
|
||||
int optionsTextColor = typedArray.getColor(R.styleable.SettingItem_optionsTextColor, mOptionsTextColor);
|
||||
tv_options.setTextColor(optionsTextColor);
|
||||
|
||||
|
||||
mEnableText = typedArray.getString(R.styleable.SettingItem_enableText);
|
||||
mDisableText = typedArray.getString(R.styleable.SettingItem_disableText);
|
||||
|
||||
int enableTextColor = typedArray.getColor(R.styleable.SettingItem_enableTextColor, mHintTextColor);
|
||||
tv_hint.setTextColor(enableTextColor);
|
||||
|
||||
// boolean rootClick = typedArray.getBoolean(R.styleable.SettingItem_rootClick, mRootClick);
|
||||
// if (rootClick) {
|
||||
// if (mRootOnClickListener != null) {
|
||||
// cl_root.setOnClickListener(mRootOnClickListener);
|
||||
// }
|
||||
// }
|
||||
mShowHintText = typedArray.getBoolean(R.styleable.SettingItem_showHintText, mShowHintText);
|
||||
mShowToggle = typedArray.getBoolean(R.styleable.SettingItem_showToggle, mShowToggle);
|
||||
mShowMore = typedArray.getBoolean(R.styleable.SettingItem_showMore, mShowMore);
|
||||
mLinkage = typedArray.getBoolean(R.styleable.SettingItem_linkage, mLinkage);
|
||||
mShowDivider = typedArray.getBoolean(R.styleable.SettingItem_dividerLine, mShowDivider);
|
||||
} finally {
|
||||
typedArray.recycle();
|
||||
}
|
||||
} else {
|
||||
tv_options.setText(mOptionsText);
|
||||
tv_hint.setText(mEnableText);
|
||||
tv_options.setTextColor(mOptionsTextColor);
|
||||
tv_hint.setTextColor(mHintTextColor);
|
||||
}
|
||||
if (mShowHintText) {
|
||||
tv_hint.setVisibility(VISIBLE);
|
||||
} else {
|
||||
tv_hint.setVisibility(GONE);
|
||||
}
|
||||
if (mShowToggle) {
|
||||
tb.setVisibility(VISIBLE);
|
||||
} else {
|
||||
tb.setVisibility(GONE);
|
||||
}
|
||||
if (mShowMore) {
|
||||
iv_more.setVisibility(VISIBLE);
|
||||
} else {
|
||||
iv_more.setVisibility(GONE);
|
||||
}
|
||||
if (mShowDivider) {
|
||||
dividerLine.setVisibility(VISIBLE);
|
||||
} else {
|
||||
dividerLine.setVisibility(GONE);
|
||||
}
|
||||
|
||||
if (tb.isToggleOn()) {
|
||||
if (!TextUtils.isEmpty(mEnableText)) {
|
||||
tv_hint.setText(mEnableText);
|
||||
} else {
|
||||
tv_hint.setText(DefaultEnableText);
|
||||
}
|
||||
} else {
|
||||
if (!TextUtils.isEmpty(mDisableText)) {
|
||||
tv_hint.setText(mDisableText);
|
||||
} else {
|
||||
tv_hint.setText(DefaultDisableText);
|
||||
}
|
||||
}
|
||||
tb.setOnToggleInsideChanged(new ToggleButton.OnToggleInsideChanged() {
|
||||
@Override
|
||||
public void onToggle(boolean on) {
|
||||
if (on) {
|
||||
if (!TextUtils.isEmpty(mEnableText)) {
|
||||
tv_hint.setText(mEnableText);
|
||||
} else {
|
||||
tv_hint.setText(DefaultEnableText);
|
||||
}
|
||||
} else {
|
||||
if (!TextUtils.isEmpty(mDisableText)) {
|
||||
tv_hint.setText(mDisableText);
|
||||
} else {
|
||||
tv_hint.setText(DefaultDisableText);
|
||||
}
|
||||
}
|
||||
requestLayout();
|
||||
}
|
||||
});
|
||||
if (mRootOnClickListener != null) {
|
||||
cl_root.setOnClickListener(mRootOnClickListener);
|
||||
}
|
||||
if (mLinkage) {
|
||||
cl_root.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (tb.isToggleOn()) {
|
||||
tb.toggleOff();
|
||||
} else {
|
||||
tb.toggleOn();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
389
app/src/main/java/com/ttstd/dialer/view/ToggleButton.java
Normal file
389
app/src/main/java/com/ttstd/dialer/view/ToggleButton.java
Normal file
@@ -0,0 +1,389 @@
|
||||
package com.ttstd.dialer.view;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
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;
|
||||
|
||||
import com.facebook.rebound.SimpleSpringListener;
|
||||
import com.facebook.rebound.Spring;
|
||||
import com.facebook.rebound.SpringConfig;
|
||||
import com.facebook.rebound.SpringSystem;
|
||||
import com.facebook.rebound.SpringUtil;
|
||||
import com.ttstd.dialer.R;
|
||||
|
||||
public class ToggleButton extends View {
|
||||
private static final String TAG = "ToggleButton";
|
||||
private SpringSystem springSystem;
|
||||
private Spring spring;
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private float radius;
|
||||
/**
|
||||
* 开启颜色
|
||||
*/
|
||||
private int onColor = Color.parseColor("#246FFE");
|
||||
/**
|
||||
* 关闭颜色
|
||||
*/
|
||||
private int offBorderColor = Color.parseColor("#c7c7c7");
|
||||
/**
|
||||
* 灰色带颜色
|
||||
*/
|
||||
private int offColor = Color.parseColor("#ffffff");
|
||||
/**
|
||||
* 手柄颜色
|
||||
*/
|
||||
private int spotColor = Color.parseColor("#ffffff");
|
||||
/**
|
||||
* 边框颜色
|
||||
*/
|
||||
private int borderColor = offBorderColor;
|
||||
/**
|
||||
* 画笔
|
||||
*/
|
||||
private Paint paint;
|
||||
/**
|
||||
* 开关状态
|
||||
*/
|
||||
private boolean toggleOn = false;
|
||||
/**
|
||||
* 边框大小
|
||||
*/
|
||||
private int borderWidth = 2;
|
||||
/**
|
||||
* 垂直中心
|
||||
*/
|
||||
private float centerY;
|
||||
/**
|
||||
* 按钮的开始和结束位置
|
||||
*/
|
||||
private float startX, endX;
|
||||
/**
|
||||
* 手柄X位置的最小和最大值
|
||||
*/
|
||||
private float spotMinX, spotMaxX;
|
||||
/**
|
||||
* 手柄大小
|
||||
*/
|
||||
private int spotSize;
|
||||
/**
|
||||
* 手柄X位置
|
||||
*/
|
||||
private float spotX;
|
||||
/**
|
||||
* 关闭时内部灰色带高度
|
||||
*/
|
||||
private float offLineWidth;
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private RectF rect = new RectF();
|
||||
/**
|
||||
* 默认使用动画
|
||||
*/
|
||||
private boolean defaultAnimate = true;
|
||||
/**
|
||||
* 是否默认处于打开状态
|
||||
*/
|
||||
private boolean isDefaultOn = false;
|
||||
/**
|
||||
* 禁止点击
|
||||
*/
|
||||
private boolean disable = false;
|
||||
|
||||
private OnToggleChanged listener;
|
||||
|
||||
public void setDisable(boolean dis) {
|
||||
this.disable = dis;
|
||||
}
|
||||
|
||||
private ToggleButton(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public ToggleButton(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
setup(attrs);
|
||||
}
|
||||
|
||||
public ToggleButton(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
setup(attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
spring.removeListener(springListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
spring.addListener(springListener);
|
||||
}
|
||||
|
||||
public void setup(AttributeSet attrs) {
|
||||
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
paint.setStrokeCap(Paint.Cap.ROUND);
|
||||
springSystem = SpringSystem.create();
|
||||
spring = springSystem.createSpring();
|
||||
//张力(tension),摩擦力(friction)
|
||||
//增大张力会使弹簧更快地向目标值运动,减小摩擦力会减少弹簧运动过程中的阻力,从而使回弹更加迅速和有力。
|
||||
spring.setSpringConfig(SpringConfig.fromOrigamiTensionAndFriction(80, 10));
|
||||
this.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View arg0) {
|
||||
if (disable) {
|
||||
|
||||
} else {
|
||||
toggle(defaultAnimate);
|
||||
}
|
||||
}
|
||||
});
|
||||
TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.ToggleButton);
|
||||
offBorderColor = typedArray.getColor(R.styleable.ToggleButton_tbOffBorderColor, offBorderColor);
|
||||
onColor = typedArray.getColor(R.styleable.ToggleButton_tbOnColor, onColor);
|
||||
spotColor = typedArray.getColor(R.styleable.ToggleButton_tbSpotColor, spotColor);
|
||||
offColor = typedArray.getColor(R.styleable.ToggleButton_tbOffColor, offColor);
|
||||
borderWidth = typedArray.getDimensionPixelSize(R.styleable.ToggleButton_tbBorderWidth, borderWidth);
|
||||
defaultAnimate = typedArray.getBoolean(R.styleable.ToggleButton_tbAnimate, defaultAnimate);
|
||||
isDefaultOn = typedArray.getBoolean(R.styleable.ToggleButton_tbAsDefaultOn, isDefaultOn);
|
||||
typedArray.recycle();
|
||||
borderColor = offBorderColor;
|
||||
if (isDefaultOn) {
|
||||
toggleOn();
|
||||
}
|
||||
}
|
||||
|
||||
public void toggle() {
|
||||
toggle(true);
|
||||
}
|
||||
|
||||
public void toggle(boolean animate) {
|
||||
toggleOn = !toggleOn;
|
||||
Log.e(TAG, "toggle: toggleOn = " + toggleOn);
|
||||
takeEffect(animate);
|
||||
if (listener != null) {
|
||||
listener.onToggle(toggleOn);
|
||||
}
|
||||
if (mOnToggleInsideChanged != null) {
|
||||
mOnToggleInsideChanged.onToggle(toggleOn);
|
||||
}
|
||||
}
|
||||
|
||||
public void toggleOn() {
|
||||
setToggleOn();
|
||||
if (listener != null) {
|
||||
listener.onToggle(toggleOn);
|
||||
}
|
||||
if (mOnToggleInsideChanged != null) {
|
||||
mOnToggleInsideChanged.onToggle(toggleOn);
|
||||
}
|
||||
}
|
||||
|
||||
public void toggleOff() {
|
||||
setToggleOff();
|
||||
if (listener != null) {
|
||||
listener.onToggle(toggleOn);
|
||||
}
|
||||
if (mOnToggleInsideChanged != null) {
|
||||
mOnToggleInsideChanged.onToggle(toggleOn);
|
||||
}
|
||||
}
|
||||
|
||||
public void setToggleStatu(boolean on) {
|
||||
if (on) {
|
||||
setToggleOn();
|
||||
} else {
|
||||
setToggleOff();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置显示成打开样式,不会触发toggle事件
|
||||
*/
|
||||
public void setToggleOn() {
|
||||
setToggleOn(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param animate asd
|
||||
*/
|
||||
public void setToggleOn(boolean animate) {
|
||||
toggleOn = true;
|
||||
takeEffect(animate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置显示成关闭样式,不会触发toggle事件
|
||||
*/
|
||||
public void setToggleOff() {
|
||||
setToggleOff(true);
|
||||
}
|
||||
|
||||
public void setToggleOff(boolean animate) {
|
||||
toggleOn = false;
|
||||
takeEffect(animate);
|
||||
}
|
||||
|
||||
|
||||
public int getToggleOnStatu() {
|
||||
Log.e(TAG, "getToggleOnStatu: " + toggleOn);
|
||||
return toggleOn ? 1 : 0;
|
||||
}
|
||||
|
||||
public boolean isToggleOn() {
|
||||
Log.e(TAG, "isToggleOn: " + toggleOn);
|
||||
return toggleOn;
|
||||
}
|
||||
|
||||
private void takeEffect(boolean animate) {
|
||||
if (animate) {
|
||||
spring.setEndValue(toggleOn ? 1 : 0);
|
||||
} else {
|
||||
//这里没有调用spring,所以spring里的当前值没有变更,这里要设置一下,同步两边的当前值
|
||||
spring.setCurrentValue(toggleOn ? 1 : 0);
|
||||
calculateEffect(toggleOn ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
|
||||
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
|
||||
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
|
||||
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
|
||||
Resources r = Resources.getSystem();
|
||||
if (widthMode == MeasureSpec.UNSPECIFIED || widthMode == MeasureSpec.AT_MOST) {
|
||||
widthSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50, r.getDisplayMetrics());
|
||||
widthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
|
||||
}
|
||||
if (heightMode == MeasureSpec.UNSPECIFIED || heightSize == MeasureSpec.AT_MOST) {
|
||||
heightSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 30, r.getDisplayMetrics());
|
||||
heightMeasureSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);
|
||||
}
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right,
|
||||
int bottom) {
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
final int width = getWidth();
|
||||
final int height = getHeight();
|
||||
radius = Math.min(width, height) * 0.5f;
|
||||
centerY = radius;
|
||||
startX = radius;
|
||||
endX = width - radius;
|
||||
spotMinX = startX + borderWidth;
|
||||
spotMaxX = endX - borderWidth;
|
||||
spotSize = height - 4 * borderWidth;
|
||||
spotX = toggleOn ? spotMaxX : spotMinX;
|
||||
offLineWidth = 0;
|
||||
}
|
||||
|
||||
SimpleSpringListener springListener = new SimpleSpringListener() {
|
||||
@Override
|
||||
public void onSpringUpdate(Spring spring) {
|
||||
final double value = spring.getCurrentValue();
|
||||
calculateEffect(value);
|
||||
}
|
||||
};
|
||||
|
||||
private int clamp(int value, int low, int high) {
|
||||
return Math.min(Math.max(value, low), high);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
//
|
||||
super.draw(canvas);
|
||||
rect.set(0, 0, getWidth(), getHeight());
|
||||
paint.setColor(borderColor);
|
||||
canvas.drawRoundRect(rect, radius, radius, paint);
|
||||
if (offLineWidth > 0) {
|
||||
final float cy = offLineWidth * 0.5f;
|
||||
rect.set(spotX - cy, centerY - cy, endX + cy, centerY + cy);
|
||||
// paint.setColor(offColor);
|
||||
canvas.drawRoundRect(rect, cy, cy, paint);
|
||||
}
|
||||
rect.set(spotX - 1 - radius, centerY - radius, spotX + 1.1f + radius, centerY + radius);
|
||||
paint.setColor(borderColor);
|
||||
canvas.drawRoundRect(rect, radius, radius, paint);
|
||||
final float spotR = spotSize * 0.5f;
|
||||
rect.set(spotX - spotR, centerY - spotR, spotX + spotR, centerY + spotR);
|
||||
paint.setColor(spotColor);
|
||||
canvas.drawRoundRect(rect, spotR, spotR, paint);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param value
|
||||
*/
|
||||
private void calculateEffect(final double value) {
|
||||
final float mapToggleX = (float) SpringUtil.mapValueFromRangeToRange(value, 0, 1, spotMinX, spotMaxX);
|
||||
spotX = mapToggleX;
|
||||
float mapOffLineWidth = (float) SpringUtil.mapValueFromRangeToRange(1 - value, 0, 1, 10, spotSize);
|
||||
offLineWidth = mapOffLineWidth;
|
||||
final int fb = Color.blue(onColor);
|
||||
final int fr = Color.red(onColor);
|
||||
final int fg = Color.green(onColor);
|
||||
final int tb = Color.blue(offBorderColor);
|
||||
final int tr = Color.red(offBorderColor);
|
||||
final int tg = Color.green(offBorderColor);
|
||||
int sb = (int) SpringUtil.mapValueFromRangeToRange(1 - value, 0, 1, fb, tb);
|
||||
int sr = (int) SpringUtil.mapValueFromRangeToRange(1 - value, 0, 1, fr, tr);
|
||||
int sg = (int) SpringUtil.mapValueFromRangeToRange(1 - value, 0, 1, fg, tg);
|
||||
sb = clamp(sb, 0, 255);
|
||||
sr = clamp(sr, 0, 255);
|
||||
sg = clamp(sg, 0, 255);
|
||||
borderColor = Color.rgb(sr, sg, sb);
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
/**
|
||||
* @author ThinkPad
|
||||
*/
|
||||
public interface OnToggleChanged {
|
||||
/**
|
||||
* @param on = =
|
||||
*/
|
||||
public void onToggle(boolean on);
|
||||
}
|
||||
|
||||
public interface OnToggleInsideChanged {
|
||||
/**
|
||||
* @param on = =
|
||||
*/
|
||||
void onToggle(boolean on);
|
||||
}
|
||||
|
||||
private OnToggleInsideChanged mOnToggleInsideChanged;
|
||||
|
||||
public void setOnToggleInsideChanged(OnToggleInsideChanged onToggleChanged) {
|
||||
mOnToggleInsideChanged = onToggleChanged;
|
||||
}
|
||||
|
||||
public void setOnToggleChanged(OnToggleChanged onToggleChanged) {
|
||||
listener = onToggleChanged;
|
||||
}
|
||||
|
||||
public boolean isAnimate() {
|
||||
return defaultAnimate;
|
||||
}
|
||||
|
||||
public void setAnimate(boolean animate) {
|
||||
this.defaultAnimate = animate;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user