diff --git a/app/build.gradle b/app/build.gradle index 860bb0a..1c2fc3a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -181,6 +181,8 @@ ext { dependencies { // implementation fileTree(dir: 'libs', include: ['*.jar']) + compileOnly files('libs/framework.jar') + //Android 4.4+ implementation files('libs/QWeather_Public_Android_V5.1.2.jar') implementation 'net.i2p.crypto:eddsa:0.3.0' @@ -333,3 +335,29 @@ dependencies { // implementation 'me.jessyan:autosize:1.2.1' } + + +preBuild { + doLast { + def imlFile = file(project.name + ".iml") +// def imlFile = file("..\\.idea\\modules\\" + project.name + "\\" + rootProject.name + "." + project.name + ".iml") + println 'Change ' + project.name + '.iml order' + try { + def parsedXml = (new XmlParser()).parse(imlFile) + def jdkNode = parsedXml.component[1].orderEntry.find { it.'@type' == 'jdk' } + parsedXml.component[1].remove(jdkNode) + def sdkString = "Android API " + android.compileSdkVersion.substring("android-".length()) + " Platform" + println 'what' + sdkString + new Node(parsedXml.component[1], 'orderEntry', ['type': 'jdk', 'jdkName': sdkString, 'jdkType': 'Android SDK']) + groovy.xml.XmlUtil.serialize(parsedXml, new FileOutputStream(imlFile)) + } catch (FileNotFoundException e) { + // nop, iml not found + println "no iml found" + } + } + //https://www.pianshen.com/article/93481144911/ + //https://blog.csdn.net/dhl_1986/article/details/102856026 + //https://blog.csdn.net/zhonghe1114/article/details/80923730 + //https://blog.csdn.net/u014175785/article/details/116235760 + //使用系统编译后的framework.jar +} \ No newline at end of file diff --git a/app/libs/framework.jar b/app/libs/framework.jar new file mode 100644 index 0000000..fefe7ec Binary files /dev/null and b/app/libs/framework.jar differ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9ca73d3..2874ff8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,9 +6,16 @@ + - + + + + + + + @@ -43,11 +50,14 @@ android:name=".activity.main.MainActivity" android:exported="true" android:launchMode="singleTask" + android:excludeFromRecents="true" android:screenOrientation="portrait" android:theme="@style/AppThemeFitsSystemWindows"> + + @@ -67,10 +77,28 @@ android:name=".activity.app.AppListActivity" android:launchMode="singleTask" android:screenOrientation="portrait" /> - + + + + + + { + +} diff --git a/app/src/main/java/com/ttstd/dialer/activity/settings/home/SettingsActivity.java b/app/src/main/java/com/ttstd/dialer/activity/settings/home/SettingsActivity.java new file mode 100644 index 0000000..6f74ad9 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/activity/settings/home/SettingsActivity.java @@ -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 { + 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)); + } + } +} diff --git a/app/src/main/java/com/ttstd/dialer/activity/settings/home/SettingsViewModel.java b/app/src/main/java/com/ttstd/dialer/activity/settings/home/SettingsViewModel.java new file mode 100644 index 0000000..d741965 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/activity/settings/home/SettingsViewModel.java @@ -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 { + +} diff --git a/app/src/main/java/com/ttstd/dialer/activity/settings/utils/SettingsUtilsActivity.java b/app/src/main/java/com/ttstd/dialer/activity/settings/utils/SettingsUtilsActivity.java new file mode 100644 index 0000000..60bae6c --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/activity/settings/utils/SettingsUtilsActivity.java @@ -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 { + 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); + } + } + } +} diff --git a/app/src/main/java/com/ttstd/dialer/activity/settings/utils/SettingsUtilsViewModel.java b/app/src/main/java/com/ttstd/dialer/activity/settings/utils/SettingsUtilsViewModel.java new file mode 100644 index 0000000..2a97966 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/activity/settings/utils/SettingsUtilsViewModel.java @@ -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 { + +} diff --git a/app/src/main/java/com/ttstd/dialer/activity/template/TemplateActivity.java b/app/src/main/java/com/ttstd/dialer/activity/template/TemplateActivity.java new file mode 100644 index 0000000..83acdea --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/activity/template/TemplateActivity.java @@ -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 { + 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(); + } + } +} diff --git a/app/src/main/java/com/ttstd/dialer/activity/template/TemplateViewModel.java b/app/src/main/java/com/ttstd/dialer/activity/template/TemplateViewModel.java new file mode 100644 index 0000000..33a392d --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/activity/template/TemplateViewModel.java @@ -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 { + +} diff --git a/app/src/main/java/com/ttstd/dialer/activity/weather/main/WeatherMainActivity.java b/app/src/main/java/com/ttstd/dialer/activity/weather/main/WeatherMainActivity.java index 24d0099..5768c53 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/weather/main/WeatherMainActivity.java +++ b/app/src/main/java/com/ttstd/dialer/activity/weather/main/WeatherMainActivity.java @@ -35,6 +35,12 @@ public class WeatherMainActivity extends BaseMvvmActivity { private static final String TAG = "WeatherAdapter"; @@ -25,6 +32,8 @@ public class WeatherAdapter extends RecyclerView.Adapter mWeatherDailyList; + private RequestBuilder requestBuilder; + public void setWeatherDailyList(List weatherDailyList) { mWeatherDailyList = weatherDailyList; @@ -34,31 +43,54 @@ public class WeatherAdapter extends RecyclerView.Adapter appInfos) { mAppInfos = appInfos; Log.e(TAG, "AppFragment: mAppInfos = " + mAppInfos.hashCode()); diff --git a/app/src/main/java/com/ttstd/dialer/fragment/contact/ContactFragment.java b/app/src/main/java/com/ttstd/dialer/fragment/contact/ContactFragment.java index 2fbb7d4..06ff019 100644 --- a/app/src/main/java/com/ttstd/dialer/fragment/contact/ContactFragment.java +++ b/app/src/main/java/com/ttstd/dialer/fragment/contact/ContactFragment.java @@ -26,6 +26,8 @@ public class ContactFragment extends BaseMvvmFragment packages) { + + } + + @Override + public List getInstallPackageTrustList() { + return null; + } +} diff --git a/app/src/main/java/com/ttstd/dialer/service/DialerAccessibilityService.java b/app/src/main/java/com/ttstd/dialer/service/DialerAccessibilityService.java index 739c60c..f4dc589 100644 --- a/app/src/main/java/com/ttstd/dialer/service/DialerAccessibilityService.java +++ b/app/src/main/java/com/ttstd/dialer/service/DialerAccessibilityService.java @@ -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() { + @Override + public void subscribe(@NonNull ObservableEmitter emitter) throws Throwable { + mAccessibilityEventCallback = emitter::onNext; + } + }).throttleLast(WAIT_TIME, TimeUnit.MILLISECONDS) + .subscribe(new Consumer() { + @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 nodeInfos = findNodesByViewId(text); + Optional optional = nodeInfos.stream().findAny(); + if (optional.isPresent()) { + AccessibilityNodeInfo node = optional.get(); + if (node.isClickable()) { + boolean performAction = node.performAction(AccessibilityNodeInfo.ACTION_CLICK); + Log.e(TAG, "clickNode: performAction = " + performAction); + 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 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 nodeInfos = findNodesByViewId("com.tencent.mm:id/gzf"); + Log.e(TAG, "findSearchContact: " + nodeInfos); + Optional optional = nodeInfos.stream().findAny(); + return optional.isPresent(); + } + + private void findSearchContact(Property type, String text, Step nextStep) { + List nodeInfos = findNodesByViewId("com.tencent.mm:id/odf"); + Log.e(TAG, "findSearchContact: " + nodeInfos); + Optional 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 nodeInfos = findNodesByViewId("com.tencent.mm:id/d98"); + Optional 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 nodeInfos = findNodesByViewId(id); + Optional 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 findNodesByViewId(String id) { + AccessibilityNodeInfo nodeInfo = getRootInActiveWindow(); + if (nodeInfo != null) { + List accessibilityNodeInfos = nodeInfo.findAccessibilityNodeInfosByViewId(id); + return accessibilityNodeInfos; + } else { + return new ArrayList<>(); + } + } + + private AccessibilityNodeInfo findNodeByText(AccessibilityNodeInfo root, String text) { + if (root == null) return null; + Log.e(TAG, "findNodeByText: getText = " + root.getText()); + Log.e(TAG, "findNodeByText: getContentDescription = " + root.getContentDescription()); + boolean found = root.getText() != null && text.contentEquals(root.getText()); + if (found) { + return root; + } else { + for (int i = 0; i < root.getChildCount(); i++) { + AccessibilityNodeInfo result = findNodeByText(root.getChild(i), text); + if (result != null) { + return result; + } + } + } + root.recycle(); + return null; + } + + private boolean stepCallDialog(Property type, String text, Step nextStep) { + AccessibilityNodeInfo node = findNode(getRootInActiveWindow(), type, text); + if (node != null) { + Log.e(TAG, "stepCallDialog: isVisibleToUser: " + node.isVisibleToUser()); + if (node.isVisibleToUser()) { + clickNode(node); + Log.e(TAG, "stepCallDialog: mCurrentStep: " + mCurrentStep + " done"); + mCurrentStep = nextStep; + Log.e(TAG, "stepCallDialog: next: " + mCurrentStep); + return true; + } else { + scrollDown(); + return false; + } + } else { + if (mFindCount == mMaxCount) { + Log.e("stepCallDialog", "mCurrentStep: max"); + ToastUtils.showShort("没有找到联系人"); + mCurrentStep = Step.WAITING; + mFindCount = 0; + return false; + } else { + Log.e("stepCallDialog", "mCurrentStep: not found"); + mFindCount++; + Log.e("stepCallDialog", "mCurrentStep: mFindCount = " + mFindCount); + scrollDown(); + return false; + } + } + } + + private boolean stepHome(Property type, String text) { + AccessibilityNodeInfo node = findNode(getRootInActiveWindow(), type, text); + if (node != null) { + clickNode(node); + Log.e(TAG, "stepHome: mCurrentStep: " + mCurrentStep + " done"); + mCurrentStep = Step.CLICK_QUICK_WECHAT_CALL; + Log.e(TAG, "stepHome: next: " + mCurrentStep); + return true; + } else { + mCurrentStep = Step.CLICK_SEARCH; + return false; + } + } + + private int mFindCount = 0; + private int mMaxCount = 5; + + + private boolean findContact(Property type, String text, Step nextStep) { + AccessibilityNodeInfo node = findNode(getRootInActiveWindow(), type, text); + if (node != null) { + clickNode(node); + Log.e("findContact", "mCurrentStep: " + mCurrentStep + " done"); + mCurrentStep = nextStep; + Log.e("findContact", "next: " + mCurrentStep); + mFindCount = 0; + return true; + } else { + if (mFindCount == mMaxCount) { + Log.e("findContact", "mCurrentStep: max"); + ToastUtils.showShort("没有找到联系人"); + mCurrentStep = Step.WAITING; + mFindCount = 0; + return false; + } else { + Log.e("findContact", "mCurrentStep: not found"); + mFindCount++; + Log.e("findContact", "mCurrentStep: mFindCount = " + mFindCount); + scrollDown(); + return false; + } + } + } + + + private AccessibilityNodeInfo findNode(AccessibilityNodeInfo root, Property type, String text) { + if (root == null) return null; +// Log.v(TAG, "findNode: getPackageName = " + root.getPackageName()); + Log.v(TAG, "findNode: getText = " + root.getText()); + Log.v(TAG, "findNode: getClassName = " + root.getClassName()); + Log.v(TAG, "findNode: getContentDescription = " + root.getContentDescription()); + boolean satisfied = false; + switch (type) { + case TEXT: + satisfied = root.getText() != null && text.contentEquals(root.getText()); + break; + case CLASS_NAME: + satisfied = root.getClassName() != null && text.contentEquals(root.getClassName()); + break; + case DESCRIPTION: + satisfied = root.getContentDescription() != null && text.contentEquals(root.getContentDescription()); + break; + default: + } + if (satisfied) { + return root; + } else { + for (int i = 0; i < root.getChildCount(); i++) { + AccessibilityNodeInfo result = findNode(root.getChild(i), type, text); + if (result != null) { + return result; + } + } + } + root.recycle(); + return null; + } + + private AccessibilityNodeInfo findNode(List windows, Property type, String text) { + for (AccessibilityWindowInfo accessibilityWindowInfo : windows) { + AccessibilityNodeInfo nodeInfo = findNode(accessibilityWindowInfo.getRoot(), type, text); + if (nodeInfo != null) { + return nodeInfo; + } + } + Log.e(TAG, "findNode windows: not found"); + return null; + } + + + private void clickNode(AccessibilityNodeInfo node) { + if (node == null) { + Log.e(TAG, "clickNode: node is null"); + return; + } + try { + Log.e(TAG, "clickNode: getText = " + node.getText()); + Log.e(TAG, "clickNode: isClickable = " + node.isClickable()); + } catch (Exception e) { + Log.e(TAG, "clickNode: e = " + e.getMessage()); + } + if (node.isClickable()) { + //防检测机制: + //添加随机延迟(避免高频操作) +// handler.postDelayed(new Runnable() { +// @Override +// public void run() { +// +// } +// }, 1000 + new Random().nextInt(100)); + boolean performAction = node.performAction(AccessibilityNodeInfo.ACTION_CLICK); + Log.e(TAG, "clickNode: performAction = " + performAction); + if (!performAction) { + Rect rect = new Rect(); + node.getBoundsInScreen(rect); + Log.e(TAG, "clickNode: rect = " + rect); + // 点击节点的中心位置 + int centerX = (rect.left + rect.right) / 2; + int centerY = (rect.top + rect.bottom) / 2; + Log.e(TAG, "clickNode: clickByNode = " + clickByPoint(centerX, centerY)); + } + node.recycle(); + } else { + Rect rect = new Rect(); + node.getBoundsInScreen(rect); + Log.e(TAG, "clickNode: rect = " + rect); + // 点击节点的中心位置 + int centerX = (rect.left + rect.right) / 2; + int centerY = (rect.top + rect.bottom) / 2; + Log.e(TAG, "clickNode: clickByNode = " + clickByPoint(centerX, centerY)); + } +// else { +// AccessibilityNodeInfo parent = node.getParent(); +// node.recycle(); +// clickNode(parent); +// } + } + + @Deprecated + private boolean stepCall(Property type, String text) { + AccessibilityNodeInfo node = findNode(getRootInActiveWindow(), type, text); + 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 nodeInfos = findNodesByViewId("com.tencent.mm:id/a12"); + Optional accessibilityNodeInfo = nodeInfos.stream().findAny(); + if (accessibilityNodeInfo.isPresent()) { + AccessibilityNodeInfo nodeInfo = accessibilityNodeInfo.get(); + clickNode(nodeInfo); + mCurrentStep = Step.CLICK_CALL; + } 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()); } } } diff --git a/app/src/main/java/com/ttstd/dialer/service/main/MainService.java b/app/src/main/java/com/ttstd/dialer/service/main/MainService.java new file mode 100644 index 0000000..143c16d --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/service/main/MainService.java @@ -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 mWhiteRunningAppSets = new HashSet() {{ + 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 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); + } +} diff --git a/app/src/main/java/com/ttstd/dialer/utils/BitmapUtils.java b/app/src/main/java/com/ttstd/dialer/utils/BitmapUtils.java new file mode 100644 index 0000000..4384360 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/utils/BitmapUtils.java @@ -0,0 +1,4 @@ +package com.ttstd.dialer.utils; + +public class BitmapUtils { +} diff --git a/app/src/main/java/com/ttstd/dialer/utils/SystemUtils.java b/app/src/main/java/com/ttstd/dialer/utils/SystemUtils.java index af9a545..dae14a0 100644 --- a/app/src/main/java/com/ttstd/dialer/utils/SystemUtils.java +++ b/app/src/main/java/com/ttstd/dialer/utils/SystemUtils.java @@ -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 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 getRunningTaskPackages(Context context) { + ActivityManager activityManager = (ActivityManager) context.getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE); + List runningTaskInfos = activityManager.getRunningTasks(Integer.MAX_VALUE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return runningTaskInfos.stream().map(new Function() { + @Override + public String apply(ActivityManager.RunningTaskInfo runningTaskInfo) { + return runningTaskInfo.topActivity.getPackageName(); + } + }).collect(Collectors.toList()); + } else { + List 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 list = getRecentTasks(ActivityManager.getMaxRecentTasksStatic(), getCurrentUserId()); + HashMap 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 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 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 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) + 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 resolveInfos = pm.queryIntentActivities(homeIntent, PackageManager.MATCH_DEFAULT_ONLY); + Optional systemLauncher = resolveInfos.stream().filter(new Predicate() { + @Override + public boolean test(ResolveInfo resolveInfo) { + return !BuildConfig.APPLICATION_ID.equals(resolveInfo.activityInfo.packageName); + } + }).filter(new Predicate() { + @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; + } + } diff --git a/app/src/main/java/com/ttstd/dialer/view/SettingItem.java b/app/src/main/java/com/ttstd/dialer/view/SettingItem.java new file mode 100644 index 0000000..8169597 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/view/SettingItem.java @@ -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); + + } +} + diff --git a/app/src/main/java/com/ttstd/dialer/view/ToggleButton.java b/app/src/main/java/com/ttstd/dialer/view/ToggleButton.java new file mode 100644 index 0000000..64a1526 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/view/ToggleButton.java @@ -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; + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/icon_more.png b/app/src/main/res/drawable-hdpi/icon_more.png new file mode 100644 index 0000000..e653dcf Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icon_more.png differ diff --git a/app/src/main/res/drawable-xhdpi/not_applicable.png b/app/src/main/res/drawable-xhdpi/not_applicable.png deleted file mode 100644 index 4ce8569..0000000 Binary files a/app/src/main/res/drawable-xhdpi/not_applicable.png and /dev/null differ diff --git a/app/src/main/res/drawable/call_phone_selector.xml b/app/src/main/res/drawable/call_phone_selector.xml new file mode 100644 index 0000000..ea6fea9 --- /dev/null +++ b/app/src/main/res/drawable/call_phone_selector.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/contact_edit_text_focused.xml b/app/src/main/res/drawable/contact_edit_text_focused.xml new file mode 100644 index 0000000..d00f075 --- /dev/null +++ b/app/src/main/res/drawable/contact_edit_text_focused.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/contact_edit_text_normal.xml b/app/src/main/res/drawable/contact_edit_text_normal.xml new file mode 100644 index 0000000..4fc7f83 --- /dev/null +++ b/app/src/main/res/drawable/contact_edit_text_normal.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/contact_edit_text_pressed.xml b/app/src/main/res/drawable/contact_edit_text_pressed.xml new file mode 100644 index 0000000..7781267 --- /dev/null +++ b/app/src/main/res/drawable/contact_edit_text_pressed.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/contact_edit_text_selector.xml b/app/src/main/res/drawable/contact_edit_text_selector.xml new file mode 100644 index 0000000..64a9fef --- /dev/null +++ b/app/src/main/res/drawable/contact_edit_text_selector.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/default_negative_background.xml b/app/src/main/res/drawable/default_negative_background.xml index aba0358..d7e663a 100644 --- a/app/src/main/res/drawable/default_negative_background.xml +++ b/app/src/main/res/drawable/default_negative_background.xml @@ -3,11 +3,11 @@ - + + android:bottom="@dimen/dp_4" + android:left="@dimen/dp_20" + android:right="@dimen/dp_20" + android:top="@dimen/dp_4" /> \ No newline at end of file diff --git a/app/src/main/res/drawable/default_positive_background.xml b/app/src/main/res/drawable/default_positive_background.xml index 3f8e31c..bcec8fc 100644 --- a/app/src/main/res/drawable/default_positive_background.xml +++ b/app/src/main/res/drawable/default_positive_background.xml @@ -3,11 +3,11 @@ - + + android:bottom="@dimen/dp_4" + android:left="@dimen/dp_20" + android:right="@dimen/dp_20" + android:top="@dimen/dp_4" /> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_cancel.xml b/app/src/main/res/drawable/ic_cancel.xml new file mode 100644 index 0000000..4cdb27e --- /dev/null +++ b/app/src/main/res/drawable/ic_cancel.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_confirm.xml b/app/src/main/res/drawable/ic_confirm.xml new file mode 100644 index 0000000..1984bad --- /dev/null +++ b/app/src/main/res/drawable/ic_confirm.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_default_avatar.xml b/app/src/main/res/drawable/ic_default_avatar.xml index 7a09e7b..7db8344 100644 --- a/app/src/main/res/drawable/ic_default_avatar.xml +++ b/app/src/main/res/drawable/ic_default_avatar.xml @@ -4,9 +4,6 @@ android:viewportWidth="1024" android:viewportHeight="1024"> - + android:pathData="M512,64C264.8,64 64,264.8 64,512s200.8,448 448,448 448,-200.8 448,-448S759.2,64 512,64zM384.8,376c4,-64 56,-115.2 120,-119.2 74.4,-4 135.2,55.2 135.2,128 0,70.4 -57.6,128 -128,128 -73.6,0 -132,-62.4 -127.2,-136.8zM768,746.4c0,12 -9.6,21.6 -21.6,21.6H278.4c-12,0 -21.6,-9.6 -21.6,-21.6v-64c0,-84.8 170.4,-128 255.2,-128 84.8,0 255.2,42.4 255.2,128l0.8,64z" + android:fillColor="#cdcdcd"/> diff --git a/app/src/main/res/drawable/ic_dialer.xml b/app/src/main/res/drawable/ic_dialer.xml new file mode 100644 index 0000000..0fe8edc --- /dev/null +++ b/app/src/main/res/drawable/ic_dialer.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_home_float_window.xml b/app/src/main/res/drawable/ic_home_float_window.xml new file mode 100644 index 0000000..e0f1065 --- /dev/null +++ b/app/src/main/res/drawable/ic_home_float_window.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_not_applicable.xml b/app/src/main/res/drawable/ic_not_applicable.xml new file mode 100644 index 0000000..0718ee7 --- /dev/null +++ b/app/src/main/res/drawable/ic_not_applicable.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_return.xml b/app/src/main/res/drawable/ic_return.xml new file mode 100644 index 0000000..cea2b37 --- /dev/null +++ b/app/src/main/res/drawable/ic_return.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/settings_card_bg.xml b/app/src/main/res/drawable/settings_card_bg.xml new file mode 100644 index 0000000..00259f8 --- /dev/null +++ b/app/src/main/res/drawable/settings_card_bg.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/weather_card_background.xml b/app/src/main/res/drawable/weather_card_background.xml new file mode 100644 index 0000000..5d72803 --- /dev/null +++ b/app/src/main/res/drawable/weather_card_background.xml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/wechat_call_normal.xml b/app/src/main/res/drawable/wechat_call_normal.xml new file mode 100644 index 0000000..13e0896 --- /dev/null +++ b/app/src/main/res/drawable/wechat_call_normal.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/wechat_call_video.xml b/app/src/main/res/drawable/wechat_call_video.xml index 7d13fb6..ee41800 100644 --- a/app/src/main/res/drawable/wechat_call_video.xml +++ b/app/src/main/res/drawable/wechat_call_video.xml @@ -4,7 +4,7 @@ - + diff --git a/app/src/main/res/layout/activity_app_list.xml b/app/src/main/res/layout/activity_app_list.xml index a6ca4f4..ae94b40 100644 --- a/app/src/main/res/layout/activity_app_list.xml +++ b/app/src/main/res/layout/activity_app_list.xml @@ -5,6 +5,7 @@ tools:context=".activity.app.AppListActivity"> + @@ -14,10 +15,46 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + + + + + + + + + + android:layout_height="0dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintTop_toBottomOf="@+id/bar" /> diff --git a/app/src/main/res/layout/activity_contact_add.xml b/app/src/main/res/layout/activity_contact_add.xml index 94655b6..88b2aac 100644 --- a/app/src/main/res/layout/activity_contact_add.xml +++ b/app/src/main/res/layout/activity_contact_add.xml @@ -15,27 +15,77 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + + + + + + + + + + + app:layout_constraintTop_toBottomOf="@+id/bar"> + + + android:layout_height="@dimen/dp_64" + android:layout_marginTop="@dimen/dp_8"> - @@ -43,12 +93,13 @@ + android:layout_height="@dimen/dp_64" + android:layout_marginTop="@dimen/dp_8"> - @@ -77,13 +123,14 @@ + + + + + + + + + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" /> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_contact_edit.xml b/app/src/main/res/layout/activity_contact_edit.xml index fdc4c77..046dad0 100644 --- a/app/src/main/res/layout/activity_contact_edit.xml +++ b/app/src/main/res/layout/activity_contact_edit.xml @@ -1,6 +1,7 @@ @@ -13,5 +14,47 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_contact_list.xml b/app/src/main/res/layout/activity_contact_list.xml index 7923fea..22cb149 100644 --- a/app/src/main/res/layout/activity_contact_list.xml +++ b/app/src/main/res/layout/activity_contact_list.xml @@ -11,27 +11,69 @@ type="com.ttstd.dialer.activity.contact.list.ContactListActivity.BtnClick" /> - - + android:layout_height="@dimen/dp_48" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> - + + + + + + + + + + + + + + + - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml new file mode 100644 index 0000000..1e4326f --- /dev/null +++ b/app/src/main/res/layout/activity_settings.xml @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_settings_call.xml b/app/src/main/res/layout/activity_settings_call.xml new file mode 100644 index 0000000..585a792 --- /dev/null +++ b/app/src/main/res/layout/activity_settings_call.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_settings_utils.xml b/app/src/main/res/layout/activity_settings_utils.xml new file mode 100644 index 0000000..ce51acc --- /dev/null +++ b/app/src/main/res/layout/activity_settings_utils.xml @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_template.xml b/app/src/main/res/layout/activity_template.xml new file mode 100644 index 0000000..5c7de6a --- /dev/null +++ b/app/src/main/res/layout/activity_template.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_weather_main.xml b/app/src/main/res/layout/activity_weather_main.xml index fcb4995..d11ab62 100644 --- a/app/src/main/res/layout/activity_weather_main.xml +++ b/app/src/main/res/layout/activity_weather_main.xml @@ -17,104 +17,108 @@ - - - - - - - - - - - - - - - - - - - - - + android:layout_height="match_parent"> + app:layout_constraintTop_toTopOf="parent"> + android:layout_height="wrap_content" + android:orientation="vertical"> + + + + + + + + + + + + + + + + + + + + + android:layout_height="wrap_content" + android:layout_margin="@dimen/dp_8" + android:background="@drawable/weather_card_background" /> + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/dp_8" + android:background="@drawable/weather_card_background" /> diff --git a/app/src/main/res/layout/fragment_call.xml b/app/src/main/res/layout/fragment_call.xml index afce9f3..5b37601 100644 --- a/app/src/main/res/layout/fragment_call.xml +++ b/app/src/main/res/layout/fragment_call.xml @@ -20,10 +20,10 @@ @@ -39,14 +39,13 @@ + android:layout_height="@dimen/dp_56"> + android:layout_height="@dimen/dp_56" + android:layout_marginTop="@dimen/dp_8"> + android:layout_height="@dimen/dp_56" + android:layout_marginTop="@dimen/dp_8"> + android:layout_height="@dimen/dp_56" + android:layout_marginTop="@dimen/dp_8"> + @@ -29,9 +29,9 @@ android:layout_height="wrap_content" android:layout_marginStart="@dimen/dp_16" android:maxLines="1" - android:minEms="4" - android:textColor="@color/white" - android:textSize="@dimen/sp_18" + android:minEms="3" + android:textColor="@color/black" + android:textSize="@dimen/sp_22" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" @@ -39,11 +39,12 @@ @@ -51,8 +52,8 @@ @@ -60,10 +61,9 @@ android:id="@+id/tv_temp_min" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="@dimen/dp_16" android:maxLines="1" - android:textColor="@color/white" - android:textSize="@dimen/sp_18" + android:textColor="@color/black" + android:textSize="@dimen/sp_24" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" @@ -74,8 +74,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:maxLines="1" - android:textColor="@color/white" - android:textSize="@dimen/sp_18" + android:textColor="@color/black" + android:textSize="@dimen/sp_24" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" diff --git a/app/src/main/res/layout/layout_floating_window.xml b/app/src/main/res/layout/layout_floating_window.xml new file mode 100644 index 0000000..2bb944f --- /dev/null +++ b/app/src/main/res/layout/layout_floating_window.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_setting_content_item.xml b/app/src/main/res/layout/layout_setting_content_item.xml new file mode 100644 index 0000000..3b81a54 --- /dev/null +++ b/app/src/main/res/layout/layout_setting_content_item.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-sw1280dp/dimens.xml b/app/src/main/res/values-sw1280dp/dimens.xml index 0d70635..3760743 100644 --- a/app/src/main/res/values-sw1280dp/dimens.xml +++ b/app/src/main/res/values-sw1280dp/dimens.xml @@ -2,6 +2,7 @@ 71.1111sp 28.4444dp + -213.3333dp -106.6667dp -71.1111dp diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml new file mode 100644 index 0000000..3ca73e9 --- /dev/null +++ b/app/src/main/res/values/attrs.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index bf50e31..d98edcb 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,14 +1,17 @@ - #6200EE - #3700B3 - #03DAC5 + #2196F3 + #3F51B5 + #03A9F4 #99bd2f25 #BD2F25 - #F8CBBD + #ECECEC + #000000 + #000000 + #9D9D9D diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index a93e9c3..94220af 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -3,6 +3,12 @@ 20sp 8dp + 80dp + 18sp + 15sp + 2dp + + -60dp -30dp -20dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5607ceb..fda7f5a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -5,6 +5,45 @@ Hello blank fragment com.ttstd.dialer.view.ScrollAwareFABBehavior + 共清理了%d个应用 + ❤拨号助手无障碍服务👈🏻 使用拨号助手一键拨号 + + + 开启快捷通话 + 已开启,快捷联系人通话类型 + 未开启,快捷联系人通话类型 + + 来电语音播报 + 已开启,电话呼入时语音播报联系人 + 未开启,电话呼入时语音播报联系人 + + 微信自动接听 + 已开启,自动接听视频和语音 + 未开启,自动接听视频和语音 + + 自动开启免提 + 已开启,自动开启免提 + 未开启,自动开启免提 + + 微信自动拨打视频 + 已开启,无障碍模式实现微信一键通话 + 未开启,无障碍模式实现微信一键通话 + + + + 开启悬浮按钮 + 已开启,点小圆点可以直接返回桌面 + 未开启,点小圆点可以直接返回桌面 + + 悬浮按钮清理内存 + 已开启,在桌面时点击悬浮窗清理内存 + 未开启,在桌面时点击悬浮窗清理内存 + + 设置为默认桌面 + 已设置为默认桌面 + 未设置为默认桌面 + + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 4e026d6..552a77e 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -13,6 +13,8 @@ @color/colorPrimary @color/colorPrimaryDark @color/colorAccent + @color/default_background_color + diff --git a/build.gradle b/build.gradle index bcba65d..d8b82d9 100644 --- a/build.gradle +++ b/build.gradle @@ -42,6 +42,12 @@ allprojects { maven { url 'https://maven.aliyun.com/repository/public' } maven { url 'https://maven.aliyun.com/repository/google' } } + + gradle.projectsEvaluated { + tasks.withType(JavaCompile) { + options.compilerArgs.add('-Xbootclasspath/p:app/libs/framework.jar') + } + } } task clean(type: Delete) {