优化天气,增加悬浮窗,增加快捷拨号,增加设置默认桌面

This commit is contained in:
2026-03-28 00:56:54 +08:00
parent 1977fd1cb2
commit b0ea6eff0a
74 changed files with 4055 additions and 242 deletions

View File

@@ -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();
}
}
}

View File

@@ -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();

View File

@@ -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();
}
}
}

View File

@@ -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));
}

View File

@@ -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

View File

@@ -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();
}
}
}

View File

@@ -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> {
}

View File

@@ -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));
}
}
}

View File

@@ -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> {
}

View File

@@ -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);
}
}
}
}

View File

@@ -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> {
}

View File

@@ -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();
}
}
}

View File

@@ -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> {
}

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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";
/*地址改变时发送*/

View File

@@ -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());

View File

@@ -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() {

View File

@@ -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;
}

View File

@@ -34,6 +34,9 @@ public class PermissionDialogFragment extends BaseDialogFragment {
private DialogFragmentPermissionsBinding mBinding;
private String mContent;
public PermissionDialogFragment() {
}
public PermissionDialogFragment(String content) {
this.mContent = content;
}

View File

@@ -35,6 +35,9 @@ public class ShortcutDialogFagment extends BaseMvvmDialogFragment<ShortcutViewMo
private String mNegativeText;
private String mPositiveText;
public ShortcutDialogFagment() {
}
public ShortcutDialogFagment(AppInfo appInfo) {
mAppInfo = appInfo;
}

View File

@@ -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) {

View File

@@ -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;
}
}

View File

@@ -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.498.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;
}
}
//根据节点信息可获得对应的xy坐标
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;
}
//实现对xy坐标进行点击操作。
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; // 屏幕密度dpi120 / 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());
}
}
}

View 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);
}
}

View File

@@ -0,0 +1,4 @@
package com.ttstd.dialer.utils;
public class BitmapUtils {
}

View File

@@ -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;
}
}

View 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);
}
}

View 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;
}
}