diff --git a/README.md b/README.md index da91c0c..26be6cc 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ ### English Version: [README](./README_eng.md) > SoloPi是一个无线化、非侵入式的Android自动化工具,公测版拥有录制回放、性能测试、一机多控三项主要功能,能为测试开发人员节省宝贵时间。 +> SoloPi新增鸿蒙版本,欢迎大家试用,切到 solopi-harmony分支 ### 功能特性 @@ -186,6 +187,8 @@ SoloPi支持通过操作一台主机设备来控制多台从机设备,不需 **目前微信群已满,推荐加入钉钉群** **除了钉钉群外,我们在TesterHome也有相关板块,可以在社区里留言回复 https://testerhome.com/topics/node152 ** +### 贡献 +SoloPi 需要开发者们的共建,也希望能在开发者的支持下更好的发展,如果你基于SoloPi开发出了更贴近业务场景的能力(商业/非商业),欢迎和我们联系,也希望能主动为开源出力,提交各种 features/bugfix/issue ,共同维护SoloPi这套自动化工具。 ### 如何贡献 diff --git a/arm64-v8a.json b/arm64-v8a.json index f83db97..cca40b7 100644 --- a/arm64-v8a.json +++ b/arm64-v8a.json @@ -16,8 +16,8 @@ "name": "hulu_screenRecord" }, { - "url": "https://raw.githubusercontent.com/alipay/SoloPi/master/plugins/hulu_minicap_6.zip", - "version": 6, + "url": "https://raw.githubusercontent.com/alipay/SoloPi/master/plugins/hulu_minicap_7.zip", + "version": 7, "type": "base", "name": "minicap" }, diff --git a/armeabi-v7a.json b/armeabi-v7a.json index acfdf30..baa8f22 100644 --- a/armeabi-v7a.json +++ b/armeabi-v7a.json @@ -16,8 +16,8 @@ "name": "hulu_screenRecord" }, { - "url": "https://raw.githubusercontent.com/alipay/SoloPi/master/plugins/hulu_minicap_6.zip", - "version": 6, + "url": "https://raw.githubusercontent.com/alipay/SoloPi/master/plugins/hulu_minicap_7.zip", + "version": 7, "type": "base", "name": "minicap" }, diff --git a/armeabi.json b/armeabi.json index f46b240..f6be6ef 100644 --- a/armeabi.json +++ b/armeabi.json @@ -16,8 +16,8 @@ "name": "hulu_screenRecord" }, { - "url": "https://raw.githubusercontent.com/alipay/SoloPi/master/plugins/hulu_minicap_6.zip", - "version": 6, + "url": "https://raw.githubusercontent.com/alipay/SoloPi/master/plugins/hulu_minicap_7.zip", + "version": 7, "type": "base", "name": "minicap" }, diff --git a/plugins/hulu_minicap_6.zip b/plugins/hulu_minicap_6.zip deleted file mode 100644 index 6ac52f2..0000000 Binary files a/plugins/hulu_minicap_6.zip and /dev/null differ diff --git a/plugins/hulu_minicap_7.zip b/plugins/hulu_minicap_7.zip new file mode 100644 index 0000000..bebf006 Binary files /dev/null and b/plugins/hulu_minicap_7.zip differ diff --git a/src/.gitignore b/src/.gitignore index f922cb1..62ef981 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -3,3 +3,9 @@ build/ .gradle/ *.iml local.properties + +.cxx/ + +git_stats/ +seeds.txt +unused.txt diff --git a/src/app/build.gradle b/src/app/build.gradle index a5ba64e..8c02aeb 100644 --- a/src/app/build.gradle +++ b/src/app/build.gradle @@ -22,27 +22,27 @@ android { minSdkVersion 18 targetSdkVersion rootProject.ext.targetSdkVersion } + lintOptions { + abortOnError false + } buildTypes { release { - minifyEnabled true - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + consumerProguardFiles 'proguard-rules.pro' } debug { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - versionNameSuffix "-" + new Date().format("yyMMddHHmm") + consumerProguardFiles 'proguard-rules.pro' } } } dependencies { - implementation 'androidx.legacy:legacy-support-v4:1.0.0' - implementation 'androidx.legacy:legacy-support-core-utils:1.0.0' - implementation 'androidx.appcompat:appcompat:1.0.0' - implementation 'androidx.recyclerview:recyclerview:1.0.0' - implementation 'com.google.android.material:material:1.0.0' + implementation "androidx.legacy:legacy-support-v4:${ANDROIDX_SUPPORT_V4_VERSION}" + implementation "androidx.legacy:legacy-support-core-utils:${ANDROIDX_SUPPORT_CORE_UTILS_VERSION}" + implementation "androidx.appcompat:appcompat:${ANDROIDX_APPCOMPAT_VERSION}" + implementation "androidx.recyclerview:recyclerview:${ANDROIDX_RECYCLERVIEW_VERSION}" + implementation "com.google.android.material:material:${ANDROIDX_MATERIAL_VERSION}" implementation 'com.github.lecho:hellocharts-library:1.5.8@aar' - implementation 'com.alibaba:fastjson:1.2.73' + implementation "com.alibaba:fastjson:${FASTJSON_VERSION}" implementation 'org.greenrobot:greendao:3.3.0' implementation 'com.squareup.okhttp3:okhttp:3.12.3' implementation 'com.liulishuo.filedownloader:library:1.7.7' @@ -58,10 +58,10 @@ dependencies { implementation('com.github.bumptech.glide:glide:4.11.0') { exclude group: "com.android.support" } - implementation 'commons-io:commons-io:2.6' + implementation "commons-io:commons-io:${COMMON_IO_VERSION}" implementation ('com.orhanobut:logger:2.2.0') { exclude group: "com.android.support" } - compileOnly 'androidx.multidex:multidex:2.0.0' + compileOnly "androidx.multidex:multidex:${ANDROIDX_MULTIDEX_VERSION}" implementation project(':shared') } diff --git a/src/app/proguard-rules.pro b/src/app/proguard-rules.pro index 61ababe..1effb5d 100644 --- a/src/app/proguard-rules.pro +++ b/src/app/proguard-rules.pro @@ -25,57 +25,6 @@ #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} - -# Uncomment this to preserve the line number information for -# debugging stack traces. --keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. --renamesourcefileattribute SourceFile - -#==================================【基本配置】================================== -# 代码混淆压缩比,在0~7之间,默认为5,一般不下需要修改 --optimizationpasses 5 -# 混淆时不使用大小写混合,混淆后的类名为小写 -# windows下的同学还是加入这个选项吧(windows大小写不敏感) --dontusemixedcaseclassnames -# 指定不去忽略非公共的库的类 -# 默认跳过,有些情况下编写的代码与类库中的类在同一个包下,并且持有包中内容的引用,此时就需要加入此条声明 --dontskipnonpubliclibraryclasses -# 指定不去忽略非公共的库的类的成员 --dontskipnonpubliclibraryclassmembers -# 不做预检验,preverify是proguard的四个步骤之一 -# Android不需要preverify,去掉这一步可以加快混淆速度 --dontpreverify -# 有了verbose这句话,混淆后就会生成映射文件 --verbose -#apk 包内所有 class 的内部结构 --dump class_files.txt -#未混淆的类和成员 --printseeds seeds.txt -#列出从 apk 中删除的代码 --printusage unused.txt -#混淆前后的映射 --printmapping mapping.txt -# 指定混淆时采用的算法,后面的参数是一个过滤器 -# 这个过滤器是谷歌推荐的算法,一般不改变 --optimizations !code/simplification/artithmetic,!field/*,!class/merging/* -# 保护代码中的Annotation不被混淆 -# 这在JSON实体映射时非常重要,比如fastJson --keepattributes *Annotation* -# 避免混淆泛型 -# 这在JSON实体映射时非常重要,比如fastJson --keepattributes Signature -# 抛出异常时保留代码行号 --keepattributes SourceFile,LineNumberTable -#忽略警告 --ignorewarning -#==================================【项目配置】================================== -# 保留所有的本地native方法不被混淆 --keepclasseswithmembernames class * { -native ; -} # 保留了继承自Activity、Application这些类的子类 -keep public class * extends android.app.Activity -keep public class * extends android.app.Application @@ -85,6 +34,19 @@ native ; -keep public class * extends android.preference.Preference -keep public class * extends android.view.View -keep public class * extends android.database.sqlite.SQLiteOpenHelper{*;} +# 如果有引用android-support-v4.jar包,可以添加下面这行 +-keep public class com.null.test.ui.fragment.** {*;} +#如果引用了v4或者v7包 +-dontwarn android.support.** + +# AndroidX 方法类 +#-keep class com.google.android.material.** {*;} +#-keep class androidx.** {*;} +-keep public class * extends androidx.** +-keep interface androidx.** {*;} +-dontwarn com.google.android.material.** +-dontnote com.google.android.material.** +-dontwarn androidx.** # 保留Activity中的方法参数是view的方法, -keepclassmembers class * extends android.app.Activity { public void * (android.view.View); @@ -132,10 +94,6 @@ void *(**On*Event); #Patch相关类 -keep class com.alipay.hulu.upgrade.PatchResponse { *; } -keep class com.alipay.hulu.upgrade.PatchResponse$DataBean { *; } --keep class com.alipay.hulu.common.utils.ClassUtil$PatchVersionInfo { *; } --keep class com.alipay.hulu.common.utils.patch.PatchDescription {*;} - --keep class com.alipay.hulu.common.utils.DeviceInfoUtil {*;} #内部方法 -keepattributes EnclosingMethod @@ -155,28 +113,6 @@ void *(**On*Event); # OkHttp platform used only on JVM and when Conscrypt dependency is available. -dontwarn okhttp3.internal.platform.ConscryptPlatform -# injector --keepclassmembers class ** { -@com.alipay.hulu.common.injector.param.Subscriber ; -} --keepclassmembers class ** { -@com.alipay.hulu.common.injector.provider.Provider ; -} - -# BroadcastPackage --keep class com.alipay.hulu.shared.io.socket.LocalNetworkBroadcastService$BroadcastPackage { *; } --keep enum com.alipay.hulu.shared.io.socket.enums.BroadcastCommandEnum { *; } - -# ActionProvider --keep @com.alipay.hulu.common.annotation.Enable class * - -#PrepareWorker --keep interface com.alipay.hulu.shared.node.utils.prepare.PrepareWorker { *; } --keep @com.alipay.hulu.shared.node.utils.prepare.PrepareWorker$PrepareTool class * implements com.alipay.hulu.shared.node.utils.prepare.PrepareWorker { *; } - -# SchemeResolver --keep interface com.alipay.hulu.common.scheme.SchemeActionResolver { *; } --keep @com.alipay.hulu.common.scheme.SchemeResolver class * implements com.alipay.hulu.common.scheme.SchemeActionResolver { *; } #Github Replease -keep class com.alipay.hulu.bean.GithubReleaseBean { *; } @@ -190,57 +126,6 @@ void *(**On*Event); -keep class com.android.permission.** {*;} -keep class com.codebutler.android_websockets.** {*;} -# greeendao --keep class com.alipay.hulu.shared.io.bean.** {*;} --keep class com.alipay.hulu.shared.io.db.** {*;} -### greenDAO 3 --keepclassmembers class * extends org.greenrobot.greendao.AbstractDao { -public static java.lang.String TABLENAME; -} --keep class **$Properties - -# If you do not use SQLCipher: --dontwarn org.greenrobot.greendao.database.** -# If you do not use RxJava: --dontwarn rx.** - - --keep class com.alipay.hulu.shared.node.tree.export.bean.** {*;} --keep class com.alipay.hulu.shared.node.action.OperationMethod {*;} --keep class com.alipay.hulu.shared.node.tree.OperationNode {*;} --keep class com.alipay.hulu.shared.node.tree.OperationNode$AssistantNode {*;} --keep class com.alipay.hulu.shared.node.tree.AbstractNodeTree { *; } --keep class com.alipay.hulu.shared.node.tree.FakeNodeTree { *; } --keep class com.alipay.hulu.shared.node.tree.accessibility.tree.AccessibilityNodeTree { *; } --keep class * extends com.alipay.hulu.shared.node.tree.AbstractNodeTree { *; } - --keep class com.alipay.hulu.common.bean.** {*;} - --keep interface com.alipay.hulu.common.tools.AbstCmdLine {*;} - --keep class com.alipay.hulu.common.utils.patch.PatchContext {*;} - -# Glide --keep class com.alipay.hulu.common.utils.Glide* { *; } --keep public class * implements com.bumptech.glide.module.GlideModule --keep public class * extends com.bumptech.glide.module.AppGlideModule --keep public enum com.bumptech.glide.load.ImageHeaderParser$** { - **[] $VALUES; - public *; -} - --keep class ** implements com.alipay.hulu.shared.display.items.base.Displayable { -public void clear(); -} - --keep interface com.alipay.hulu.common.service.base.ExportService { *; } --keep @interface com.alipay.hulu.common.service.base.LocalService {*;} --keep class com.alipay.hulu.common.utils.patch.PatchClassLoader { -public com.alipay.hulu.common.utils.patch.PatchContext getContext(); -} --keep class ** implements com.alipay.hulu.common.service.base.ExportService { *; } - --keep interface ** extends com.alipay.hulu.common.service.base.ExportService { *; } -keepattributes Exceptions,InnerClasses,Signature @@ -263,4 +148,16 @@ public void setUserVisibleHint(boolean); public void onHiddenChanged(boolean); public void onResume(); public void onPause(); +} +-keep class android.support.v4.app.Fragment { +public void setUserVisibleHint(boolean); +public void onHiddenChanged(boolean); +public void onResume(); +public void onPause(); +} +-keep class * extends android.support.v4.app.Fragment { +public void setUserVisibleHint(boolean); +public void onHiddenChanged(boolean); +public void onResume(); +public void onPause(); } \ No newline at end of file diff --git a/src/app/src/main/AndroidManifest.xml b/src/app/src/main/AndroidManifest.xml index fe912a6..7728b8b 100644 --- a/src/app/src/main/AndroidManifest.xml +++ b/src/app/src/main/AndroidManifest.xml @@ -25,6 +25,7 @@ + @@ -138,15 +139,6 @@ - - - - - - - = Build.VERSION_CODES.N) { - //7.0及以后 - return updateResourcesForAndroidN(context); - } else { - //7.0之前 - Configuration configuration = context.getResources().getConfiguration(); - configuration.locale = LauncherApplication.getInstance().getLanguageLocale(); - context.getResources().updateConfiguration(configuration, null); - return context; - } + @Override + public void onConfigurationChanged(@NonNull Configuration newConfig) { + super.onConfigurationChanged(newConfig); + ContextUtil.updateResources(this); } - @TargetApi(Build.VERSION_CODES.N) - private static Context updateResourcesForAndroidN(Context context) { - Resources resources = context.getResources(); - Configuration configuration = resources.getConfiguration(); - configuration.setLocale(LauncherApplication.getInstance().getLanguageLocale()); - return context.createConfigurationContext(configuration); - } @Override protected void onResume() { @@ -301,6 +288,20 @@ private void getScreenSizeInfo() { getWindowManager().getDefaultDisplay().getMetrics(DeviceInfoUtil.metrics); } + @Override + public void startActivity(final Intent intent) { + if (Thread.currentThread() != Looper.getMainLooper().getThread()) { + LauncherApplication.getInstance().runOnUiThread(new Runnable() { + @Override + public void run() { + BaseActivity.super.startActivity(intent); + } + }); + } else { + super.startActivity(intent); + } + } + /** * 添加Fragment tag信息 * @param tag diff --git a/src/app/src/main/java/com/alipay/hulu/activity/CaseReplayResultActivity.java b/src/app/src/main/java/com/alipay/hulu/activity/CaseReplayResultActivity.java index eaa19e4..c2dadce 100644 --- a/src/app/src/main/java/com/alipay/hulu/activity/CaseReplayResultActivity.java +++ b/src/app/src/main/java/com/alipay/hulu/activity/CaseReplayResultActivity.java @@ -26,11 +26,18 @@ import android.widget.LinearLayout; import android.widget.TextView; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentPagerAdapter; +import androidx.viewpager.widget.ViewPager; + import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.alipay.hulu.R; import com.alipay.hulu.bean.CaseStepHolder; import com.alipay.hulu.bean.ReplayResultBean; +import com.alipay.hulu.bean.ScreenshotBean; import com.alipay.hulu.common.application.LauncherApplication; import com.alipay.hulu.common.tools.BackgroundExecutor; import com.alipay.hulu.common.utils.FileUtils; @@ -53,12 +60,6 @@ import java.util.Locale; import java.util.Map; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentPagerAdapter; -import androidx.viewpager.widget.ViewPager; - public class CaseReplayResultActivity extends BaseActivity { private static final String TAG = "CaseActivity"; @@ -219,8 +220,8 @@ private File saveReplayResult() { // 记录拷贝成功的截图信息 ScreenshotBean bean = new ScreenshotBean(); - bean.name = entry.getKey(); - bean.file = copyTo.getName(); + bean.setName(entry.getKey()); + bean.setFile(copyTo.getName()); screenshots.add(bean); } catch (IOException e) { LogUtil.e(TAG, "拷贝截图文件失败", e); @@ -345,27 +346,4 @@ public CharSequence getPageTitle(int position) { } - /** - * 截图信息 - */ - public static class ScreenshotBean { - private String name; - private String file; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getFile() { - return file; - } - - public void setFile(String file) { - this.file = file; - } - } } diff --git a/src/app/src/main/java/com/alipay/hulu/activity/IndexActivity.java b/src/app/src/main/java/com/alipay/hulu/activity/IndexActivity.java index 3d05f90..42c954f 100644 --- a/src/app/src/main/java/com/alipay/hulu/activity/IndexActivity.java +++ b/src/app/src/main/java/com/alipay/hulu/activity/IndexActivity.java @@ -45,6 +45,7 @@ import com.alipay.hulu.common.service.SPService; import com.alipay.hulu.common.tools.BackgroundExecutor; import com.alipay.hulu.common.tools.CmdTools; +import com.alipay.hulu.common.trigger.Trigger; import com.alipay.hulu.common.utils.ClassUtil; import com.alipay.hulu.common.utils.ContextUtil; import com.alipay.hulu.common.utils.FileUtils; @@ -97,8 +98,10 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { initData(); loadOthers(); +// SPService.putBoolean(SPService.KEY_USE_EASY_MODE, true); + // check update - if (SPService.getBoolean(SPService.KEY_CHECK_UPDATE, true)) { + if (SPService.getBoolean(SPService.KEY_SHOULD_UPDATE_IN_APP, true) && SPService.getBoolean(SPService.KEY_CHECK_UPDATE, true)) { BackgroundExecutor.execute(new Runnable() { @Override public void run() { @@ -166,6 +169,9 @@ public void onUpdateFailed(Throwable t) { } }); } + + // 进入主页的trigger + LauncherApplication.getInstance().triggerAtTime(Trigger.TRIGGER_TIME_HOME_PAGE); } /** diff --git a/src/app/src/main/java/com/alipay/hulu/activity/MyApplication.java b/src/app/src/main/java/com/alipay/hulu/activity/MyApplication.java index 541f01b..0094833 100644 --- a/src/app/src/main/java/com/alipay/hulu/activity/MyApplication.java +++ b/src/app/src/main/java/com/alipay/hulu/activity/MyApplication.java @@ -19,6 +19,7 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; @@ -43,6 +44,7 @@ import com.alipay.hulu.common.utils.LogUtil; import com.alipay.hulu.common.utils.StringUtil; import com.alipay.hulu.service.CaseReplayManager; +import com.alipay.hulu.service.InstallReceiver; import com.alipay.hulu.shared.io.db.GreenDaoManager; import com.alipay.hulu.util.LargeObjectHolder; import com.alipay.hulu.util.SystemUtil; @@ -292,6 +294,15 @@ protected void initInMain() { LogUtil.e(TAG, "Fail to load my app version info", e); } + // Android 8.0及以上,显式监控应用状态 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + IntentFilter intentFilter =new IntentFilter(); + intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED); + intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); + intentFilter.addDataScheme("package"); + registerReceiver(new InstallReceiver(), intentFilter); + } + registerLifecycleCallbacks(); } diff --git a/src/app/src/main/java/com/alipay/hulu/activity/NewRecordActivity.java b/src/app/src/main/java/com/alipay/hulu/activity/NewRecordActivity.java index 33e0b87..09f7504 100644 --- a/src/app/src/main/java/com/alipay/hulu/activity/NewRecordActivity.java +++ b/src/app/src/main/java/com/alipay/hulu/activity/NewRecordActivity.java @@ -72,7 +72,7 @@ /** * Created by lezhou.wyl on 2018/2/1. */ -@EntryActivity(iconName = "com.alipay.hulu.R$drawable.icon_luxiang", nameResName = "com.alipay.hulu.R$string.activity__record", permissions = {"adb", "float", "background", "toast:${com.alipay.hulu.R$string.toast_message__add_solopi_background}"}, index = 1, cornerText = "New", cornerPersist = 3, cornerBg = 0xFFFF5900) +@EntryActivity(iconName = "com.alipay.hulu.R$drawable.icon_luxiang", nameResName = "com.alipay.hulu.R$string.activity__record", permissions = {"adb", "float", "background", "toast:${com.alipay.hulu.R$string.toast_message__add_solopi_background}", "powerSave"}, index = 1, cornerText = "New", cornerPersist = 3, cornerBg = 0xFFFF5900) public class NewRecordActivity extends BaseActivity { private static final String TAG = NewRecordActivity.class.getSimpleName(); diff --git a/src/app/src/main/java/com/alipay/hulu/activity/PerformanceActivity.java b/src/app/src/main/java/com/alipay/hulu/activity/PerformanceActivity.java index 9018609..26cd535 100644 --- a/src/app/src/main/java/com/alipay/hulu/activity/PerformanceActivity.java +++ b/src/app/src/main/java/com/alipay/hulu/activity/PerformanceActivity.java @@ -61,7 +61,7 @@ /** * Created by lezhou.wyl on 2018/1/28. */ -@EntryActivity(iconName = "com.alipay.hulu.R$drawable.icon_xingneng", nameResName = "com.alipay.hulu.R$string.activity__performance_test", permissions = {"adb", "float", "background"}, index = 2) +@EntryActivity(iconName = "com.alipay.hulu.R$drawable.icon_xingneng", nameResName = "com.alipay.hulu.R$string.activity__performance_test", permissions = {"adb", "float", "background", "powerSave"}, index = 2) public class PerformanceActivity extends BaseActivity { private String TAG = "PerformanceFragment"; diff --git a/src/app/src/main/java/com/alipay/hulu/activity/PerformanceChartActivity.java b/src/app/src/main/java/com/alipay/hulu/activity/PerformanceChartActivity.java index a867056..3ccd671 100644 --- a/src/app/src/main/java/com/alipay/hulu/activity/PerformanceChartActivity.java +++ b/src/app/src/main/java/com/alipay/hulu/activity/PerformanceChartActivity.java @@ -18,15 +18,16 @@ import android.content.Intent; import android.os.Build; import android.os.Bundle; -import androidx.annotation.Nullable; -import androidx.core.app.ActivityCompat; -import androidx.appcompat.widget.AppCompatSpinner; import android.view.View; import android.widget.AdapterView; import android.widget.SimpleAdapter; import android.widget.TextView; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatSpinner; + import com.alipay.hulu.R; +import com.alipay.hulu.common.service.SPService; import com.alipay.hulu.common.utils.FileUtils; import com.alipay.hulu.common.utils.LogUtil; import com.alipay.hulu.common.utils.StringUtil; @@ -37,8 +38,11 @@ import java.io.BufferedReader; import java.io.File; import java.io.FileFilter; -import java.io.FileReader; +import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.nio.charset.UnsupportedCharsetException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -238,8 +242,18 @@ public void onItemSelected(AdapterView parent, View view, int position, long } else { // 重新构造录制文件名称 File f = new File(currentFolder, realPattern.getName() + "_" + realPattern.getSource() + "_" + realPattern.getStartTime() + "_" + realPattern.getEndTime() + ".csv"); + + // 加载编码信息 + String charsetName = SPService.getString(SPService.KEY_OUTPUT_CHARSET, "GBK"); + Charset charset; + try { + charset = Charset.forName(charsetName); + } catch (UnsupportedCharsetException e) { + LogUtil.w(TAG, "unsupported charset for name=" + charsetName, e); + charset = Charset.forName("UTF-8"); + } try { - BufferedReader reader = new BufferedReader(new FileReader(f)); + BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(f), charset)); String dataTitle = reader.readLine(); // 首行定义数据单位 if (dataTitle != null) { @@ -253,7 +267,7 @@ public void onItemSelected(AdapterView parent, View view, int position, long while ((line = reader.readLine()) != null) { String[] contents = line.split(","); LogUtil.d(TAG, "read line: %s", Arrays.toString(contents)); - if (contents.length == 3) { + if (contents.length == 3 || contents.length == 4) { RecordPattern.RecordItem item = new RecordPattern.RecordItem(Long.parseLong(contents[0]), Float.parseFloat(contents[1]), contents[2]); records.add(item); } else if (contents.length == 2) { diff --git a/src/app/src/main/java/com/alipay/hulu/activity/SettingsActivity.java b/src/app/src/main/java/com/alipay/hulu/activity/SettingsActivity.java index 72451ef..b9342f9 100644 --- a/src/app/src/main/java/com/alipay/hulu/activity/SettingsActivity.java +++ b/src/app/src/main/java/com/alipay/hulu/activity/SettingsActivity.java @@ -112,12 +112,18 @@ public class SettingsActivity extends BaseActivity { private View mAutoReplaySettingWrapper; private TextView mAutoReplaySettingInfo; + private View mRecordCoverModeSettingWrapper; + private TextView mRecordCoverModeSettingInfo; + private View mSkipAccessibilitySettingWrapper; private TextView mSkipAccessibilitySettingInfo; private View mMaxWaitSettingWrapper; private TextView mMaxWaitSettingInfo; + private View mMaxScrollFindSettingWrapper; + private TextView mMaxScrollFindSettingInfo; + private View mDefaultRotationSettingWrapper; private TextView mDefaultRotationSettingInfo; @@ -441,6 +447,29 @@ public void onClick(DialogInterface dialog, int which) { } }); + mRecordCoverModeSettingWrapper.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + new AlertDialog.Builder(SettingsActivity.this, R.style.SimpleDialogTheme) + .setMessage(R.string.setting__choose_action_block_mode) + .setPositiveButton(R.string.setting__conver_mode, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + SPService.putBoolean(SPService.KEY_RECORD_COVER_MODE, true); + mRecordCoverModeSettingInfo.setText(R.string.setting__conver_mode); + dialog.dismiss(); + } + }).setNegativeButton(R.string.setting__block_mode, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + SPService.putBoolean(SPService.KEY_RECORD_COVER_MODE, false); + mRecordCoverModeSettingInfo.setText(R.string.setting__block_mode); + dialog.dismiss(); + } + }).show(); + } + }); + mSkipAccessibilitySettingWrapper.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -480,6 +509,28 @@ public void onDialogPositive(List data) { } }); + + mMaxScrollFindSettingWrapper.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + List> data = new ArrayList<>(2); + data.add(new Pair<>(getString(R.string.settings__max_scroll_find_count), "" + SPService.getLong(SPService.KEY_MAX_SCROLL_FIND_COUNT, 0))); + showMultipleEditDialog(SettingsActivity.this, new OnDialogResultListener() { + @Override + public void onDialogPositive(List data) { + if (data.size() != 1) { + LogUtil.e("SettingActivity", "获取编辑项不为1项"); + return; + } + + // 更新截图分辨率信息 + SPService.putInt(SPService.KEY_MAX_SCROLL_FIND_COUNT, Integer.parseInt(data.get(0))); + mMaxWaitSettingInfo.setText(data.get(0)); + } + }, getString(R.string.settings__max_scroll_find_count), data); + } + }); + mBaseDirSettingWrapper.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -808,6 +859,15 @@ private void initView() { mHightlightSettingInfo = (TextView) findViewById(R.id.replay_highlight_setting_info); mHightlightSettingInfo.setText(SPService.getBoolean(SPService.KEY_HIGHLIGHT_REPLAY_NODE, true)? R.string.constant__yes: R.string.constant__no); + mRecordCoverModeSettingWrapper = findViewById(R.id.record_cover_mode_setting_wrapper); + mRecordCoverModeSettingInfo = _findViewById(R.id.record_cover_mode_setting_info); + boolean coverMode = SPService.getBoolean(SPService.KEY_RECORD_COVER_MODE, false); + if (coverMode) { + mRecordCoverModeSettingInfo.setText(R.string.setting__conver_mode); + } else { + mRecordCoverModeSettingInfo.setText(R.string.setting__block_mode); + } + mLanguageSettingWrapper = findViewById(R.id.language_setting_wrapper); mLanguageSettingInfo = (TextView) findViewById(R.id.language_setting_info); int pos = SPService.getInt(SPService.KEY_USE_LANGUAGE, 0); @@ -864,6 +924,12 @@ private void initView() { long maxWaitTime = SPService.getLong(SPService.KEY_MAX_WAIT_TIME, 10000L); mMaxWaitSettingInfo.setText(maxWaitTime + "ms"); + + mMaxScrollFindSettingWrapper = findViewById(R.id.max_scroll_find_setting_wrapper); + mMaxScrollFindSettingInfo = _findViewById(R.id.max_scroll_find_setting_info); + mMaxScrollFindSettingInfo.setText(Integer.toString(SPService.getInt(SPService.KEY_MAX_SCROLL_FIND_COUNT, 2))); + + mCheckUpdateSettingWrapper = findViewById(R.id.check_update_setting_wrapper); mCheckUpdateSettingInfo = (TextView) findViewById(R.id.check_update_setting_info); boolean checkUpdate = SPService.getBoolean(SPService.KEY_CHECK_UPDATE, true); @@ -873,6 +939,11 @@ private void initView() { mCheckUpdateSettingInfo.setText(R.string.constant__no); } + // 如果不应该展示检测更新部分 + if (!SPService.getBoolean(SPService.KEY_SHOULD_UPDATE_IN_APP, true)) { + mCheckUpdateSettingWrapper.setVisibility(View.GONE); + } + mBaseDirSettingWrapper = findViewById(R.id.base_dir_setting_wrapper); mBaseDirSettingInfo = (TextView) findViewById(R.id.base_dir_setting_info); mBaseDirSettingInfo.setText(FileUtils.getSolopiDir().getPath()); diff --git a/src/app/src/main/java/com/alipay/hulu/adapter/FloatStressAdapter.java b/src/app/src/main/java/com/alipay/hulu/adapter/FloatStressAdapter.java new file mode 100644 index 0000000..3d466f3 --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/adapter/FloatStressAdapter.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.adapter; + +import android.content.Context; +import android.graphics.Color; +import android.os.Build; +import android.view.View; +import android.widget.SeekBar; +import android.widget.TextView; + +import androidx.annotation.NonNull; + +import com.alipay.hulu.R; +import com.alipay.hulu.common.injector.InjectorService; +import com.alipay.hulu.common.injector.param.Subscriber; +import com.alipay.hulu.common.injector.provider.Param; +import com.alipay.hulu.shared.display.items.MemoryTools; +import com.alipay.hulu.tools.PerformStressImpl; + +import java.util.ArrayList; +import java.util.List; + +public class FloatStressAdapter extends SoloBaseRecyclerAdapter { + public FloatStressAdapter(Context context) { + super(context, R.layout.float_stress_item); + init(); + } + + private int cpuCount = 0; + private int cpuPercent = 0; + private int memory = 0; + + @Subscriber(@Param(PerformStressImpl.PERFORMANCE_STRESS_CPU_COUNT)) + public void receiveCpuCount(int count) { + if (count == cpuCount) { + return; + } + + // CPU变了 + cpuCount = count; + List items = getAllData(); + items.get(1).count = cpuCount; + notifyDataSetChanged(); + } + + @Subscriber(@Param(PerformStressImpl.PERFORMANCE_STRESS_CPU_PERCENT)) + public void receiveCpuPercent(int percent) { + if (cpuPercent == percent) { + return; + } + + // CPU占比变了 + cpuPercent = percent; + List items = getAllData(); + items.get(0).count = cpuPercent; + notifyDataSetChanged(); + } + + + @Subscriber(@Param(PerformStressImpl.PERFORMANCE_STRESS_MEMORY)) + public void receiveMemory(int memory) { + if (memory == this.memory) { + return; + } + + // 内存变了 + this.memory = memory; + List items = getAllData(); + StressItem item = items.get(2); + item.count = memory; + item.max = MemoryTools.getTotalMemory(context).intValue(); + notifyDataSetChanged(); + } + + @Override + public SimpleViewHolder generateViewHolder(View view) { + return new FloatViewHolder(view); + } + + private void init() { + InjectorService.g().register(this); + + List stressItemList = new ArrayList<>(); + StressItem map = new StressItem(); + map.type = 0; + map.title = "CPU负载"; + map.unit = "%"; + map.max = 100; + map.count = cpuPercent; + stressItemList.add(map); + + map = new StressItem(); + map.type = 1; + map.title = "CPU占用核数"; + map.unit = "核"; + map.max = Runtime.getRuntime().availableProcessors(); + map.count = cpuCount; + stressItemList.add(map); + + map = new StressItem(); + map.type = 2; + map.title = "内存占用"; + map.unit = "MB"; + map.max = MemoryTools.getTotalMemory(context).intValue(); + map.count = memory; + stressItemList.add(map); + updateDate(stressItemList); + } + + public static class StressItem { + int type; + String title; + String unit; + int max; + int count; + } + + private static class FloatViewHolder extends SimpleViewHolder { + private TextView title; + private TextView unit; + private TextView count; + private SeekBar seekBar; + public FloatViewHolder(@NonNull View itemView) { + super(itemView); + } + + @Override + public void bindView(View base) { + this.title = base.findViewById(R.id.display_stress_title); + this.unit = base.findViewById(R.id.display_stress_data_unit); + this.count = base.findViewById(R.id.display_stress_data); + this.seekBar = base.findViewById(R.id.display_stress_sb); + } + + @Override + public void bindData(StressItem data, int index) { + if (data == null) { + return; + } + title.setText(data.title); + unit.setText(data.unit); + seekBar.setMax(data.max); + if (Build.VERSION.SDK_INT >= 26) { + seekBar.setMin(0); + } + + int type = data.type; + seekBar.setTag(type); + + int countValue = data.count; + if (type == 2) { + if (countValue > data.max / 2) { + count.setTextColor(Color.RED); + count.setText("⚠️" + countValue); + } else { + count.setTextColor(count.getResources().getColor(R.color.secondaryText)); + count.setText("" + countValue); + } + } else { + count.setText("" + countValue); + } + seekBar.setProgress(countValue); + seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (fromUser) { + int type = (int) seekBar.getTag(); + if (type == 2) { + if (progress > seekBar.getMax() / 2) { + count.setTextColor(Color.RED); + count.setText("⚠️" + progress); + } else { + count.setTextColor(count.getResources().getColor(R.color.secondaryText)); + count.setText("" + progress); + } + } else { + count.setText("" + progress); + } + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + int type = (int) seekBar.getTag(); + String event; + if (type == 0) { + event = PerformStressImpl.PERFORMANCE_STRESS_CPU_PERCENT; + } else if (type == 1) { + event = PerformStressImpl.PERFORMANCE_STRESS_CPU_COUNT; + } else { + event = PerformStressImpl.PERFORMANCE_STRESS_MEMORY; + } + InjectorService.g().pushMessage(event, seekBar.getProgress()); + } + }); + } + } +} diff --git a/src/app/src/main/java/com/alipay/hulu/adapter/PerformStressAdapter.java b/src/app/src/main/java/com/alipay/hulu/adapter/PerformStressAdapter.java index ab76f80..8868147 100644 --- a/src/app/src/main/java/com/alipay/hulu/adapter/PerformStressAdapter.java +++ b/src/app/src/main/java/com/alipay/hulu/adapter/PerformStressAdapter.java @@ -16,6 +16,7 @@ package com.alipay.hulu.adapter; import android.content.Context; +import android.graphics.Color; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -176,6 +177,14 @@ public void onProgressChanged(SeekBar arg0, int progress, boolean fromUser) { default: return; } + // 超过一半,将颜色变成红色,提示危险 + if (position == 2) { + if (progress > arg0.getMax() / 2) { + finalHolder.data.setTextColor(Color.RED); + } else { + finalHolder.data.setTextColor(finalHolder.data.getResources().getColor(R.color.secondaryText)); + } + } finalHolder.data.setText(String.valueOf(progress)); } } @@ -220,6 +229,11 @@ public void onStopTrackingTouch(SeekBar seekBar) { break; case 2: holder.sBar.setProgress(memory); + if (memory > holder.sBar.getMax() / 2) { + holder.data.setTextColor(Color.RED); + } else { + holder.data.setTextColor(holder.data.getResources().getColor(R.color.secondaryText)); + } holder.data.setText(Integer.toString(memory)); break; } diff --git a/src/app/src/main/java/com/alipay/hulu/adapter/SoloBaseRecyclerAdapter.java b/src/app/src/main/java/com/alipay/hulu/adapter/SoloBaseRecyclerAdapter.java new file mode 100644 index 0000000..37e5644 --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/adapter/SoloBaseRecyclerAdapter.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.adapter; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.alipay.hulu.common.utils.LogUtil; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +/** + * 通用Adapter对象 + * @param + */ +public abstract class SoloBaseRecyclerAdapter extends RecyclerView.Adapter> { + private static final String TAG = SoloBaseRecyclerAdapter.class.getSimpleName(); + protected List dataList = new ArrayList<>(); + protected Context context; + protected LayoutInflater inflater; + protected OnItemClickListener listener; + private int layoutId; + + protected View.OnClickListener itemsOnClickListener; + protected View.OnLongClickListener itemsOnLongClickListener; + + public SoloBaseRecyclerAdapter(Context context, @LayoutRes int layoutId) { + this.context = context; + this.inflater = LayoutInflater.from(context); + this.layoutId = layoutId; + + this.itemsOnClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + Integer position = (Integer) v.getTag(); + if (listener != null && position != null && position >= 0 && position < dataList.size()) { + T data = dataList.get(position); + listener.onItemClick(data, position); + } + } + }; + + this.itemsOnLongClickListener = new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + Integer position = (Integer) v.getTag(); + if (listener != null && position != null && position >= 0 && position < dataList.size()) { + T data = dataList.get(position); + return listener.onItemLongClick(data, position); + } + + return false; + } + }; + + } + + /** + * 更新数据 + * @param newData + */ + public void updateDate(List newData) { + dataList.clear(); + if (newData != null) { + dataList.addAll(newData); + } + notifyDataSetChanged(); + } + + /** + * 添加数据 + * @param data + */ + public void addItem(T data) { + if (data != null) { + dataList.add(data); + notifyItemInserted(dataList.size()); + } + } + + /** + * 删除对象 + * @param index + */ + public void deleteItem(int index) { + dataList.remove(index); + notifyItemRemoved(index); + } + + /** + * 获取对象数量 + * @return + */ + public int getCount() { + return dataList.size(); + } + + /** + * 获取所有对象 + * @return + */ + public List getAllData() { + return new ArrayList<>(dataList); + } + + /** + * 生成ViewHolder + * @param view + * @return + */ + public abstract SimpleViewHolder generateViewHolder(View view); + + @NonNull + @Override + public SimpleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = inflater.inflate(layoutId, parent, false); + + // 监听点击事件 + registerListener(v); + SimpleViewHolder holder = generateViewHolder(v); + holder.setRef(this); + return holder; + } + + private void registerListener(View v) { + v.setOnClickListener(itemsOnClickListener); + v.setOnLongClickListener(itemsOnLongClickListener); + } + + @Override + public void onBindViewHolder(@NonNull SimpleViewHolder holder, int position) { + T data = dataList.get(position); + holder._bindData(data, position); + } + + @Override + public int getItemCount() { + return dataList.size(); + } + + /** + * 设置事件监听器 + * @param listener + */ + public void setItemOperationListener(OnItemClickListener listener) { + this.listener = listener; + } + + /** + * 简易ViewHolder对象 + * @param + */ + public static abstract class SimpleViewHolder extends RecyclerView.ViewHolder { + protected WeakReference> ref; + public SimpleViewHolder(@NonNull View itemView) { + super(itemView); + bindView(itemView); + } + + public void setRef(SoloBaseRecyclerAdapter adapter) { + this.ref = new WeakReference<>(adapter); + } + + /** + * 删除自身数据 + */ + public void deleteSelf() { + if (ref == null || ref.get() == null) { + LogUtil.w(TAG, "Holder ref is null"); + return; + } + + ref.get().deleteItem((Integer) itemView.getTag()); + } + + /** + * 绑定View对象 + * @param base + */ + public abstract void bindView(View base); + + public void _bindData(K data, int position) { + itemView.setTag(position); + bindData(data, position); + } + + /** + * 绑定数据 + * @param data + */ + public abstract void bindData(K data, int index); + } + + public interface OnItemClickListener { + void onItemClick(K data, int position); + boolean onItemLongClick(K data, int position); + } +} diff --git a/src/app/src/main/java/com/alipay/hulu/bean/AdvanceCaseSetting.java b/src/app/src/main/java/com/alipay/hulu/bean/AdvanceCaseSetting.java index 3f6980b..853fcfc 100644 --- a/src/app/src/main/java/com/alipay/hulu/bean/AdvanceCaseSetting.java +++ b/src/app/src/main/java/com/alipay/hulu/bean/AdvanceCaseSetting.java @@ -15,6 +15,8 @@ */ package com.alipay.hulu.bean; +import com.alipay.hulu.shared.node.tree.export.bean.OperationStep; + import java.util.List; /** @@ -28,6 +30,16 @@ public class AdvanceCaseSetting { private String overrideApp; private CaseRunningParam runningParam; + /** + * 准备步骤(不录制) + */ + private List prepareActions; + + /** + * 后续步骤(不录制) + */ + private List suffixActions; + public AdvanceCaseSetting() { } @@ -82,4 +94,20 @@ public CaseRunningParam getRunningParam() { public void setRunningParam(CaseRunningParam runningParam) { this.runningParam = runningParam; } + + public List getPrepareActions() { + return prepareActions; + } + + public void setPrepareActions(List prepareActions) { + this.prepareActions = prepareActions; + } + + public List getSuffixActions() { + return suffixActions; + } + + public void setSuffixActions(List suffixActions) { + this.suffixActions = suffixActions; + } } \ No newline at end of file diff --git a/src/app/src/main/java/com/alipay/hulu/bean/ReplayResultBean.java b/src/app/src/main/java/com/alipay/hulu/bean/ReplayResultBean.java index 267e64b..f8e0dd5 100644 --- a/src/app/src/main/java/com/alipay/hulu/bean/ReplayResultBean.java +++ b/src/app/src/main/java/com/alipay/hulu/bean/ReplayResultBean.java @@ -19,7 +19,6 @@ import android.os.Parcelable; import com.alibaba.fastjson.JSON; -import com.alipay.hulu.activity.CaseReplayResultActivity; import com.alipay.hulu.common.bean.DeviceInfo; import com.alipay.hulu.common.utils.StringUtil; import com.alipay.hulu.shared.node.tree.export.bean.OperationStep; @@ -116,7 +115,7 @@ public class ReplayResultBean implements Parcelable { /** * (仅用于本地结果)结果截图列表 */ - private List screenshots; + private List screenshots; /** * (仅用于本地结果)结果目录 @@ -195,11 +194,11 @@ public void setCurrentOperationLog(List currentOperationLog) { this.currentOperationLog = currentOperationLog; } - public List getScreenshots() { + public List getScreenshots() { return screenshots; } - public void setScreenshots(List screenshots) { + public void setScreenshots(List screenshots) { this.screenshots = screenshots; } diff --git a/src/app/src/main/java/com/alipay/hulu/bean/ScreenshotBean.java b/src/app/src/main/java/com/alipay/hulu/bean/ScreenshotBean.java new file mode 100644 index 0000000..2dce8e6 --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/bean/ScreenshotBean.java @@ -0,0 +1,25 @@ +package com.alipay.hulu.bean; + +/** + * 截图信息 + */ +public class ScreenshotBean { + private String name; + private String file; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getFile() { + return file; + } + + public void setFile(String file) { + this.file = file; + } +} diff --git a/src/app/src/main/java/com/alipay/hulu/fragment/CaseStepEditFragment.java b/src/app/src/main/java/com/alipay/hulu/fragment/CaseStepEditFragment.java index f372809..8f167a4 100644 --- a/src/app/src/main/java/com/alipay/hulu/fragment/CaseStepEditFragment.java +++ b/src/app/src/main/java/com/alipay/hulu/fragment/CaseStepEditFragment.java @@ -1053,6 +1053,7 @@ public void onCancel() { clickActions.add(convertPerformActionToSubMenu(PerformActionEnum.LONG_CLICK)); clickActions.add(convertPerformActionToSubMenu(PerformActionEnum.CLICK_IF_EXISTS)); clickActions.add(convertPerformActionToSubMenu(PerformActionEnum.CLICK_QUICK)); + clickActions.add(convertPerformActionToSubMenu(PerformActionEnum.CLICK_AND_INPUT)); clickActions.add(convertPerformActionToSubMenu(PerformActionEnum.MULTI_CLICK)); NODE_ACTION_MAP.put(R.string.function_group__click, clickActions); @@ -1114,6 +1115,8 @@ public void onCancel() { gScrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.GLOBAL_SCROLL_TO_TOP)); gScrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.GLOBAL_SCROLL_TO_LEFT)); gScrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.GLOBAL_SCROLL_TO_RIGHT)); + gScrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.KEYBOARD_INPUT)); + gScrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.INPUT_GLOBAL)); gScrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.GLOBAL_PINCH_OUT)); gScrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.GLOBAL_PINCH_IN)); GLOBAL_ACTION_MAP.put(R.string.function_group__scroll, gScrollActions); diff --git a/src/app/src/main/java/com/alipay/hulu/fragment/LocalReplayResultListFragment.java b/src/app/src/main/java/com/alipay/hulu/fragment/LocalReplayResultListFragment.java index 841e0d6..e9acd79 100644 --- a/src/app/src/main/java/com/alipay/hulu/fragment/LocalReplayResultListFragment.java +++ b/src/app/src/main/java/com/alipay/hulu/fragment/LocalReplayResultListFragment.java @@ -35,6 +35,7 @@ import com.alipay.hulu.bean.CaseStepHolder; import com.alipay.hulu.bean.ReplayResultBean; import com.alipay.hulu.bean.ReplayStepInfoBean; +import com.alipay.hulu.bean.ScreenshotBean; import com.alipay.hulu.common.bean.DeviceInfo; import com.alipay.hulu.common.tools.BackgroundExecutor; import com.alipay.hulu.common.utils.FileUtils; @@ -300,10 +301,13 @@ private void showReplayResult(int position) { LogUtil.e(TAG, "Fail to find ", e); } - List screenshotBeans = resultBean.getScreenshots(); + List screenshotBeans = resultBean.getScreenshots(); if (screenshotBeans != null) { ArrayMap screenshots = new ArrayMap<>(); - for (CaseReplayResultActivity.ScreenshotBean screenshot: screenshotBeans) { + for (ScreenshotBean screenshot: screenshotBeans) { + if (screenshot == null || StringUtil.isEmpty(screenshot.getFile()) || StringUtil.isEmpty(screenshot.getName())) { + continue; + } screenshots.put(screenshot.getName(), new File(baseDir, screenshot.getFile()).getPath()); } resultBean.setScreenshotFiles(screenshots); diff --git a/src/app/src/main/java/com/alipay/hulu/prepare/StartAppPreparer.java b/src/app/src/main/java/com/alipay/hulu/prepare/StartAppPreparer.java index e870c64..7eab6d4 100644 --- a/src/app/src/main/java/com/alipay/hulu/prepare/StartAppPreparer.java +++ b/src/app/src/main/java/com/alipay/hulu/prepare/StartAppPreparer.java @@ -61,7 +61,7 @@ public boolean doPrepareWork(String targetApp, PrepareUtil.PrepareStatus status) // 处理清理数据后弹出的权限弹窗 final CountDownLatch latch = new CountDownLatch(1); OperationMethod method = new OperationMethod(PerformActionEnum.HANDLE_ALERT); - service.doSomeAction(method, null, new OperationContext.OperationListener() { + service.doSomeAction(method, null, new OperationContext.BaseOperationListener() { @Override public void notifyOperationFinish() { latch.countDown(); diff --git a/src/app/src/main/java/com/alipay/hulu/replay/AbstractStepProvider.java b/src/app/src/main/java/com/alipay/hulu/replay/AbstractStepProvider.java index a8d0183..1856901 100644 --- a/src/app/src/main/java/com/alipay/hulu/replay/AbstractStepProvider.java +++ b/src/app/src/main/java/com/alipay/hulu/replay/AbstractStepProvider.java @@ -15,18 +15,27 @@ */ package com.alipay.hulu.replay; +import android.app.ActivityManager; import android.content.Context; import android.content.DialogInterface; import androidx.appcompat.app.AlertDialog; + +import android.os.Build; import android.view.View; import com.alipay.hulu.R; import com.alipay.hulu.bean.ReplayResultBean; import com.alipay.hulu.bean.ReplayStepInfoBean; +import com.alipay.hulu.common.application.LauncherApplication; +import com.alipay.hulu.common.tools.BackgroundExecutor; +import com.alipay.hulu.common.tools.CmdTools; import com.alipay.hulu.common.utils.LogUtil; import com.alipay.hulu.service.CaseReplayManager; +import com.alipay.hulu.shared.node.action.PerformActionEnum; import com.alipay.hulu.shared.node.tree.export.bean.OperationStep; +import com.alipay.hulu.util.DialogUtils; +import java.io.IOException; import java.util.Arrays; import java.util.Date; import java.util.List; @@ -93,12 +102,62 @@ public List genReplayResult() { public abstract void onStepInfo(ReplayStepInfoBean bean); public void onFloatClick(Context context, final CaseReplayManager manager) { - showFunctionView(context, "是否终止回放", new Runnable() { + DialogUtils.showFunctionView(context, Arrays.asList(PerformActionEnum.NORMAL_EXIT, PerformActionEnum.FORCE_STOP), new DialogUtils.FunctionViewCallback() { + + @Override + public void onExecute(DialogInterface dialog, PerformActionEnum action) { + if (action == PerformActionEnum.NORMAL_EXIT) { + manager.stopRunning(); + } else if (action == PerformActionEnum.FORCE_STOP) { + // 移除所有Task + ActivityManager am = (ActivityManager) LauncherApplication.getInstance() + .getSystemService(Context.ACTIVITY_SERVICE); + if (am != null && Build.VERSION.SDK_INT >= 21) { + try { + List tasks = am.getAppTasks(); + for (ActivityManager.AppTask task: tasks) { + task.finishAndRemoveTask(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + BackgroundExecutor.execute(new Runnable() { + @Override + public void run() { + int pid = android.os.Process.myPid(); + String command = "kill -9 "+ pid; + try { + Runtime.getRuntime().exec(command); + } catch (IOException e) { + LogUtil.e(TAG, "强制关闭进程失败"); + } + // adb强杀 + try { + String cmd = "am force-stop " + LauncherApplication.getInstance().getPackageName(); + CmdTools.execCmd(cmd + " && " + cmd); + } catch (Throwable e) { + LogUtil.w(TAG, "force-stop fail??", e); + } + } + }, 200); + + // System exit + System.exit(0); + } + dialog.dismiss(); + } + + @Override + public void onCancel(DialogInterface dialog) { + dialog.dismiss(); + } + @Override - public void run() { - manager.stopRunning(); + public void onDismiss(DialogInterface dialog) { + } - }, null); + }); } /** @@ -109,45 +168,4 @@ public void run() { public View provideView(Context context) { return null; } - - /** - * 展示操作dialog - * @param message 消息 - * @param confirmAction 确定动作 - * @param cancelAction 取消动作 - */ - protected void showFunctionView(Context context, String message, final Runnable confirmAction, final Runnable cancelAction) { - try { - AlertDialog dialog = new AlertDialog.Builder(context, R.style.SimpleDialogTheme) - .setMessage(message) - .setPositiveButton(R.string.constant__confirm, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - if (confirmAction != null) { - confirmAction.run(); - } - dialog.dismiss(); - } - }) - .setNegativeButton(R.string.constant__cancel, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - if (cancelAction != null) { - cancelAction.run(); - } - dialog.dismiss(); - } - }).setOnCancelListener(new DialogInterface.OnCancelListener() { - @Override - public void onCancel(DialogInterface dialog) { - dialog.dismiss(); - } - }).create(); - dialog.getWindow().setType(com.alipay.hulu.common.constant.Constant.TYPE_ALERT); - dialog.setCanceledOnTouchOutside(false); - dialog.show(); - } catch (Exception e) { - LogUtil.e(TAG, e.getMessage()); - } - } } diff --git a/src/app/src/main/java/com/alipay/hulu/replay/OperationStepProvider.java b/src/app/src/main/java/com/alipay/hulu/replay/OperationStepProvider.java index 6467690..2199869 100644 --- a/src/app/src/main/java/com/alipay/hulu/replay/OperationStepProvider.java +++ b/src/app/src/main/java/com/alipay/hulu/replay/OperationStepProvider.java @@ -497,7 +497,7 @@ protected void takeScreenshot() { final CountDownLatch latch = new CountDownLatch(1); // 执行截图操作 - operationService.doSomeAction(method, null, new OperationContext.OperationListener() { + operationService.doSomeAction(method, null, new OperationContext.BaseOperationListener() { @Override public void notifyOperationFinish() { latch.countDown(); diff --git a/src/app/src/main/java/com/alipay/hulu/scheme/ConfigSchemeResolver.java b/src/app/src/main/java/com/alipay/hulu/scheme/ConfigSchemeResolver.java index 0846c62..4d3fbd6 100644 --- a/src/app/src/main/java/com/alipay/hulu/scheme/ConfigSchemeResolver.java +++ b/src/app/src/main/java/com/alipay/hulu/scheme/ConfigSchemeResolver.java @@ -4,9 +4,11 @@ import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; +import com.alipay.hulu.common.application.LauncherApplication; import com.alipay.hulu.common.scheme.SchemeActionResolver; import com.alipay.hulu.common.scheme.SchemeResolver; import com.alipay.hulu.common.service.SPService; +import com.alipay.hulu.common.utils.Callback; import com.alipay.hulu.common.utils.LogUtil; import com.alipay.hulu.common.utils.StringUtil; @@ -22,7 +24,7 @@ public class ConfigSchemeResolver implements SchemeActionResolver { private static final String VALUE = "value"; @Override - public boolean processScheme(Context context, Map params) { + public boolean processScheme(Context context, Map params, Callback> callback) { String key = params.get(KEY); String value = params.get(VALUE); if (StringUtil.isEmpty(key) || value == null) { @@ -40,6 +42,7 @@ public boolean processScheme(Context context, Map params) { */ private boolean processConfigSet(String key, String value) { switch (key) { + case KEY_AUTO_CLEAR_FILES_DAYS: return processInt(key, value, null, -1); case KEY_SCREEN_FACTOR_ROTATION: @@ -50,6 +53,7 @@ private boolean processConfigSet(String key, String value) { case KEY_HIGHLIGHT_REPLAY_NODE: case KEY_REPLAY_AUTO_START: case KEY_SCREEN_ROTATION: + case KEY_RECORD_COVER_MODE: if (StringUtil.equalsIgnoreCase(value, "true")) { LogUtil.i(TAG, "Update Config " + key + " to value " + true); SPService.putBoolean(key, true); @@ -60,6 +64,12 @@ private boolean processConfigSet(String key, String value) { return false; } break; + case KEY_CONTROL_PORT: + boolean processed = processInt(key, value, 65535, 5000); + if (processed) { + LauncherApplication.getInstance().startHttpServerAtPort(SPService.getInt(KEY_CONTROL_PORT, 23342)); + } + return processed; case KEY_GLOBAL_SETTINGS: JSONObject obj = JSON.parseObject(value); if (obj == null) { diff --git a/src/app/src/main/java/com/alipay/hulu/scheme/PerformanceSchemeResolver.java b/src/app/src/main/java/com/alipay/hulu/scheme/PerformanceSchemeResolver.java index 39f09f7..bfe005b 100644 --- a/src/app/src/main/java/com/alipay/hulu/scheme/PerformanceSchemeResolver.java +++ b/src/app/src/main/java/com/alipay/hulu/scheme/PerformanceSchemeResolver.java @@ -30,6 +30,7 @@ import com.alipay.hulu.common.scheme.SchemeResolver; import com.alipay.hulu.common.tools.AppInfoProvider; import com.alipay.hulu.common.tools.BackgroundExecutor; +import com.alipay.hulu.common.utils.Callback; import com.alipay.hulu.common.utils.PermissionUtil; import com.alipay.hulu.common.utils.StringUtil; import com.alipay.hulu.screenRecord.Notifications; @@ -64,7 +65,7 @@ public class PerformanceSchemeResolver implements SchemeActionResolver { private boolean isRecording = false; @Override - public boolean processScheme(Context context, Map params) { + public boolean processScheme(Context context, Map params, Callback> callback) { String mode = params.get(PERFORMANCE_MODE); if (StringUtil.isEmpty(mode)) { return false; @@ -123,6 +124,7 @@ private boolean processNormalRecord(final Context context, Map p } } allPermissions.add("adb"); + allPermissions.add("powerSave"); PermissionUtil.requestPermissions(new ArrayList<>(allPermissions), (Activity) context, new PermissionUtil.OnPermissionCallback() { @Override @@ -137,7 +139,7 @@ public void onPermissionResult(boolean result, String reason) { // 逐项开启 displayProvider.stopAllDisplay(); for (String key : items) { - displayProvider.startDisplayByKey(key); + displayProvider.startDisplay(key); } displayProvider.startRecording(); notification = Notifications.generateNotificationBuilder(context) diff --git a/src/app/src/main/java/com/alipay/hulu/scheme/RecordSchemeResolver.java b/src/app/src/main/java/com/alipay/hulu/scheme/RecordSchemeResolver.java index 3f90230..7f7b2a9 100644 --- a/src/app/src/main/java/com/alipay/hulu/scheme/RecordSchemeResolver.java +++ b/src/app/src/main/java/com/alipay/hulu/scheme/RecordSchemeResolver.java @@ -26,6 +26,7 @@ import com.alipay.hulu.common.scheme.SchemeActionResolver; import com.alipay.hulu.common.scheme.SchemeResolver; import com.alipay.hulu.common.tools.BackgroundExecutor; +import com.alipay.hulu.common.utils.Callback; import com.alipay.hulu.common.utils.PermissionUtil; import com.alipay.hulu.common.utils.StringUtil; import com.alipay.hulu.service.CaseRecordManager; @@ -52,7 +53,7 @@ public class RecordSchemeResolver implements SchemeActionResolver { public static final String MODE_NORMAL = "normal"; @Override - public boolean processScheme(Context context, Map params) { + public boolean processScheme(Context context, Map params, Callback> callback) { String mode = params.get(RECORD_MODE); if (StringUtil.isEmpty(mode)) { return false; @@ -78,7 +79,7 @@ private boolean startNormalMode(final Context context, Map param } caseInfo.setRecordMode("local"); - PermissionUtil.requestPermissions(Arrays.asList("adb", "float", Settings.ACTION_ACCESSIBILITY_SETTINGS), (Activity) context, new PermissionUtil.OnPermissionCallback() { + PermissionUtil.requestPermissions(Arrays.asList("adb", "float", Settings.ACTION_ACCESSIBILITY_SETTINGS, "powerSave"), (Activity) context, new PermissionUtil.OnPermissionCallback() { @Override public void onPermissionResult(boolean result, String reason) { if (result) { diff --git a/src/app/src/main/java/com/alipay/hulu/scheme/ReplaySchemeResolver.java b/src/app/src/main/java/com/alipay/hulu/scheme/ReplaySchemeResolver.java index d034ee3..42386d1 100644 --- a/src/app/src/main/java/com/alipay/hulu/scheme/ReplaySchemeResolver.java +++ b/src/app/src/main/java/com/alipay/hulu/scheme/ReplaySchemeResolver.java @@ -23,6 +23,7 @@ import com.alipay.hulu.common.application.LauncherApplication; import com.alipay.hulu.common.scheme.SchemeActionResolver; import com.alipay.hulu.common.scheme.SchemeResolver; +import com.alipay.hulu.common.utils.Callback; import com.alipay.hulu.common.utils.PermissionUtil; import com.alipay.hulu.common.utils.StringUtil; import com.alipay.hulu.shared.io.bean.RecordCaseInfo; @@ -46,7 +47,7 @@ public class ReplaySchemeResolver implements SchemeActionResolver { public static final String MODE_NORMAL = "normal"; @Override - public boolean processScheme(Context context, Map params) { + public boolean processScheme(Context context, Map params, Callback> callback) { String mode = params.get(REPLAY_MODE); if (StringUtil.isEmpty(mode)) { return false; diff --git a/src/app/src/main/java/com/alipay/hulu/scheme/StatusSchemeResolver.java b/src/app/src/main/java/com/alipay/hulu/scheme/StatusSchemeResolver.java new file mode 100644 index 0000000..aa7f6d1 --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/scheme/StatusSchemeResolver.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.scheme; + +import android.content.Context; +import android.provider.Settings; +import android.util.Log; + +import com.alibaba.fastjson.JSONObject; +import com.alipay.hulu.common.application.LauncherApplication; +import com.alipay.hulu.common.injector.InjectorService; +import com.alipay.hulu.common.injector.param.Subscriber; +import com.alipay.hulu.common.injector.provider.Param; +import com.alipay.hulu.common.scheme.SchemeActionResolver; +import com.alipay.hulu.common.scheme.SchemeResolver; +import com.alipay.hulu.common.tools.BackgroundExecutor; +import com.alipay.hulu.common.utils.Callback; +import com.alipay.hulu.common.utils.LogUtil; +import com.alipay.hulu.common.utils.PermissionUtil; +import com.alipay.hulu.common.utils.StringUtil; +import com.alipay.hulu.shared.node.OperationService; +import com.alipay.hulu.shared.node.tree.AbstractNodeTree; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.alipay.hulu.shared.event.constant.Constant.RUNNING_STATUS; + +@SchemeResolver("status") +public class StatusSchemeResolver implements SchemeActionResolver { + private static final String TAG = StatusSchemeResolver.class.getSimpleName(); + + public static final String KEY_STATUS_TYPE = "type"; + public static final String KEY_STATUS = "status"; + public static final String KEY_PAGE = "page"; + + public StatusSchemeResolver() { + InjectorService.g().register(this); + } + + private String currentStatus = "none"; + + @Subscriber(@Param(RUNNING_STATUS)) + public void setCurrentStatus(String currentStatus) { + this.currentStatus = currentStatus; + } + + @Override + public boolean processScheme(Context context, Map params, final Callback> callback) { + String type = params.get(KEY_STATUS_TYPE); + if (StringUtil.isEmpty(type)) { + return false; + } + + LogUtil.i(TAG, "Status Scheme处理中,请求参数:" + params); + switch (type) { + case KEY_STATUS: + callback.onResult(Collections.singletonMap("status", currentStatus)); + return true; + case KEY_PAGE: + boolean isGranted = PermissionUtil.getPermissionStatus(context, "adb") && PermissionUtil.getPermissionStatus(context, Settings.ACTION_ACCESSIBILITY_SETTINGS); + if (!isGranted) { + final AtomicBoolean permissionResult = new AtomicBoolean(false); + final CountDownLatch latch = new CountDownLatch(1); + PermissionUtil.requestPermissions(Arrays.asList("adb", Settings.ACTION_ACCESSIBILITY_SETTINGS), LauncherApplication.getInstance().getBestForegroundContext(), new PermissionUtil.OnPermissionCallback() { + @Override + public void onPermissionResult(boolean result, String reason) { + permissionResult.set(result); + latch.countDown(); + } + }); + + try { + latch.await(); + } catch (InterruptedException e) { + Log.e(TAG, "等待权限授予失败", e); + } + if (!permissionResult.get()) { + callback.onResult(Collections.singletonMap("error", "未授予权限")); + return true; + } + } + // 等500ms后再加载页面信息 + final CountDownLatch getNodeLatch = new CountDownLatch(1); + BackgroundExecutor.execute(new Runnable() { + @Override + public void run() { + OperationService service = LauncherApplication.service(OperationService.class); + AbstractNodeTree root = service.getBaseCurrentRoot(); + + // 构造可传输的树结构 + JSONObject obj = root.exportToJsonObject(); + + callback.onResult(Collections.singletonMap("page", obj)); + service.invalidRoot(); + getNodeLatch.countDown(); + } + }, 500); + try { + getNodeLatch.await(); + } catch (InterruptedException e) { + LogUtil.e(TAG, "Load node failed", e); + } + + return true; + } + + return false; + } +} diff --git a/src/app/src/main/java/com/alipay/hulu/service/BaseService.java b/src/app/src/main/java/com/alipay/hulu/service/BaseService.java index 5d886ef..3f5b8bc 100644 --- a/src/app/src/main/java/com/alipay/hulu/service/BaseService.java +++ b/src/app/src/main/java/com/alipay/hulu/service/BaseService.java @@ -15,21 +15,18 @@ */ package com.alipay.hulu.service; -import android.annotation.TargetApi; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.Service; import android.content.Context; +import android.content.Intent; import android.content.res.Configuration; -import android.content.res.Resources; import android.os.Build; -import android.os.LocaleList; +import android.os.Looper; import com.alipay.hulu.common.application.LauncherApplication; -import com.alipay.hulu.common.service.SPService; - -import java.util.Locale; +import com.alipay.hulu.common.utils.ContextUtil; /** * 应用启动的Service,目前只需要FloatWinService来承载 @@ -39,6 +36,20 @@ public abstract class BaseService extends Service { private static final String HULU_SERVICE_CHANNEL_ID = "hulu-service"; protected NotificationManager mNotificationManager; + @Override + public void startActivity(final Intent intent) { + if (Thread.currentThread() != Looper.getMainLooper().getThread()) { + LauncherApplication.getInstance().runOnUiThread(new Runnable() { + @Override + public void run() { + BaseService.super.startActivity(intent); + } + }); + } else { + super.startActivity(intent); + } + } + @Override public void onCreate() { super.onCreate(); @@ -56,21 +67,14 @@ public void onDestroy() { @Override protected void attachBaseContext(Context newBase) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - newBase = updateResources(newBase); - } super.attachBaseContext(newBase); + ContextUtil.updateResources(this); } - @TargetApi(Build.VERSION_CODES.N) - private static Context updateResources(Context context) { - Resources resources = context.getResources(); - Locale locale = LauncherApplication.getInstance().getLanguageLocale(); - - Configuration configuration = resources.getConfiguration(); - configuration.setLocale(locale); - configuration.setLocales(new LocaleList(locale)); - return context.createConfigurationContext(configuration); + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + ContextUtil.updateResources(this); } public Notification.Builder generateNotificationBuilder() { diff --git a/src/app/src/main/java/com/alipay/hulu/service/CaseRecordManager.java b/src/app/src/main/java/com/alipay/hulu/service/CaseRecordManager.java index c05c888..f9cc619 100644 --- a/src/app/src/main/java/com/alipay/hulu/service/CaseRecordManager.java +++ b/src/app/src/main/java/com/alipay/hulu/service/CaseRecordManager.java @@ -15,6 +15,8 @@ */ package com.alipay.hulu.service; +import static com.alipay.hulu.shared.node.action.Constant.TRIGGER_INPUT_METHOD; + import android.app.ProgressDialog; import android.content.ComponentName; import android.content.Context; @@ -31,13 +33,18 @@ import android.text.TextUtils; import android.util.DisplayMetrics; import android.util.Pair; +import android.view.Gravity; import android.view.LayoutInflater; +import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; import android.widget.LinearLayout; import android.widget.TextView; +import androidx.appcompat.app.AlertDialog; + import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; import com.alipay.hulu.R; import com.alipay.hulu.activity.MyApplication; import com.alipay.hulu.activity.NewRecordActivity; @@ -55,6 +62,7 @@ import com.alipay.hulu.common.service.SPService; import com.alipay.hulu.common.service.ScreenCaptureService; import com.alipay.hulu.common.service.TouchService; +import com.alipay.hulu.common.service.base.AppGuardian; import com.alipay.hulu.common.service.base.ExportService; import com.alipay.hulu.common.service.base.LocalService; import com.alipay.hulu.common.tools.BackgroundExecutor; @@ -69,8 +77,8 @@ import com.alipay.hulu.event.ScanSuccessEvent; import com.alipay.hulu.shared.event.EventService; import com.alipay.hulu.shared.event.accessibility.AccessibilityServiceImpl; -import com.alipay.hulu.shared.event.bean.UniversalEventBean; import com.alipay.hulu.shared.event.constant.Constant; +import com.alipay.hulu.shared.event.touch.TouchWrapper; import com.alipay.hulu.shared.io.OperationStepService; import com.alipay.hulu.shared.io.bean.OperationStepMessage; import com.alipay.hulu.shared.io.bean.RecordCaseInfo; @@ -84,15 +92,19 @@ import com.alipay.hulu.shared.node.action.provider.ActionProviderManager; import com.alipay.hulu.shared.node.locater.PositionLocator; import com.alipay.hulu.shared.node.tree.AbstractNodeTree; +import com.alipay.hulu.shared.node.tree.InputWindowTree; import com.alipay.hulu.shared.node.tree.accessibility.AccessibilityNodeProcessor; import com.alipay.hulu.shared.node.tree.accessibility.AccessibilityProvider; +import com.alipay.hulu.shared.node.tree.accessibility.tree.AccessibilityNodeTree; import com.alipay.hulu.shared.node.tree.capture.CaptureTree; import com.alipay.hulu.shared.node.tree.export.OperationStepExporter; import com.alipay.hulu.shared.node.tree.export.bean.OperationStep; import com.alipay.hulu.shared.node.utils.BitmapUtil; +import com.alipay.hulu.shared.node.utils.NodeContext; import com.alipay.hulu.shared.node.utils.PrepareUtil; import com.alipay.hulu.shared.node.utils.RectUtil; import com.alipay.hulu.shared.scan.ScanCodeType; +import com.alipay.hulu.status.StatusListener; import com.alipay.hulu.tools.HighLightService; import com.alipay.hulu.ui.TwoLevelSelectLayout; import com.alipay.hulu.util.DialogUtils; @@ -102,6 +114,7 @@ import java.io.File; import java.lang.ref.WeakReference; import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -112,11 +125,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import androidx.appcompat.app.AlertDialog; - -import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_UP; -import static com.alipay.hulu.shared.event.constant.Constant.KEY_TOUCH_POINT; - /** * 操作录制服务 * @@ -142,6 +150,12 @@ public class CaseRecordManager implements ExportService { // 操作日志输出Handler protected OperationStepService operationStepService; + /** + * 状态监听器 + */ + protected StatusListener statusListener; + + protected volatile boolean displayDialog = false; /** @@ -163,9 +177,11 @@ public class CaseRecordManager implements ExportService { protected OperationStepExporter stepProvider; + protected volatile OperationContext executingContext; + private WindowManager windowManager; - private String app; + protected String app; protected RecordCaseInfo caseInfo; @@ -185,9 +201,17 @@ public class CaseRecordManager implements ExportService { // 截图服务 private ScreenCaptureService captureService; - private static FloatWinService.OnFloatListener DEFAULT_FLOAT_LISTENER = new FloatWinService.OnFloatListener() { + private FloatWinService.OnFloatListener DEFAULT_FLOAT_LISTENER = new FloatWinService.OnFloatListener() { @Override public void onFloatClick(boolean hide) { + if (isRecording && isExecuting) { + if (executingContext != null) { + executingContext.cancelRunning(); + } + setServiceToTouchBlockMode(); + operationService.invalidRoot(); + notifyDialogDismiss(1000); + } } }; @@ -229,14 +253,13 @@ public void onCreate(Context context) { PermissionUtil.grantHighPrivilegePermission(LauncherApplication.getContext()); currentRecordId = StringUtil.generateRandomString(10); - setServiceToNormalMode(); +// setServiceToNormalMode(); // 启动悬浮窗 connection = new RecordFloatConnection(this); listener = new FloatClickListener(this); stopListener = new FloatStopListener(); - - context.getApplicationContext().bindService(new Intent(context.getApplicationContext(), FloatWinService.class), connection, Context.BIND_AUTO_CREATE); + context.bindService(new Intent(context, FloatWinService.class), connection, Context.BIND_AUTO_CREATE); // 开始扩展功能处理 operationService.startExtraActionHandle(); @@ -287,6 +310,18 @@ public void onScanEvent(final ScanSuccessEvent event) { } } + @Subscriber(@Param(value = LauncherApplication.SYSTEM_GUARDIAN_EVENT, sticky = false)) + public void onSystemEvent(AppGuardian.ReceiveSystemEvent event) { + if (!isRecording || isExecuting) { + return; + } + if (event == AppGuardian.ReceiveSystemEvent.SCREEN_LOCK && !pauseFlag && !displayDialog) { + processAction(new OperationMethod(PerformActionEnum.PAUSE), null, binder.loadServiceContext()); + } else if (event == AppGuardian.ReceiveSystemEvent.SCREEN_UNLOCK && pauseFlag) { + processAction(new OperationMethod(PerformActionEnum.RESUME), null, binder.loadServiceContext()); + } + } + @Subscriber(@Param(value = CmdTools.FATAL_ADB_CANNOT_RECOVER, sticky = false)) public void notifyAdbClose() { // 先暂停,等ADB恢复 @@ -328,6 +363,13 @@ public void setRecordCase(RecordCaseInfo caseInfo) { return; } + if (statusListener != null) { + JSONObject obj = new JSONObject(); + obj.put("case", caseInfo); + obj.put("time", System.currentTimeMillis()); + statusListener.onStatusChange(StatusListener.STATUS_START, obj); + } + this.caseInfo = caseInfo; // 重置RecordId和operationIdx @@ -396,6 +438,12 @@ public void run() { } }); } + + if (statusListener != null) { + JSONObject obj = new JSONObject(); + obj.put("time", System.currentTimeMillis()); + statusListener.onStatusChange(StatusListener.STATUS_PREPARED, obj); + } } } @@ -409,9 +457,12 @@ public void startRecord() { isRecording = true; displayDialog = true; + InjectorService.g().pushMessage(Constant.RUNNING_STATUS, "record"); + // 先记录下默认输入法 defaultIme = CmdTools.execHighPrivilegeCmd("settings get secure default_input_method"); - MyApplication.getInstance().updateDefaultIme("com.alipay.hulu/.tools.AdbIME"); + MyApplication.getInstance().updateDefaultIme("com.alipay.hulu/.common.tools.AdbIME"); + CmdTools.switchToIme("com.alipay.hulu/.common.tools.AdbIME"); // 初始化 operationStepService.startRecord(caseInfo); @@ -437,6 +488,11 @@ public void startRecord() { // 重载下当前界面 operationService.invalidRoot(); + // 覆盖模式不记录操作位置 + if (SPService.getBoolean(SPService.KEY_RECORD_COVER_MODE, false)) { + eventService.stopTrackTouch(); + } + // 通知进入触摸屏蔽模式 setServiceToTouchBlockModeNoDelay(); @@ -444,8 +500,25 @@ public void startRecord() { notifyDialogDismiss(1000); operationService.invalidRoot(); + + TouchWrapper.getInstance().listen(gestureListener); + TouchWrapper.getInstance().start(); + + // 执行准备操作 + AdvanceCaseSetting setting = JSON.parseObject(caseInfo.getAdvanceSettings(), AdvanceCaseSetting.class); + if (setting != null && setting.getPrepareActions() != null) { + List steps = setting.getPrepareActions(); + for (OperationStep step: steps) { + // 直接操作不录制 + if (step.getOperationNode() == null && step.getOperationMethod() != null) { + operationService.doSomeAction(step.getOperationMethod(), null); + } + } + } } + private View coverView = null; + /** * 进入触摸屏蔽模式 */ @@ -467,38 +540,433 @@ protected void setServiceToTouchBlockModeNoDelay() { } LogUtil.d(TAG, "进入触摸阻塞模式"); touchBlockMode = true; - injectorService.pushMessage(com.alipay.hulu.shared.event.constant.Constant.EVENT_ACCESSIBILITY_MODE, AccessibilityServiceImpl.MODE_BLOCK); + // 可选CoverMode + if (SPService.getBoolean(SPService.KEY_RECORD_COVER_MODE, false)) { + LauncherApplication.getInstance().runOnUiThread(new Runnable() { + @Override + public void run() { + if (coverView == null) { + coverView = LayoutInflater.from(binder.loadServiceContext()).inflate(R.layout.record_cover_view, null); + WindowManager.LayoutParams wmParams = new WindowManager.LayoutParams(); + wmParams.type = com.alipay.hulu.common.constant.Constant.TYPE_ALERT; + wmParams.flags |= 8; + wmParams.gravity = Gravity.LEFT | Gravity.TOP; // 调整悬浮窗口至左上角 + // 以屏幕左上角为原点,设置x、y初始值 + wmParams.x = 0; + wmParams.y = 0; + // 设置悬浮窗口长宽数据 + wmParams.width = WindowManager.LayoutParams.MATCH_PARENT; + wmParams.height = WindowManager.LayoutParams.MATCH_PARENT; + wmParams.format = 1; + wmParams.alpha = 1F; + + coverView.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (SPService.getBoolean(SPService.KEY_USE_EASY_MODE, false)) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + TouchWrapper.getInstance().receiveTouchDown(event.getEventTime()); + TouchWrapper.getInstance().receiveTouchPosition(new Point((int) event.getRawX(), (int) event.getRawY()), event.getEventTime()); + } else if (event.getAction() == MotionEvent.ACTION_MOVE) { + TouchWrapper.getInstance().receiveTouchPosition(new Point((int) event.getRawX(), (int) event.getRawY()), event.getEventTime()); + } else if (event.getAction() == MotionEvent.ACTION_UP) { + TouchWrapper.getInstance().receiveTouchPosition(new Point((int) event.getRawX(), (int) event.getRawY()), event.getEventTime()); + TouchWrapper.getInstance().receiveTouchUp(event.getEventTime()); + } + } else { + if (event.getAction() == MotionEvent.ACTION_UP) { + receiveClickPosition(new Point((int) event.getRawX(), (int) event.getRawY())); + } + } + return false; + } + }); + + coverView.setFocusable(false); + + binder.addView(coverView, wmParams); + } + + coverView.setVisibility(View.VISIBLE); + } + }); + } else { + // 延迟500ms + injectorService.pushMessage(com.alipay.hulu.shared.event.constant.Constant.EVENT_ACCESSIBILITY_MODE, AccessibilityServiceImpl.MODE_BLOCK); + } } + private long startCallTime = 0; + /** * 进入正常模式 */ protected void setServiceToNormalMode() { touchBlockMode = false; LogUtil.d(TAG, "进入正常触摸模式"); - // 200ms后点击 - injectorService.pushMessage(com.alipay.hulu.shared.event.constant.Constant.EVENT_ACCESSIBILITY_MODE, AccessibilityServiceImpl.MODE_NORMAL, 200); + + // 可选CoverMode + if (SPService.getBoolean(SPService.KEY_RECORD_COVER_MODE, false)) { + LauncherApplication.getInstance().runOnUiThread(new Runnable() { + @Override + public void run() { + if (coverView != null) { + coverView.setVisibility(View.GONE); + } + } + }, 200); + } else { + // 200ms后点击 + injectorService.pushMessage(com.alipay.hulu.shared.event.constant.Constant.EVENT_ACCESSIBILITY_MODE, AccessibilityServiceImpl.MODE_NORMAL, 200); + } + } + + /** + * 进入正常模式 + */ + protected void setServiceToNormalModeNoDelay() { + touchBlockMode = false; + LogUtil.d(TAG, "进入正常触摸模式"); + + // 可选CoverMode + if (SPService.getBoolean(SPService.KEY_RECORD_COVER_MODE, false)) { + LauncherApplication.getInstance().runOnUiThread(new Runnable() { + @Override + public void run() { + if (coverView != null) { + coverView.setVisibility(View.GONE); + } + } + }); + } else { + // 200ms后点击 + injectorService.pushMessage(com.alipay.hulu.shared.event.constant.Constant.EVENT_ACCESSIBILITY_MODE, AccessibilityServiceImpl.MODE_NORMAL); + } } - private UniversalEventBean touchPos; + /** + * 手势监听器 + */ + protected TouchWrapper.GestureListener gestureListener = new TouchWrapper.GestureListener() { + @Override + public void receiveClick(Point point) { + if (SPService.getBoolean(SPService.KEY_USE_EASY_MODE, false)) { + receiveDirectClick(point); + } else { + receiveClickPosition(point); + } + } + + @Override + public void receiveLongClick(Point p, long time) { + receiveClickPosition(p); + } + + @Override + public void receiveScroll(Point start, Point end, long time) { + if (SPService.getBoolean(SPService.KEY_USE_EASY_MODE, false)) { + receiveDirectScroll(start, end, time); + } else { + receiveClickPosition(end); + } + } + }; + + /** + * 收到直接点击 + * @param point + */ + protected void receiveDirectClick(Point point) { + if (point == null) { + LogUtil.w(TAG, "收到空触摸消息"); + return; + } + + // 非触摸阻塞模式 + if (!touchBlockMode) { + LogUtil.d(TAG, "当前非阻塞模式"); + return; + } + + LogUtil.d(TAG, "Receive Touch at time " + System.currentTimeMillis()); + + int x = point.x; + int y = point.y; + + // 只针对显示dialog的情况 + if (displayDialog || pauseFlag || nodeLoading || isExecuting || !isRecording) { + if (isExecuting && isRecording) { + if (binder.checkInFloat(point)) { + LogUtil.i(TAG, "录制时点到了SoloPi"); + if (executingContext != null) { + executingContext.cancelRunning(); + } + setServiceToTouchBlockMode(); + operationService.invalidRoot(); + notifyDialogDismiss(1000); + } + } + return; + } + + LogUtil.i(TAG, "Start notify Touch Event at (%d, %d)", x, y); + + // 看下是否点到SoloPi图标 + if (binder.checkInFloat(point)) { + LogUtil.i(TAG, "点到了SoloPi"); + startCallTime = System.currentTimeMillis(); + showFunctionView(null); + return; + } + + setServiceToNormalModeNoDelay(); + + nodeLoading = true; + try { + AbstractNodeTree root = operationService.getCurrentRoot(); + + // 如果有显示输入法框,找有input focus的输入框 + NodeContext context = operationService.getNodeContext(); + if (context != null && StringUtil.equals(context.getField(TRIGGER_INPUT_METHOD, ""), "true")) { + AbstractNodeTree node = null; + for (AbstractNodeTree tmp: root) { + if (tmp.getNodeBound().contains(x, y)) { + if (tmp instanceof AccessibilityNodeTree) { + // 找输入框 + if (((AccessibilityNodeTree) tmp).isEditable() && ((AccessibilityNodeTree) tmp).getCurrentNode().isFocused()) { + node = tmp; + break; + } + } + } + } + + + // 如果找到了待输入控件 + if (node != null) { + + // 先切换到默认输入法 + CmdTools.switchToIme(defaultIme); + + displayDialog = true; + + highLightService.highLight(node.getNodeBound(), null); + final AbstractNodeTree finalNode = node; + LauncherApplication.getInstance().runOnUiThread(new Runnable() { + @Override + public void run() { + FunctionSelectUtil.showEditView(finalNode, new OperationMethod(PerformActionEnum.INPUT), + binder.loadServiceContext(), new FunctionSelectUtil.FunctionListener() { + @Override + public void onProcessFunction(final OperationMethod method, final AbstractNodeTree node) { + highLightService.removeHightLightSync(); + + // 切换回SoloPi输入法 + CmdTools.switchToIme("com.alipay.hulu/.common.tools.AdbIME"); + + // 等悬浮窗消失了再操作 + BackgroundExecutor.execute(new Runnable() { + @Override + public void run() { + LogUtil.d(TAG, "开始执行操作"); + boolean result = processAction(method, node, binder.loadServiceContext()); + + // 是否需要处理 + if (!result) { + setServiceToTouchBlockMode(); + notifyDialogDismiss(); + } + } + }, 50); + } + + @Override + public void onCancel() { + highLightService.removeHightLightSync(); + + setServiceToTouchBlockModeNoDelay(); + notifyDialogDismiss(); + } + }); + } + }); + return; + } + } + + final AbstractNodeTree node = PositionLocator.findDeepestNode(root, x, y); + LogUtil.i(TAG, "目标节点:%s", node); + + // 节点没拿到 + if (node == null) { + LogUtil.e(TAG, "Get node at (" + x + ", " + y + ") null"); + setServiceToTouchBlockMode(); + return; + } + + if (node instanceof InputWindowTree) { + + AbstractNodeTree targetNode = null; + for (AbstractNodeTree tmp: root) { + if (tmp instanceof AccessibilityNodeTree) { + // 找输入框 + if (((AccessibilityNodeTree) tmp).isEditable() && ((AccessibilityNodeTree) tmp).getCurrentNode().isFocused()) { + targetNode = tmp; + break; + } + } + } + + + // 如果找到了待输入控件 + if (targetNode != null) { + + // 先切换到默认输入法 + CmdTools.switchToIme(defaultIme); + + displayDialog = true; + + highLightService.highLight(targetNode.getNodeBound(), null); + final AbstractNodeTree finalNode = targetNode; + LauncherApplication.getInstance().runOnUiThread(new Runnable() { + @Override + public void run() { + FunctionSelectUtil.showEditView(finalNode, new OperationMethod(PerformActionEnum.INPUT), + binder.loadServiceContext(), new FunctionSelectUtil.FunctionListener() { + @Override + public void onProcessFunction(final OperationMethod method, final AbstractNodeTree node) { + highLightService.removeHightLightSync(); + + // 切换回SoloPi输入法 + CmdTools.switchToIme("com.alipay.hulu/.common.tools.AdbIME"); + + // 等悬浮窗消失了再操作 + BackgroundExecutor.execute(new Runnable() { + @Override + public void run() { + LogUtil.d(TAG, "开始执行操作"); + boolean result = processAction(method, node, binder.loadServiceContext()); + + // 是否需要处理 + if (!result) { + setServiceToTouchBlockMode(); + notifyDialogDismiss(); + } + } + }, 50); + } + + @Override + public void onCancel() { + highLightService.removeHightLightSync(); + + setServiceToTouchBlockModeNoDelay(); + notifyDialogDismiss(); + } + }); + } + }); + return; + } else { + LogUtil.w(TAG, "Can't find target node, even input method is display"); + LauncherApplication.getInstance().showToast("未能找到输入控件,无法操作"); + setServiceToTouchBlockMode(); + return; + } + } + + Rect bound = node.getNodeBound(); + float xFactor = (x - bound.left) / (float) bound.width(); + float yFactor = (y - bound.top) / (float) bound.height(); + + final OperationMethod method = new OperationMethod(PerformActionEnum.CLICK); + // 添加控件点击位置 + method.putParam(OperationExecutor.LOCAL_CLICK_POS_KEY, xFactor + "," + yFactor); +// startCallTime = System.currentTimeMillis(); + highLightService.highLight(bound, point); + BackgroundExecutor.execute(new Runnable() { + @Override + public void run() { + highLightService.removeHightLightSync(); + boolean result = CaseRecordManager.this.processAction(method, node, binder.loadServiceContext()); + if (!result) { + CaseRecordManager.this.setServiceToTouchBlockMode(); + CaseRecordManager.this.notifyDialogDismiss(); + } + } + }, 200); - @Subscriber(value = @Param(Constant.EVENT_TOUCH_POSITION), thread = RunningThread.BACKGROUND) - public void receiveTouchPos(UniversalEventBean eventBean) { - this.touchPos = eventBean; + + } finally { + nodeLoading = false; + } } - @Subscriber(value = @Param(Constant.EVENT_TOUCH_UP), thread = RunningThread.BACKGROUND) - public void receiveTouchUp(UniversalEventBean event) { - if (touchPos != null) { - receiveTouchPosition(touchPos); - touchPos = null; + /** + * 收到直接滑动 + * @param start + * @param end + * @param time + */ + protected void receiveDirectScroll(Point start, Point end, long time) { + if (start == null || end == null) { + LogUtil.w(TAG, "收到空触摸消息"); + return; + } + + // 非触摸阻塞模式 + if (!touchBlockMode) { + LogUtil.d(TAG, "当前非阻塞模式"); + return; + } + + LogUtil.d(TAG, "Receive Touch at time " + System.currentTimeMillis()); + + // 只针对显示dialog的情况 + if (displayDialog || pauseFlag || nodeLoading || isExecuting || !isRecording) { + return; + } + + setServiceToNormalModeNoDelay(); + + LogUtil.i(TAG, "Receive scroll from %s to %s", start, end); + int xDistance = end.x - start.x; + int yDistance = end.y - start.y; + DisplayMetrics dm = new DisplayMetrics(); + ((WindowManager) LauncherApplication.getInstance().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getRealMetrics(dm); + int height = dm.heightPixels; + int width = dm.widthPixels; + + OperationMethod method = new OperationMethod(); + method.putParam(OperationExecutor.SCROLL_TIME, Long.toString(time)); + if (Math.abs(xDistance) > Math.abs(yDistance)) { + if (xDistance < 0) { + method.setActionEnum(PerformActionEnum.GLOBAL_SCROLL_TO_RIGHT); + } else { + method.setActionEnum(PerformActionEnum.GLOBAL_SCROLL_TO_LEFT); + } + + method.putParam(OperationExecutor.SCROLL_DISTANCE, Integer.toString((int) (Math.abs(xDistance) / (float) width))); + } else { + if (yDistance < 0) { + method.setActionEnum(PerformActionEnum.GLOBAL_SCROLL_TO_TOP); + } else { + method.setActionEnum(PerformActionEnum.GLOBAL_SCROLL_TO_BOTTOM); + } + method.putParam(OperationExecutor.SCROLL_DISTANCE, Integer.toString((int) (Math.abs(yDistance) / (float) height))); + } + + boolean result = processAction(method, null, binder.loadServiceContext()); + if (!result) { + setServiceToTouchBlockMode(); + notifyDialogDismiss(); } } - public void receiveTouchPosition(UniversalEventBean eventBean) { - Point point = eventBean.getParam(KEY_TOUCH_POINT); +// @Subscriber(value = @Param(com.alipay.hulu.shared.event.constant.Constant.EVENT_TOUCH_POSITION), thread = RunningThread.BACKGROUND) + public void receiveClickPosition(Point point) { if (point == null) { - LogUtil.w(TAG, "收到空触摸消息【%s】", eventBean); + LogUtil.w(TAG, "收到空触摸消息"); return; } @@ -508,7 +976,7 @@ public void receiveTouchPosition(UniversalEventBean eventBean) { return; } - LogUtil.d(TAG, "Receive Touch at time " + eventBean.getTime()); + LogUtil.d(TAG, "Receive Touch at time " + System.currentTimeMillis()); int x = point.x; int y = point.y; @@ -523,6 +991,7 @@ public void receiveTouchPosition(UniversalEventBean eventBean) { // 看下是否点到SoloPi图标 if (binder.checkInFloat(point)) { LogUtil.i(TAG, "点到了SoloPi"); + startCallTime = System.currentTimeMillis(); showFunctionView(null); return; } @@ -545,6 +1014,7 @@ public void receiveTouchPosition(UniversalEventBean eventBean) { float yFactor = (y - bound.top) / (float) bound.height(); localClickPos = new Pair<>(xFactor, yFactor); + startCallTime = System.currentTimeMillis(); showFunctionView(node); } finally { nodeLoading = false; @@ -651,6 +1121,11 @@ public void notifyOperationFinish() { } updateFloatIcon(R.drawable.solopi_float); } + + @Override + public void onContextReceive(OperationContext context) { + executingContext = context; + } }); } catch (Exception e) { LogUtil.e(TAG, "doRecord action throw : " + e.getMessage(), e); @@ -707,6 +1182,7 @@ public void run() { clickActions.add(convertPerformActionToSubMenu(PerformActionEnum.CLICK_IF_EXISTS)); clickActions.add(convertPerformActionToSubMenu(PerformActionEnum.CLICK_QUICK)); clickActions.add(convertPerformActionToSubMenu(PerformActionEnum.MULTI_CLICK)); + clickActions.add(convertPerformActionToSubMenu(PerformActionEnum.CLICK_AND_INPUT)); NODE_ACTION_MAP.put(R.string.function_group__click, clickActions); NODE_KEYS.add(R.string.function_group__input); @@ -774,6 +1250,8 @@ public void run() { gScrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.GLOBAL_SCROLL_TO_TOP)); gScrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.GLOBAL_SCROLL_TO_LEFT)); gScrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.GLOBAL_SCROLL_TO_RIGHT)); + gScrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.KEYBOARD_INPUT)); + gScrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.INPUT_GLOBAL)); gScrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.GLOBAL_PINCH_OUT)); gScrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.GLOBAL_PINCH_IN)); gScrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.GLOBAL_GESTURE)); @@ -924,10 +1402,10 @@ public void onProcessFunction(final OperationMethod method, final AbstractNodeTr LogUtil.d(TAG, "悬浮窗消失"); // 切换回SoloPi输入法 - CmdTools.switchToIme("com.alipay.hulu/.tools.AdbIME"); + CmdTools.switchToIme("com.alipay.hulu/.common.tools.AdbIME"); // 等悬浮窗消失了再操作 - LauncherApplication.getInstance().runOnUiThread(new Runnable() { + BackgroundExecutor.execute(new Runnable() { @Override public void run() { // 返回是否处理完毕 @@ -1024,10 +1502,22 @@ protected boolean processAction(OperationMethod method, AbstractNodeTree node, f eventService.stopTrackAccessibilityEvent(); eventService.stopTrackTouch(); + LauncherApplication.getInstance().stopServiceByName(OperationService.class.getName()); setServiceToNormalMode(); + if (statusListener != null) { + JSONObject obj = new JSONObject(); + obj.put("time", System.currentTimeMillis()); + if (caseInfo.getId() > 0) { + obj.put("id", caseInfo.getId()); + } + obj.put("caseName", caseInfo.getCaseName()); + statusListener.onStatusChange(StatusListener.STATUS_STOP, obj); + } + if (!processed) { + binder.restoreFloat(); // 恢复悬浮窗 binder.restoreFloat(); Intent intent = new Intent(context, NewRecordActivity.class); @@ -1039,6 +1529,14 @@ protected boolean processAction(OperationMethod method, AbstractNodeTree node, f } else { LauncherApplication.getInstance().stopServiceByName(CaseRecordManager.class.getName()); } + + TouchWrapper.getInstance().cancelListen(gestureListener); + TouchWrapper.getInstance().stop(); + + // 不能影响其他操作 + if (SPService.getBoolean(SPService.KEY_USE_EASY_MODE, false)) { + SPService.putBoolean(SPService.KEY_USE_EASY_MODE, false); + } return true; } else if (action == PerformActionEnum.PAUSE) { setServiceToNormalMode(); @@ -1150,18 +1648,6 @@ public boolean isSupportedDevice() { return true; } - @Subscriber(@Param(com.alipay.hulu.shared.event.constant.Constant.EVENT_ACCESSIBILITY_GESTURE)) - public void onGesture(UniversalEventBean gestureEvent) { - LogUtil.i(TAG, "System Call Gesture Method: " + gestureEvent); - - Integer gestureId; - if (gestureEvent != null && (gestureId = gestureEvent.getParam(com.alipay.hulu.shared.event.constant.Constant.KEY_GESTURE_TYPE)) != null) { - if (gestureId == GESTURE_SWIPE_UP && !displayDialog && !pauseFlag && !nodeLoading) { - showFunctionView(null); - } - } - } - public void onDestroy(Context context) { LogUtil.i(TAG, "onDestroy"); @@ -1232,7 +1718,7 @@ public void receiveDeviceInfoMessage(UIOperationMessage message) { * @param deviceInfo * @param context */ - public void showDialog(String title, String deviceInfo, Context context, long timeout) { + public void showDialog(final String title, String deviceInfo, Context context, long timeout) { if (TextUtils.isEmpty(deviceInfo)) { return; } @@ -1270,10 +1756,12 @@ public void showDialog(String title, String deviceInfo, Context context, long ti .setPositiveButton(R.string.constant__confirm, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - setServiceToTouchBlockMode(); + if (!"SLEEP".equals(title)) { + setServiceToTouchBlockMode(); + notifyDialogDismiss(2000); + } forceStopBlocking = false; dialog.dismiss(); - notifyDialogDismiss(2000); } }).create(); dialog.getWindow().setType(com.alipay.hulu.common.constant.Constant.TYPE_ALERT); @@ -1353,6 +1841,10 @@ private void executeDelay(Runnable runnable, long mill) { } } + public void registerStatusListener(StatusListener statusListener) { + this.statusListener = statusListener; + } + @Subscriber(@Param(SubscribeParamEnum.APP)) public void setApp(String app) { this.app = app; @@ -1372,7 +1864,7 @@ public void onServiceConnected(ComponentName name, IBinder service) { managerRef.get().provideDisplayContent(binder); binder.registerRunClickListener(managerRef.get().listener); binder.registerStopClickListener(managerRef.get().stopListener); - binder.registerFloatClickListener(DEFAULT_FLOAT_LISTENER); + binder.registerFloatClickListener(managerRef.get().DEFAULT_FLOAT_LISTENER); } @Override diff --git a/src/app/src/main/java/com/alipay/hulu/service/CaseReplayManager.java b/src/app/src/main/java/com/alipay/hulu/service/CaseReplayManager.java index 44e1e37..dc8d62d 100644 --- a/src/app/src/main/java/com/alipay/hulu/service/CaseReplayManager.java +++ b/src/app/src/main/java/com/alipay/hulu/service/CaseReplayManager.java @@ -152,6 +152,8 @@ public class CaseReplayManager implements ExportService { */ private InjectorService injectorService; + private volatile OperationContext runningContext; + /** * 用例运行器 */ @@ -434,8 +436,8 @@ public void run() { // 先记录下默认输入法 defaultIme = CmdTools.execHighPrivilegeCmd("settings get secure default_input_method"); - MyApplication.getInstance().updateDefaultIme("com.alipay.hulu/.tools.AdbIME"); - CmdTools.switchToIme("com.alipay.hulu/.tools.AdbIME"); + MyApplication.getInstance().updateDefaultIme("com.alipay.hulu/.common.tools.AdbIME"); + CmdTools.switchToIme("com.alipay.hulu/.common.tools.AdbIME"); // 初始化 stepCount = 1; @@ -551,6 +553,9 @@ private void suffixAction() { public void stopRunning() { this.runningFlag = false; + if (runningContext != null) { + runningContext.cancelRunning(); + } } @@ -611,9 +616,15 @@ public void notifyOperationFinish() { LogUtil.d(TAG, "当前操作【%s】执行完毕,执行耗时: %dms", method.getActionEnum().getDesc(), System.currentTimeMillis() - startTime); runningFlag.countDown(); } + + @Override + public void onContextReceive(OperationContext context) { + runningContext = context; + } }; // 对于需要操作节点的记录 + AbstractNodeTree node = null; if (operation.getOperationNode() != null) { // 解析Node数据 OperationNode origin = new OperationNode(operation.getOperationNode(), operationService); @@ -624,8 +635,6 @@ public void notifyOperationFinish() { MiscUtil.sleep(500); } List prepareActions = new ArrayList<>(); - - AbstractNodeTree node = null; if (operation.getOperationMethod().getActionEnum() == PerformActionEnum.CLICK_IF_EXISTS) { node = OperationUtil.findAbstractNodeWithoutScroll(origin, operationService, prepareActions); @@ -633,7 +642,7 @@ public void notifyOperationFinish() { LogUtil.i(TAG, "未查找到节点【%s】,不进行操作", origin); return null; } - } else if (operation.getOperationMethod().getActionEnum() == PerformActionEnum.CHECK_NODE) { + } else if (operation.getOperationMethod().getActionEnum() == PerformActionEnum.CHECK_NODE || operation.getOperationMethod().getActionEnum() == PerformActionEnum.CLICK_QUICK) { node = OperationUtil.findAbstractNodeWithoutScroll(origin, operationService, prepareActions); if (node == null) { @@ -724,58 +733,51 @@ public void notifyOperationFinish() { // 高亮下 highLightAndRemove(node, operation.getOperationMethod()); } + } else { + // 前一次操作时间有记录,需要Sleep这段时间 + watcher.sleepUntilContentDontChange(); + } - // 执行操作 - boolean result = operationService.doSomeAction(operation.getOperationMethod(), node, listener); - if (!result) { - return "执行失败"; - } - - OperationNode opNode = OperationStepExporter.exportNodeToOperationNode(node); - - // 等待操作结束 - try { - runningFlag.await(600 * 100, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - LogUtil.e(TAG, "Catch java.lang.InterruptedException: " + e.getMessage(), e); - } - // 输入操作会较耗时,需要等待下 - if (method.getActionEnum() == PerformActionEnum.INPUT || method.getActionEnum() == PerformActionEnum.INPUT_SEARCH) { - MiscUtil.sleep(1000); - watcher.sleepUntilContentDontChange(); - } + // 执行操作 + boolean result = operationService.doSomeAction(operation.getOperationMethod(), node, listener); + if (!result) { + return "执行失败"; + } - stepInfoBean.setFindNode(opNode); + OperationNode opNode = null; + if (node != null) { + opNode = OperationStepExporter.exportNodeToOperationNode(node); + } - provider.onStepInfo(stepInfoBean); + // 等待操作结束 + long sleepTime; + // 成功执行,需要等待10分钟 + // 等待操作结束 + if (node != null) { + sleepTime = 60; + } else if (operation.getOperationMethod().getActionEnum() != PerformActionEnum.SLEEP) { + sleepTime = 600; } else { - // 前一次操作时间有记录,需要Sleep这段时间 + sleepTime = 60 * 60; + } + try { + runningFlag.await(sleepTime, TimeUnit.SECONDS); + } catch (InterruptedException e) { + LogUtil.e(TAG, "Catch java.lang.InterruptedException: " + e.getMessage(), e); + } + + // 输入操作会较耗时,需要等待下 + if (method.getActionEnum() == PerformActionEnum.INPUT + || method.getActionEnum() == PerformActionEnum.INPUT_SEARCH + || method.getActionEnum() == PerformActionEnum.CLICK_AND_INPUT) { + MiscUtil.sleep(1000); watcher.sleepUntilContentDontChange(); + } - // 对于全局操作,直接执行 - boolean result = operationService.doSomeAction(method, null, listener); - if (!result) { - return "执行失败"; - } + stepInfoBean.setFindNode(opNode); - // 成功执行,需要等待10分钟 - // 等待操作结束 - if (operation.getOperationMethod().getActionEnum() != PerformActionEnum.SLEEP) { - try { - runningFlag.await(600, TimeUnit.SECONDS); - } catch (InterruptedException e) { - LogUtil.e(TAG, "Catch java.lang.InterruptedException: " + e.getMessage(), e); - } - } else { - // SLEEP特殊处理,等待1小时 - try { - runningFlag.await(60, TimeUnit.MINUTES); - } catch (InterruptedException e) { - LogUtil.e(TAG, "Catch java.lang.InterruptedException: " + e.getMessage(), e); - } - } - } + provider.onStepInfo(stepInfoBean); LogUtil.d(TAG, "操作执行完毕"); return null; } diff --git a/src/app/src/main/java/com/alipay/hulu/service/DisplayManager.java b/src/app/src/main/java/com/alipay/hulu/service/DisplayManager.java index 6a9bb76..4e22e77 100644 --- a/src/app/src/main/java/com/alipay/hulu/service/DisplayManager.java +++ b/src/app/src/main/java/com/alipay/hulu/service/DisplayManager.java @@ -29,6 +29,7 @@ import android.widget.LinearLayout; import com.alipay.hulu.R; +import com.alipay.hulu.adapter.FloatStressAdapter; import com.alipay.hulu.adapter.FloatWinAdapter; import com.alipay.hulu.common.application.LauncherApplication; import com.alipay.hulu.common.injector.InjectorService; @@ -71,6 +72,8 @@ public class DisplayManager { private FloatWinAdapter floatWinAdapter; + private FloatStressAdapter floatStressAdapter; + private int runningMode; private volatile boolean runningFlag = true; @@ -90,6 +93,10 @@ public boolean onStopClick() { private RecyclerView floatWinList; + private RecyclerView floatStressList; + + private View floatStressHide; + private DisplayConnection connection; private static DisplayManager instance; @@ -185,7 +192,7 @@ public synchronized List updateRecordingItems(List failed = new ArrayList<>(); if (newItems != null && newItems.size() > 0) { for (DisplayItemInfo info : newItems) { - boolean result = provider.startDisplay(info.getName()); + boolean result = provider.startDisplay(info.getKey()); // 失败项将取消 if (result) { @@ -284,8 +291,8 @@ private View provideMainView(Context context) { if (runningMode == DisplayProvider.RECORDING_MODE) { return null; } - - floatWinList = (RecyclerView) LayoutInflater.from(context).inflate(R.layout.display_main_layout, null); + View root = LayoutInflater.from(context).inflate(R.layout.display_main_layout, null); + floatWinList = root.findViewById(R.id.float_recycler_view); floatWinList.setLayoutManager(new LinearLayoutManager(context)); floatWinList.setOnTouchListener(new View.OnTouchListener() { @Override @@ -300,7 +307,37 @@ public boolean onTouch(View v, MotionEvent event) { floatWinList.addItemDecoration(new RecycleViewDivider(context, LinearLayoutManager.HORIZONTAL, 1, context.getResources().getColor(R.color.divider_color))); - return floatWinList; + floatStressList = root.findViewById(R.id.float_stress_recycler_view); + + floatStressList.setLayoutManager(new LinearLayoutManager(context)); + floatStressList.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + return false; + } + }); + + floatStressAdapter = new FloatStressAdapter(context); + floatStressList.setAdapter(floatStressAdapter); + // 添加分割线 + floatStressList.addItemDecoration(new RecycleViewDivider(context, + LinearLayoutManager.HORIZONTAL, 1, context.getResources().getColor(R.color.divider_color))); + + floatStressHide = root.findViewById(R.id.float_stress_hide); + floatStressHide.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (floatStressList.getVisibility() == View.VISIBLE) { + floatStressHide.setRotation(0); + floatStressList.setVisibility(View.GONE); + } else { + floatStressHide.setRotation(180); + floatStressList.setVisibility(View.VISIBLE); + } + } + }); + + return root; } private View provideExpendView(Context context) { diff --git a/src/app/src/main/java/com/alipay/hulu/service/FloatWinService.java b/src/app/src/main/java/com/alipay/hulu/service/FloatWinService.java index 9d56dd7..a557231 100644 --- a/src/app/src/main/java/com/alipay/hulu/service/FloatWinService.java +++ b/src/app/src/main/java/com/alipay/hulu/service/FloatWinService.java @@ -50,6 +50,7 @@ import com.alipay.hulu.activity.IndexActivity; import com.alipay.hulu.activity.MyApplication; import com.alipay.hulu.common.application.LauncherApplication; +import com.alipay.hulu.common.constant.Constant; import com.alipay.hulu.common.injector.InjectorService; import com.alipay.hulu.common.injector.param.RunningThread; import com.alipay.hulu.common.injector.param.SubscribeParamEnum; @@ -198,7 +199,7 @@ public void run() { } } - @Subscriber(@Param(LauncherApplication.SCREEN_ORIENTATION)) + @Subscriber(@Param(Constant.SCREEN_ORIENTATION)) public void setScreenOrientation(int orientation) { if (orientation != currentOrientation) { currentOrientation = orientation; diff --git a/src/app/src/main/java/com/alipay/hulu/status/StatusListener.java b/src/app/src/main/java/com/alipay/hulu/status/StatusListener.java new file mode 100644 index 0000000..c8fe33b --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/status/StatusListener.java @@ -0,0 +1,36 @@ +package com.alipay.hulu.status; + +import com.alibaba.fastjson.JSONObject; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import androidx.annotation.StringDef; + +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +public interface StatusListener { + public static final String STATUS_START = "START"; + public static final String STATUS_PREPARED = "PREPARED"; + public static final String STATUS_STOP = "STOP"; + public static final String STATUS_STEP = "STEP"; + + @StringDef({ + STATUS_START, + STATUS_PREPARED, + STATUS_STEP, + STATUS_STOP + }) + @Retention(SOURCE) + @Target({PARAMETER}) + @interface StateDefine{}; + + + /** + * 通知状态变化 + * @param state + * @param extra + */ + void onStatusChange(@StateDefine String state, JSONObject extra); +} diff --git a/src/app/src/main/java/com/alipay/hulu/status/impl/HttpStatusListener.java b/src/app/src/main/java/com/alipay/hulu/status/impl/HttpStatusListener.java new file mode 100644 index 0000000..f03570c --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/status/impl/HttpStatusListener.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.status.impl; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.alipay.hulu.common.utils.HttpUtil; +import com.alipay.hulu.common.utils.LogUtil; +import com.alipay.hulu.status.StatusListener; + +import java.io.IOException; + +import okhttp3.Call; +import okhttp3.MediaType; +import okhttp3.RequestBody; + +/** + * 基于HTTP的状态上报器 + */ +public class HttpStatusListener implements StatusListener { + private static final String TAG = HttpStatusListener.class.getSimpleName(); + private String reportUrl; + + private JSONObject reportExtra; + + private StatusListener wrapper; + + public HttpStatusListener(String reportUrl, JSONObject reportExtra) { + this.reportUrl = reportUrl; + this.reportExtra = new JSONObject(); + if (reportExtra != null) { + this.reportExtra.putAll(reportExtra); + } + } + + @Override + public void onStatusChange(String state, JSONObject extra) { + if (wrapper != null) { + wrapper.onStatusChange(state, extra); + } + JSONObject toReport = new JSONObject(reportExtra); + toReport.put("type", state); + toReport.put("value", extra); + + LogUtil.i(TAG, "Prepare to report status %s to url %s", state, reportUrl); + + HttpUtil.post(reportUrl, RequestBody.create(MediaType.get("application/json"), + JSON.toJSONBytes(toReport)), new HttpUtil.Callback(String.class) { + @Override + public void onFailure(Call call, IOException e) { + LogUtil.e(TAG, "Report status failed, throw exception", e); + } + + @Override + public void onResponse(Call call, String result) throws IOException { + LogUtil.i(TAG, "Report status finished, reponse:" + result); + } + }); + } + + public void setWrapper(StatusListener wrapper) { + this.wrapper = wrapper; + } +} diff --git a/src/app/src/main/java/com/alipay/hulu/tools/AdbIME.java b/src/app/src/main/java/com/alipay/hulu/tools/AdbIME.java deleted file mode 100644 index 9692bca..0000000 --- a/src/app/src/main/java/com/alipay/hulu/tools/AdbIME.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Copyright (C) 2015-present, Ant Financial Services Group - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.alipay.hulu.tools; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.inputmethodservice.InputMethodService; -import android.view.KeyEvent; -import android.view.View; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputConnection; -import android.view.inputmethod.InputMethodManager; - -import com.alipay.hulu.R; -import com.alipay.hulu.common.application.LauncherApplication; -import com.alipay.hulu.common.constant.Constant; -import com.alipay.hulu.common.injector.InjectorService; -import com.alipay.hulu.common.injector.param.RunningThread; -import com.alipay.hulu.common.injector.param.Subscriber; -import com.alipay.hulu.common.injector.provider.Param; -import com.alipay.hulu.common.tools.BackgroundExecutor; -import com.alipay.hulu.common.utils.MiscUtil; -import com.alipay.hulu.common.utils.StringUtil; - -/** - * Created by lezhou.wyl on 2018/2/8. - */ -public class AdbIME extends InputMethodService { - private static final String TAG = "AdbIME"; - public static final String MSG_HIDE_INPUT = "MSG_HIDE_INPUT"; - - public static final String IME_MESSAGE = "ADB_INPUT_TEXT"; - public static final String IME_SEARCH_MESSAGE = "ADB_SEARCH_TEXT"; - public static final String IME_CHARS = "ADB_INPUT_CHARS"; - public static final String IME_KEYCODE = "ADB_INPUT_CODE"; - public static final String IME_EDITORCODE = "ADB_EDITOR_CODE"; - private BroadcastReceiver mReceiver = null; - private InputMethodManager manager; - - @Override - public void onCreate() { - super.onCreate(); - - // System Alert类型,不影响背景显示 - getWindow().getWindow().setType(Constant.TYPE_ALERT); - - this.manager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); - BackgroundExecutor.execute(new Runnable() { - @Override - public void run() { - while (!LauncherApplication.getInstance().hasFinishInit()) { - MiscUtil.sleep(50); - } - - // 初始化好后再注册 - InjectorService.g().register(AdbIME.this); - } - }); - } - - @Subscriber(value = @Param(value = MSG_HIDE_INPUT, sticky = false), thread = RunningThread.MAIN_THREAD) - public void hideInput() { - requestHideSelf(0); - } - - @Subscriber(value = @Param(value = IME_MESSAGE, sticky = false), thread = RunningThread.MAIN_THREAD) - public boolean inputText(String text) { - if (text != null) { - InputConnection ic = getCurrentInputConnection(); - if (ic != null) { - ic.commitText(text, 1); - return true; - } - } - - return false; - } - - @Subscriber(value = @Param(value = IME_SEARCH_MESSAGE, sticky = false), thread = RunningThread.MAIN_THREAD) - public boolean inputSearchText(String text) { - if (text != null) { - InputConnection ic = getCurrentInputConnection(); - if (ic != null) { - ic.commitText(text, 1); - - // 需要额外点击发送 - EditorInfo editorInfo = getCurrentInputEditorInfo(); - if (editorInfo != null) { - int options = editorInfo.imeOptions; - final int actionId = options & EditorInfo.IME_MASK_ACTION; - - switch (actionId) { - case EditorInfo.IME_ACTION_SEARCH: - sendDefaultEditorAction(true); - break; - case EditorInfo.IME_ACTION_GO: - sendDefaultEditorAction(true); - break; - case EditorInfo.IME_ACTION_SEND: - sendDefaultEditorAction(true); - break; - default: - ic.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER)); - } - } - return true; - } - } - return false; - } - - @Subscriber(value = @Param(value = IME_CHARS, sticky = false), thread = RunningThread.MAIN_THREAD) - public boolean inputChars(int[] chars) { - if (chars != null) { - String msg = new String(chars, 0, chars.length); - InputConnection ic = getCurrentInputConnection(); - if (ic != null){ - ic.commitText(msg, 1); - return true; - } - } - return false; - } - - @Subscriber(value = @Param(value = IME_KEYCODE, sticky = false), thread = RunningThread.MAIN_THREAD) - public boolean inputKeyCode(int code) { - if (code != -1) { - InputConnection ic = getCurrentInputConnection(); - if (ic != null) { - ic.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, code)); - return true; - } - } - return false; - } - - @Subscriber(value = @Param(value = IME_EDITORCODE, sticky = false), thread = RunningThread.MAIN_THREAD) - public boolean inputEditorCode(int code) { - if (code != -1) { - InputConnection ic = getCurrentInputConnection(); - if (ic != null) { - ic.performEditorAction(code); - return true; - } - } - return false; - } - - @Override - public View onCreateInputView() { - View mInputView = getLayoutInflater().inflate(R.layout.input_view, null); - - if (mReceiver == null) { - IntentFilter filter = new IntentFilter(IME_MESSAGE); - filter.addAction(IME_SEARCH_MESSAGE); - filter.addAction(IME_CHARS); - filter.addAction(IME_KEYCODE); - filter.addAction(IME_EDITORCODE); - mReceiver = new AdbReceiver(); - registerReceiver(mReceiver, filter); - } - - // 当出现特殊情况,没有切换回系统输入法,需要用户手动点击切换 - mInputView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - manager.showInputMethodPicker(); - } - }); - - return mInputView; - } - - public void onDestroy() { - if (mReceiver != null) { - unregisterReceiver(mReceiver); - } - - super.onDestroy(); - } - - class AdbReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - boolean sendFlag = false; - - if (intent.getAction().equals(IME_MESSAGE)) { - String msg = intent.getStringExtra("msg"); - sendFlag = inputText(msg); - } else if (intent.getAction().equals(IME_SEARCH_MESSAGE)) { - // 输入并搜索 - String msg = intent.getStringExtra("msg"); - sendFlag = inputSearchText(msg); - } else if (intent.getAction().equals(IME_CHARS)) { - int[] chars = intent.getIntArrayExtra("chars"); - sendFlag = inputChars(chars); - } else if (intent.getAction().equals(IME_KEYCODE)) { - int code = intent.getIntExtra("code", -1); - sendFlag = inputKeyCode(code); - } else if (intent.getAction().equals(IME_EDITORCODE)) { - int code = intent.getIntExtra("code", -1); - sendFlag = inputEditorCode(code); - } - - // 进行了输入,发广播通知切换回原始输入法 - if (sendFlag) { - hideInput(); - } - } - } -} diff --git a/src/app/src/main/java/com/alipay/hulu/tools/PerformStressImpl.java b/src/app/src/main/java/com/alipay/hulu/tools/PerformStressImpl.java index e11e573..9030cc5 100644 --- a/src/app/src/main/java/com/alipay/hulu/tools/PerformStressImpl.java +++ b/src/app/src/main/java/com/alipay/hulu/tools/PerformStressImpl.java @@ -16,7 +16,6 @@ package com.alipay.hulu.tools; import android.content.Context; -import android.widget.Toast; import com.alipay.hulu.common.application.LauncherApplication; import com.alipay.hulu.common.injector.InjectorService; @@ -32,7 +31,7 @@ import java.util.concurrent.atomic.AtomicInteger; @LocalService -public class PerformStressImpl implements IPerformStress, ExportService { +public class PerformStressImpl implements ExportService { public static final String PERFORMANCE_STRESS_CPU_COUNT = "performanceStressCpuCount"; public static final String PERFORMANCE_STRESS_CPU_PERCENT = "performanceStressCpuPercent"; public static final String PERFORMANCE_STRESS_MEMORY = "performanceStressMemory"; @@ -72,6 +71,18 @@ public void setMemory(int memory) { performMemoryStress(); } + /** + * 内存不足时调整一下内存数据 + */ + @Subscriber(@Param(value = LauncherApplication.ON_TRIM_MEMORY, sticky = false)) + public void onTrimMemory() { + LogUtil.w(TAG, "Urgent!!!!, lower memory"); + if (memory > 0) { + int newMemory = (int) (memory * 0.8); + InjectorService.g().pushMessage(PERFORMANCE_STRESS_MEMORY, newMemory); + } + } + @Override public void onCreate(Context context) { cachedThreadPool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); @@ -84,10 +95,6 @@ public void onDestroy(Context context) { InjectorService.g().unregister(this); } - public void addOrReduceToTargetThread(int count) { - - } - public void performCpuStressByCount() { if (targetCount > currentCount.get()) { for (int i = 0; i < targetCount - currentCount.get(); i++) { @@ -148,11 +155,4 @@ void performMemoryStress() { } } - - @Override - public void PerformEntry(int param) { - // TODO Auto-generated method stub - - } - } diff --git a/src/app/src/main/java/com/alipay/hulu/upgrade/PatchRequest.java b/src/app/src/main/java/com/alipay/hulu/upgrade/PatchRequest.java index 4f924a9..5ab08f1 100644 --- a/src/app/src/main/java/com/alipay/hulu/upgrade/PatchRequest.java +++ b/src/app/src/main/java/com/alipay/hulu/upgrade/PatchRequest.java @@ -74,8 +74,9 @@ public static void updatePatchList(final LoadPatchCallback callback) { return; } + String cpuAbi = DeviceInfoUtil.getCPUABIInArm(); // 替换ABI参数 - String realUrl = StringUtil.patternReplace(storedUrl, "", filterAcceptAbi(DeviceInfoUtil.getCPUABI())); + String realUrl = StringUtil.patternReplace(storedUrl, "", cpuAbi); LogUtil.i(TAG, "Start request patch list on: " + realUrl); @@ -230,18 +231,6 @@ public void run() { } }; - /** - * 过滤可用ABI - * @param abi - * @return - */ - private static String filterAcceptAbi(String abi) { - if (ACCEPT_ABI.contains(abi)) { - return abi; - } - return "armeabi-v7a"; - } - public interface LoadPatchCallback { void onLoaded(); void onFailed(); diff --git a/src/app/src/main/java/com/alipay/hulu/util/FunctionSelectUtil.java b/src/app/src/main/java/com/alipay/hulu/util/FunctionSelectUtil.java index 3b84825..15c482d 100644 --- a/src/app/src/main/java/com/alipay/hulu/util/FunctionSelectUtil.java +++ b/src/app/src/main/java/com/alipay/hulu/util/FunctionSelectUtil.java @@ -45,6 +45,9 @@ import android.widget.ScrollView; import android.widget.TextView; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.AppCompatSpinner; + import com.alibaba.fastjson.JSON; import com.alipay.hulu.R; import com.alipay.hulu.common.application.LauncherApplication; @@ -227,10 +230,13 @@ protected static boolean processAction(OperationMethod method, AbstractNodeTree PerformActionEnum action = method.getActionEnum(); if (action == PerformActionEnum.INPUT || action == PerformActionEnum.INPUT_SEARCH + || action == PerformActionEnum.CLICK_AND_INPUT || action == PerformActionEnum.LONG_CLICK || action == PerformActionEnum.MULTI_CLICK || action == PerformActionEnum.SLEEP_UNTIL || action == PerformActionEnum.SLEEP + || action == PerformActionEnum.KEYBOARD_INPUT + || action == PerformActionEnum.INPUT_GLOBAL || action == PerformActionEnum.SCREENSHOT || action == PerformActionEnum.SCROLL_TO_BOTTOM || action == PerformActionEnum.SCROLL_TO_TOP @@ -1277,7 +1283,7 @@ public void run() { * * @param node */ - protected static void showEditView(final AbstractNodeTree node, final OperationMethod method, + public static void showEditView(final AbstractNodeTree node, final OperationMethod method, final Context context, final FunctionListener listener) { try { @@ -1322,6 +1328,8 @@ public void onClick(View v) { edit.setHint(R.string.function__adb_cmd); title = StringUtil.getString(R.string.function__set_adb_cmd); textPattern = null; + } else if (action == PerformActionEnum.KEYBOARD_INPUT) { + textPattern = Pattern.compile("[a-zA-Z0-9]+"); } else if (action == PerformActionEnum.LONG_CLICK) { edit.setHint(R.string.function__long_press); title = StringUtil.getString(R.string.function__set_long_press); @@ -1403,7 +1411,7 @@ public void afterTextChanged(Editable s) { } catch (Exception e) { LogUtil.e(TAG, "Throw exception: " + e.getMessage(), e); - + listener.onCancel(); } } @@ -1661,6 +1669,7 @@ public void onClick(DialogInterface dialog, int which) { } catch (Exception e) { LogUtil.e(TAG, "Login info dialog throw exception: " + e.getMessage(), e); + listener.onCancel(); } } @@ -1674,8 +1683,11 @@ private static Bitmap capture(File captureFile) { ((WindowManager) LauncherApplication.getInstance().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getRealMetrics(metrics); ScreenCaptureService captureService = LauncherApplication.service(ScreenCaptureService.class); - Bitmap bitmap = captureService.captureScreen(captureFile, metrics.widthPixels, metrics.heightPixels, - metrics.widthPixels, metrics.heightPixels); + Bitmap bitmap = null; + if (captureService != null) { + bitmap = captureService.captureScreen(captureFile, metrics.widthPixels, metrics.heightPixels, + metrics.widthPixels, metrics.heightPixels); + } // 原有截图方案失败 if (bitmap == null) { String path = FileUtils.getPathInShell(captureFile); diff --git a/src/app/src/main/java/com/alipay/hulu/util/RecordUtil.java b/src/app/src/main/java/com/alipay/hulu/util/RecordUtil.java index ec1d317..216936f 100644 --- a/src/app/src/main/java/com/alipay/hulu/util/RecordUtil.java +++ b/src/app/src/main/java/com/alipay/hulu/util/RecordUtil.java @@ -93,12 +93,13 @@ public static File saveToFile(Map> BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(saveFile), charset)); // 第一行写标题 - writer.write("RecordTime," + pattern.getName() + "(" + pattern.getUnit() + "),extra\n"); + writer.write("RecordTime," + pattern.getName() + "(" + pattern.getUnit() + "),extra,SimpleTime\n"); writer.flush(); + long dataStartTime = entry.getKey().getStartTime(); // 写入录制 for (RecordPattern.RecordItem item: entry.getValue()) { - writer.write(item.time + "," + item.value + "," + item.extra + "\n"); + writer.write(item.time + "," + item.value + "," + item.extra + "," + (item.time - dataStartTime) / 1000F + "\n"); writer.flush(); } writer.close(); diff --git a/src/app/src/main/res/layout/activity_settings.xml b/src/app/src/main/res/layout/activity_settings.xml index 4d184d8..6d6c25d 100644 --- a/src/app/src/main/res/layout/activity_settings.xml +++ b/src/app/src/main/res/layout/activity_settings.xml @@ -308,6 +308,51 @@ + + + + + + + + + + + - + + + + + + + + + + + + + @@ -117,6 +118,7 @@ - \ No newline at end of file + android:orientation="vertical" + android:layout_height="wrap_content"> + + + + \ No newline at end of file diff --git a/src/app/src/main/res/layout/float_stress_item.xml b/src/app/src/main/res/layout/float_stress_item.xml new file mode 100644 index 0000000..efad45f --- /dev/null +++ b/src/app/src/main/res/layout/float_stress_item.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/app/src/main/res/layout/float_win_list.xml b/src/app/src/main/res/layout/float_win_list.xml index e5e6958..c84f7dd 100644 --- a/src/app/src/main/res/layout/float_win_list.xml +++ b/src/app/src/main/res/layout/float_win_list.xml @@ -37,7 +37,6 @@ android:focusable="false" android:layout_gravity="center_vertical" android:textColor="@color/colorPrimaryDark" - android:maxLines="2" android:textSize="@dimen/control_float_per_item_text_size" /> diff --git a/src/app/src/main/res/layout/head_panel_layout.xml b/src/app/src/main/res/layout/head_panel_layout.xml index 5482c3e..988b2e8 100644 --- a/src/app/src/main/res/layout/head_panel_layout.xml +++ b/src/app/src/main/res/layout/head_panel_layout.xml @@ -22,6 +22,7 @@ + \ No newline at end of file diff --git a/src/app/src/main/res/values-zh/strings.xml b/src/app/src/main/res/values-zh/strings.xml index 32e6f30..0144502 100644 --- a/src/app/src/main/res/values-zh/strings.xml +++ b/src/app/src/main/res/values-zh/strings.xml @@ -278,7 +278,6 @@ CPU负载(%) CPU多核(n) 内存占用(m) - 点击切换输入法 字符串 整数 自定义 @@ -337,6 +336,8 @@ 处理应用启动弹窗 页面变化最长等待时间 页面变化最长等待时间(毫秒) + 最多滑动查找次数 + 是否保存用例 插件加载失败,原因: ∫f(x): %.2f 平均值: %.2f 最小值: %.2f 最大值: %.2f @@ -378,6 +379,10 @@ 尚未执行 请选择屏幕默认选择方向 是否切换屏幕横纵比? + 操作拦截模式 + 覆盖模式 + 录制模式选择 + 阻塞模式 执行序列 当前值:%s 当前值:- diff --git a/src/app/src/main/res/values/strings.xml b/src/app/src/main/res/values/strings.xml index 68c99d6..d81e3f1 100644 --- a/src/app/src/main/res/values/strings.xml +++ b/src/app/src/main/res/values/strings.xml @@ -29,16 +29,16 @@ Mind space Automation Testing Tools -    SoloPi开源自动化测试工具的功能是我们针对不同用户的需求而特别提供。SoloPi提供的应用、代码和资料的著作权均归SoloPi所有,用户具有自由的使用权。\n -    如果用户下载、安装、使用、修改本工具及相关代码,即表明用户信任该工具。那么,用户在使用本工具时造成对用户自己或他人任何形式的损失和伤害,SoloPi工具不承担任何责任。\n -    本工具不含有任何旨在破坏用户计算机数据和获取用户隐私信息的恶意代码;本工具使用过程中获取到的操作信息在打印日志时默认会进行脱敏,且不会进行上传;当应用出现异常时,用户可手动选择是否通过邮件上报故障日志,不会泄漏用户隐私。\n -    对于用户从非工具发布站点下载的应用,本工具无法保证其是否感染计算机病毒、是否隐藏有伪装的特洛伊木马程序或者黑客软件。用户使用此类软件,将可能导致不可预测的风险,建议用户不要轻易下载、安装、使用。本工具不承担由此产生的一切法律责任。\n -    用户不得利用本工具提供的功能误导、欺骗他人;用户仅可将本工具用于学习交流,不得用于非法用途;不得故意避开或者破坏本工具为保护著作权而采取的技术措施。\n -    本工具提供的功能经过详细的测试,但不能保证与所有的软硬件系统完全兼容,不能保证本工具完全没有错误。如果出现不兼容及软件错误的情况,用户可以到交流群获得技术支持。我们将尽力为用户解决问题、解答疑问。\n -    使用本工具所提供的系统所产生的风险由用户自行承担。在适用法律允许的最大范围内,对因使用或不能使用本系统所产生的损害及风险,本工具不承担任何相关责任。\n -    如果本协议中的任何条款无论因何种原因完全或部分无效或不具有执行力,或违反任何适用的法律条款,则该条款被视为删除,但本协议的其余条款仍有效并且具有约束力。\n -    作者有权根据有关法律、法规的变化以及工具的发展需求等因素修改本协议。若新旧协议有冲突,以最新版本协议为准。如果用户继续使用本系统,则视为您接受本协议的变动。\n -    本协议的一切解释权与修改权归本工具所有。\n +    SoloPi is an open source automation test tool that aims for the needs of different users. All applications, codes and materials of copyright provided by SoloPi shall be owned by SoloPi, and users have the right of free use.\n +    If the user downloads, installs, uses, or modifies the tool and related codes, it indicates that the user trusts the tool. Therefore, SoloPi tool shall not be liable for any loss or injury caused by users to themselves or others when using the tool.\n +    This tool does not contain any malicious code designed to destroy user computer data and obtain user privacy information; The operation information obtained in the process of using this tool will be desensitized by default when the log is printed, and will not be uploaded; When an exception occurs to an application, the user can manually choose whether to report the fault log via email, without disclosing the user\'s privacy.\n +    The tool does not guarantee that applications downloaded from non-tools publishing sites are infected by computer viruses, hidden by disguised Trojan horses program or hacker software. Users using such software may lead to unpredictable risks, users are suggested not to easily download, install and use. SoloPi shall not bear any legal liability arising therefrom.\n +    Users shall not use the functions provided by this tool to mislead or deceive others; Users can only use this tool for learning and communication, and shall not use it for illegal purposes; Users shall not intentionally avoid or destroy the technical measures taken by this tool for the protection of copyright.\n +    The functions provided by this tool have been tested in detail, but it cannot be guaranteed to be fully compatible with all hardware and software systems, and it cannot be guaranteed that this tool is completely error-free. If incompatibilities and software errors occur, users can go to the communication group for technical support. We will try our best to solve problems and answer questions for users.\n +    The use of the system provided by this tool is at the user\'s own risk. To the maximum extent permitted by applicable law, the Tool shall not be liable for any damage or risk arising from the use or inability to use the System.\n +    If any provision of this Agreement is invalid or unenforceable in whole or in part for any reason, or violates any applicable legal provisions, such provision shall be deemed to be deleted, but the remaining provisions of this Agreement shall remain valid and binding.\n +    The author has the right to modify this Agreement in accordance with the changes of relevant laws and regulations and the development needs of tools. In case of any conflict between the old and the new, the latest version shall prevail. If the User continues to use the System, you shall be deemed to have accepted the change of this Agreement.\n +    All rights of interpretation and modification of this Agreement belong to this tool.\n %d x %d @@ -279,7 +279,6 @@ CPU Load(%) CPU Cores(n) Memory(MB) - Change IME String Integer Custom @@ -338,6 +337,8 @@ Handle Alert Max Step Wait Time Max Step Wait Time(ms) + Max Scroll Find Times + times Save Case? Plugin load failed: ∫f(x): %.2f avg: %.2f min: %.2f max: %.2f @@ -379,6 +380,10 @@ Unenforced Select Default Screen Orientation Change Screen XY Axis? + Operation block mode + Cover View Mode + Block Mode + Choose operation block mode Execution List Current: %s Current: - diff --git a/src/build.gradle b/src/build.gradle index bf78d1f..b282c2c 100644 --- a/src/build.gradle +++ b/src/build.gradle @@ -16,11 +16,15 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { + maven { url 'https://maven.aliyun.com/repository/public' } + maven { url 'https://maven.aliyun.com/repository/central' } + maven { url 'https://maven.aliyun.com/repository/google' } + maven { url 'https://maven.aliyun.com/repository/gradle-plugin' } google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.0.1' + classpath 'com.android.tools.build:gradle:4.0.2' classpath 'org.greenrobot:greendao-gradle-plugin:3.3.0' } @@ -28,15 +32,20 @@ buildscript { compileSdkVersion = 29 buildToolsVersion = "29.0.2" targetSdkVersion = 29 - appVersionCode = 32 - appVersionName = '0.11.1' + appVersionCode = 35 + appVersionName = '0.12.0' } } allprojects { repositories { + maven { url 'https://maven.aliyun.com/repository/public' } + maven { url 'https://maven.aliyun.com/repository/central' } + maven { url 'https://maven.aliyun.com/repository/google' } + maven { url 'https://maven.aliyun.com/repository/gradle-plugin' } google() jcenter() + mavenCentral() maven { url "https://jitpack.io" } } } diff --git a/src/common/build.gradle b/src/common/build.gradle index 98ae934..7602c27 100644 --- a/src/common/build.gradle +++ b/src/common/build.gradle @@ -30,14 +30,21 @@ android { } buildTypes { + release { + consumerProguardFiles 'proguard-rules.pro' + } + debug { + consumerProguardFiles 'proguard-rules.pro' + } } } dependencies { - compileOnly 'androidx.appcompat:appcompat:1.0.0' - compileOnly 'androidx.legacy:legacy-support-v4:1.0.0' - compileOnly 'com.google.android.material:material:1.0.0' + api "org.nanohttpd:nanohttpd:2.2.0" + compileOnly "androidx.appcompat:appcompat:${ANDROIDX_APPCOMPAT_VERSION}" + compileOnly "androidx.legacy:legacy-support-v4:${ANDROIDX_SUPPORT_V4_VERSION}" + compileOnly "com.google.android.material:material:${ANDROIDX_MATERIAL_VERSION}" compileOnly ('com.orhanobut:logger:2.2.0') { exclude group: "com.android.support" } @@ -46,9 +53,9 @@ dependencies { { exclude group: "com.android.support" } - compileOnly 'com.alibaba:fastjson:1.2.73' - compileOnly group: 'commons-io', name: 'commons-io', version: '2.6' - compileOnly 'androidx.multidex:multidex:2.0.0' + compileOnly "com.alibaba:fastjson:${FASTJSON_VERSION}" + compileOnly "commons-io:commons-io:${COMMON_IO_VERSION}" + compileOnly "androidx.multidex:multidex:${ANDROIDX_MULTIDEX_VERSION}" annotationProcessor ('com.github.bumptech.glide:compiler:4.11.0') { exclude group: "com.android.support" diff --git a/src/common/proguard-rules.pro b/src/common/proguard-rules.pro index f1b4245..dd3de5d 100644 --- a/src/common/proguard-rules.pro +++ b/src/common/proguard-rules.pro @@ -19,3 +19,58 @@ # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile + +-keep class com.alipay.hulu.common.utils.ClassUtil$PatchVersionInfo { *; } +-keep class com.alipay.hulu.common.utils.patch.PatchDescription {*;} + +-keep class com.alipay.hulu.common.utils.DeviceInfoUtil {*;} +-keep class com.alipay.hulu.common.utils.Callback {*;} +-keep class com.alipay.hulu.common.utils.LogUtil {*;} +# injector +-keepclassmembers class ** { +@com.alipay.hulu.common.injector.param.Subscriber ; +} +-keepclassmembers class ** { +@com.alipay.hulu.common.injector.provider.Provider ; +} +# ActionProvider +-keep @com.alipay.hulu.common.annotation.Enable class * + +# SchemeResolver +-keep interface com.alipay.hulu.common.scheme.SchemeActionResolver { *; } +-keep @com.alipay.hulu.common.scheme.SchemeResolver class * implements com.alipay.hulu.common.scheme.SchemeActionResolver { *; } + +-keep class com.alipay.hulu.common.bean.** {*;} + +-keep interface com.alipay.hulu.common.tools.AbstCmdLine {*;} + +-keep class com.alipay.hulu.common.utils.patch.PatchContext {*;} + +# Glide +-keep class com.alipay.hulu.common.utils.Glide* { *; } + +-keep public class * implements com.bumptech.glide.module.GlideModule +-keep public class * extends com.bumptech.glide.module.AppGlideModule +-keep public enum com.bumptech.glide.load.ImageHeaderParser$** { + **[] $VALUES; + public *; +} + +-keep @interface com.alipay.hulu.common.trigger.Trigger { *; } +-keep @com.alipay.hulu.common.trigger.Trigger class ** implements java.lang.Runnable { +public void run(); +} + +-keep interface com.alipay.hulu.common.service.base.ExportService { *; } +-keep @interface com.alipay.hulu.common.service.base.LocalService {*;} +-keep class com.alipay.hulu.common.utils.patch.PatchClassLoader { +public com.alipay.hulu.common.utils.patch.PatchContext getContext(); +} +-keep class ** implements com.alipay.hulu.common.service.base.ExportService { *; } + +-keep interface ** extends com.alipay.hulu.common.service.base.ExportService { *; } + +-keep interface com.alipay.hulu.common.service.base.AppGuardian { *; } +-keep @interface com.alipay.hulu.common.service.base.AppGuardian$AppGuardianEnable { *; } +-keep enum com.alipay.hulu.common.service.base.AppGuardian$ReceiveSystemEvent { *; } +-keep @interface com.alipay.hulu.common.service.base.LocalService {*;} \ No newline at end of file diff --git a/src/common/src/main/AndroidManifest.xml b/src/common/src/main/AndroidManifest.xml index bac597a..164e6a2 100644 --- a/src/common/src/main/AndroidManifest.xml +++ b/src/common/src/main/AndroidManifest.xml @@ -18,6 +18,7 @@ package="com.alipay.hulu.common"> + @@ -35,5 +36,14 @@ + + + + + + + diff --git a/src/common/src/main/java/com/alipay/hulu/common/application/LauncherApplication.java b/src/common/src/main/java/com/alipay/hulu/common/application/LauncherApplication.java index 8959194..87e1e39 100644 --- a/src/common/src/main/java/com/alipay/hulu/common/application/LauncherApplication.java +++ b/src/common/src/main/java/com/alipay/hulu/common/application/LauncherApplication.java @@ -15,40 +15,56 @@ */ package com.alipay.hulu.common.application; +import androidx.annotation.StringRes; +import androidx.multidex.MultiDex; + +import static android.content.DialogInterface.BUTTON_NEGATIVE; +import static android.content.DialogInterface.BUTTON_POSITIVE; + +import static android.content.DialogInterface.BUTTON_NEGATIVE; +import static android.content.DialogInterface.BUTTON_POSITIVE; + import android.app.Activity; import android.app.ActivityManager; import android.app.AlertDialog; import android.app.Application; import android.app.PendingIntent; import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.ComponentCallbacks2; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.ApplicationInfo; -import android.content.res.Configuration; -import android.content.res.Resources; import android.os.Build; import android.os.Handler; import android.os.LocaleList; import android.os.Looper; -import androidx.annotation.StringRes; -import androidx.multidex.MultiDex; -import android.util.DisplayMetrics; import android.view.WindowManager; import android.widget.Toast; +import androidx.annotation.StringRes; +import androidx.multidex.MultiDex; + import com.alipay.hulu.common.R; +import com.alipay.hulu.common.http.HttpServer; import com.alipay.hulu.common.injector.InjectorService; +import com.alipay.hulu.common.injector.provider.Param; +import com.alipay.hulu.common.injector.provider.Provider; import com.alipay.hulu.common.logger.DiskLogStrategy; import com.alipay.hulu.common.logger.SimpleFormatStrategy; -import com.alipay.hulu.common.logger.ThreadInfoLoggerPrinter; import com.alipay.hulu.common.scheme.SchemeActionResolver; +import com.alipay.hulu.common.scheme.SchemeHttpListener; import com.alipay.hulu.common.scheme.SchemeResolver; import com.alipay.hulu.common.service.SPService; +import com.alipay.hulu.common.service.base.AppGuardian; import com.alipay.hulu.common.service.base.ExportService; import com.alipay.hulu.common.service.base.LocalService; import com.alipay.hulu.common.tools.BackgroundExecutor; +import com.alipay.hulu.common.trigger.Trigger; import com.alipay.hulu.common.utils.ClassUtil; +import com.alipay.hulu.common.utils.ContextUtil; import com.alipay.hulu.common.utils.LogUtil; import com.alipay.hulu.common.utils.MiscUtil; import com.alipay.hulu.common.utils.SortedList; @@ -64,7 +80,9 @@ import com.orhanobut.logger.Logger; import java.io.File; +import java.io.IOException; import java.lang.ref.WeakReference; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -77,22 +95,26 @@ import java.util.Stack; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantReadWriteLock; -import static android.content.DialogInterface.BUTTON_NEGATIVE; -import static android.content.DialogInterface.BUTTON_POSITIVE; -import static android.view.Surface.ROTATION_0; -import static android.view.Surface.ROTATION_90; - /** * Created by qiaoruikai on 2018/9/29 10:59 AM. */ +@Provider(@Param(value = LauncherApplication.SYSTEM_GUARDIAN_EVENT, sticky = false, type = AppGuardian.ReceiveSystemEvent.class)) public abstract class LauncherApplication extends Application { private static final String TAG = LauncherApplication.class.getSimpleName(); public static final String SHOW_LOADING_DIALOG = "showLoadingDialog"; public static final String DISMISS_LOADING_DIALOG = "dismissLoadingDialog"; + public static final String ON_TRIM_MEMORY = "system_trim_memory"; + public static final String SYSTEM_GUARDIAN_EVENT = "system_guardian_event"; private Map> schemeResolver; @@ -101,10 +123,26 @@ public abstract class LauncherApplication extends Application { */ public final Locale DEFAULT_LOCALE; + private HttpServer totalControlHttpServer; + private SchemeHttpListener schemeListener; + + /** + * 触发器线程池(无常备线程) + */ + private final ExecutorService triggerThreadPool = new ThreadPoolExecutor(0, 3, 3, TimeUnit.MINUTES, new LinkedBlockingQueue(), new ThreadFactory() { + AtomicInteger counter = new AtomicInteger(1); + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setName("Trigger-thread-" + counter.getAndIncrement()); + return t; + } + }); + /** - * 屏幕方向监控 + * 触发器队列 */ - public static final String SCREEN_ORIENTATION = "screenOrientation"; + private Map>> triggerClasses; protected static LauncherApplication appInstance; protected Map registeredService = new HashMap<>(); @@ -184,11 +222,13 @@ public void onCreate() { // 初始化过,看看不重新onCreate的影响 if (finishInit) { + super.onCreate(); LogUtil.e(TAG, "Already initialized"); return; } finishInit = false; + appInstance = this; super.onCreate(); SPService.init(this); setApplicationLanguage(); @@ -197,11 +237,12 @@ public void onCreate() { ApplicationInfo info = getApplicationInfo(); DEBUG = (info.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0; - appInstance = this; initialLogger(); handler = new Handler(); + initEventTracker(); + BackgroundExecutor.execute(new Runnable() { @Override public void run() { @@ -212,21 +253,32 @@ public void run() { } catch (Throwable t) { LogUtil.e(TAG, "加载类失败, " + t.getMessage(), t); } - + + registerTriggerClasses(); + // 起始时间触发器 + triggerAtTime(Trigger.TRIGGER_TIME_START); + // 初始化基础服务 registerServices(); // 实际初始化 init(); + triggerAtTime(Trigger.TRIGGER_TIME_START_FINISH); finishInit = true; BackgroundExecutor.execute(new Runnable() { @Override public void run() { initActionResolvers(); + + startHttpServerAtPort(SPService.getInt(SPService.KEY_CONTROL_PORT, 23342)); + schemeListener = new SchemeHttpListener(); + registerControlListener(schemeListener); + + triggerAtTime(Trigger.TRIGGER_TIME_SCHEME_INIT); } - }, 10000); + }, 5000); } catch (Throwable e) { LogUtil.e(TAG, "无法处理", e); // 解决不了 @@ -239,6 +291,44 @@ public void run() { initInMain(); } + /** + * 启动http服务 + * @param port + */ + public void startHttpServerAtPort(int port) { + List listeners = new ArrayList<>(); + if (totalControlHttpServer != null) { + // 转移注册 + listeners.addAll(totalControlHttpServer.getAllListeners()); + totalControlHttpServer.removeAllListeners(); + totalControlHttpServer.stop(); + totalControlHttpServer = null; + } + + totalControlHttpServer = new HttpServer(port); + try { + totalControlHttpServer.start(); + totalControlHttpServer.addAllListeners(listeners); + } catch (IOException e) { + LogUtil.e(TAG, "Start totalControlHttpServer failed", e); + showToast("启动HTTP服务失败,当前端口为 " + port + " ,请确认是否端口冲突"); + } + } + + /** + * 注册控制服务监听器 + * @param listener + */ + public boolean registerControlListener(HttpServer.OnUrlRequestListener listener) { + if (totalControlHttpServer == null) { + LogUtil.w(TAG, "Control server is null, can't register"); + return false; + } + + totalControlHttpServer.addListener(listener); + return true; + } + @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); @@ -276,25 +366,98 @@ public void runOnUiThread(Runnable runnable, long delay) { protected void initInMain() { } - - /** * 设置应用默认语言 */ public void setApplicationLanguage() { - Resources resources = getApplicationContext().getResources(); - Configuration config = resources.getConfiguration(); - Locale locale = getLanguageLocale(); - config.setLocale(locale); - Locale.setDefault(locale); + Locale.setDefault(getLanguageLocale()); + ContextUtil.updateResources(this); + } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - LocaleList localeList = new LocaleList(locale); - LocaleList.setDefault(localeList); - config.setLocales(localeList); -// getApplicationContext().createConfigurationContext(config); + /** + * 注册触发器类 + */ + protected synchronized void registerTriggerClasses() { + if (triggerClasses != null) { + LogUtil.i(TAG, "Trigger Classes already registered"); + return; + } + List> triggerClasses = ClassUtil.findSubClass(Runnable.class, Trigger.class); + Map>> triggerMap = new HashMap<>(); + for (Class triggerClz: triggerClasses) { + Trigger trigger = triggerClz.getAnnotation(Trigger.class); + if (trigger == null) { + LogUtil.e(TAG, "Trigger class %s has no trigger annotation", triggerClz); + continue; + } + + String[] values = trigger.value(); + if (values == null || values.length == 0) { + LogUtil.e(TAG, "Trigger class %s has no related time", triggerClz); + continue; + } + + // 批量注册 + for (String value: values) { + if (StringUtil.isEmpty(value)) { + LogUtil.w(TAG, "Invalid trigger time for class " + triggerClz); + continue; + } + if (!triggerMap.containsKey(value)) { + triggerMap.put(value, new SortedList>(true)); + } + + triggerMap.get(value).add(triggerClz, trigger.level()); + } + } + + this.triggerClasses = triggerMap; + } + + /** + * 触发特定环节触发器 + * @param trigger + */ + public void triggerAtTime(final String trigger) { + if (triggerClasses.containsKey(trigger)) { + LogUtil.i(TAG, "Trigger at time: " + trigger); + final SortedList> clzList = triggerClasses.get(trigger); + if (clzList == null || clzList.size() == 0) { + LogUtil.w(TAG, "No trigger registered at time: " + trigger); + return; + } + + // 实际触发执行 + triggerThreadPool.execute(new Runnable() { + @Override + public void run() { + long startTime = System.currentTimeMillis(); + LogUtil.i(trigger, "Do Trigger at time: " + trigger); + + for (Class triggerClz: clzList) { + LogUtil.i(trigger, "Start trigger for class: " + triggerClz); + Runnable r = ClassUtil.constructClass(triggerClz); + if (r == null) { + LogUtil.e(trigger, "Initialize failed for trigger class: " + triggerClz); + return; + } + + // 实际执行 + try { + r.run(); + } catch (Throwable t) { + LogUtil.e(trigger, "Trigger Run failed for class: " + triggerClz); + } finally { + LogUtil.i(trigger, "Finish trigger for class: " + triggerClz); + } + } + + LogUtil.i(trigger, "Finish Trigger at time: " + trigger + ", total spend time: " + (System.currentTimeMillis() - startTime)); + } + }); + } else { + LogUtil.w(TAG, "No trigger registered at time: " + trigger); } - resources.updateConfiguration(config, null); } /** @@ -743,6 +906,32 @@ public synchronized void initActionResolvers() { } setSchemeResolver(resolvers); + + } + + @Override + public void onTrimMemory(int level) { + if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) { + InjectorService.g().pushMessage(ON_TRIM_MEMORY); + } + LogUtil.i("ScreenEventBroadCastReceiver", "Receive memory state " + level); + super.onTrimMemory(level); + } + + /** + * 获取最适合的前台Context + * @return + */ + public Context getBestForegroundContext() { + Context activity = loadActivityOnTop(); + if (activity != null) { + return activity; + } + Context service = loadRunningService(); + if (service != null) { + return service; + } + return this; } /** @@ -997,9 +1186,9 @@ public void onClick(DialogInterface dialog, int which) { public Locale getLanguageLocale() { switch (SPService.getInt(SPService.KEY_USE_LANGUAGE, 0)) { case 1: - return Locale.CHINESE; + return Locale.CHINA; case 2: - return Locale.ENGLISH; + return Locale.US; default: return DEFAULT_LOCALE; } @@ -1061,41 +1250,6 @@ public void moveSelfToFront() { } } - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - - // 等Injector初始化 - if (InjectorService.g() == null) { - return; - } - - // 如果有Activity - Activity context = (Activity) loadActivityOnTop(); - if (context != null) { - InjectorService.g().pushMessage(SCREEN_ORIENTATION, context.getWindowManager().getDefaultDisplay().getRotation()); - return; - } - - // 从Service获取 - Service service = (Service) loadRunningService(); - - // 没有就只能去0或90 - if (service == null) { - InjectorService.g().pushMessage(SCREEN_ORIENTATION, newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE ? ROTATION_90 : ROTATION_0); - return; - } - - // 找WindowManager - WindowManager wm = (WindowManager) service.getSystemService(WINDOW_SERVICE); - int rotation = wm.getDefaultDisplay().getRotation(); - - // 推送屏幕方向 - if (InjectorService.g() != null) { - InjectorService.g().pushMessage(SCREEN_ORIENTATION, rotation); - } - } - /** * 是否在顶层运行 * @param context @@ -1114,17 +1268,41 @@ private void setSchemeResolver(Map> sch this.schemeResolver = schemeResolver; } + private void initEventTracker() { + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_SCREEN_ON); + filter.addAction(Intent.ACTION_SCREEN_OFF); + filter.addAction(Intent.ACTION_USER_PRESENT); + registerReceiver(new ScreenEventBroadCastReceiver(this), filter); + } + + private void triggerServiceEvent(AppGuardian.ReceiveSystemEvent event) { + if (!finishInit) { + return; + } + for (ServiceReference service: registeredService.values()) { + service.onSystemEventTriggered(event); + } + InjectorService.g().pushMessage(SYSTEM_GUARDIAN_EVENT, event); + } + /** * 服务引用 */ private static class ServiceReference { private Class targetClass; private ExportService target; + private boolean isAppGuardianEnable = false; private int level; + private volatile boolean isInitialized = false; public ServiceReference(LocalService annotation, Class targetClass) { this.targetClass = targetClass; this.level = annotation.level(); + AppGuardian.AppGuardianEnable guardianEnable = targetClass.getAnnotation(AppGuardian.AppGuardianEnable.class); + if (guardianEnable != null && guardianEnable.value() && AppGuardian.class.isAssignableFrom(targetClass)) { + this.isAppGuardianEnable = true; + } initializedService(targetClass, annotation.lazy()); } @@ -1161,6 +1339,7 @@ public Object intercept(Object object, Object[] args, MethodProxy methodProxy) t @Override public void run() { enhancerInterface.setTarget$Enhancer$(initClass()); + isInitialized = true; runningFLag.set(false); } }); @@ -1191,6 +1370,7 @@ public void run() { @Override public void run() { ExportService service = initClass(); + isInitialized = true; result.setTarget$Enhancer$(service); } }); @@ -1215,6 +1395,12 @@ private ExportService initClass() { return target; } + private synchronized void onSystemEventTriggered(AppGuardian.ReceiveSystemEvent event) { + if (this.isAppGuardianEnable && this.isInitialized) { + ((AppGuardian) this.target).onEventTrigger(event); + } + } + /** * 调用清理 * @param context @@ -1223,6 +1409,7 @@ private void onDestroy(final Context context) { EnhancerInterface target = (EnhancerInterface) this.target; final ExportService service = (ExportService) target.getTarget$Enhancer$(); if (service != null) { + isInitialized = false; getInstance().runOnUiThread(new Runnable() { @Override public void run() { @@ -1234,4 +1421,25 @@ public void run() { } } } + + private static class ScreenEventBroadCastReceiver extends BroadcastReceiver { + private static final String TAG = ScreenEventBroadCastReceiver.class.getSimpleName(); + private LauncherApplication app; + private ScreenEventBroadCastReceiver(LauncherApplication app) { + this.app = app; + } + + @Override + public void onReceive(Context context, Intent intent) { + LogUtil.i(TAG, "Receive broadcast event" + intent.getAction()); + switch (intent.getAction()) { + case Intent.ACTION_SCREEN_OFF: + app.triggerServiceEvent(AppGuardian.ReceiveSystemEvent.SCREEN_LOCK); + break; + case Intent.ACTION_USER_PRESENT: + app.triggerServiceEvent(AppGuardian.ReceiveSystemEvent.SCREEN_UNLOCK); + break; + } + } + } } diff --git a/src/common/src/main/java/com/alipay/hulu/common/constant/Constant.java b/src/common/src/main/java/com/alipay/hulu/common/constant/Constant.java index e87b75a..375c1ad 100644 --- a/src/common/src/main/java/com/alipay/hulu/common/constant/Constant.java +++ b/src/common/src/main/java/com/alipay/hulu/common/constant/Constant.java @@ -33,4 +33,19 @@ public class Constant { public static final int HOTPATCH_VERSION = 1; public static final int TYPE_ALERT = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + + /** + * 屏幕方向监控 + */ + public static final String SCREEN_ORIENTATION = "screenOrientation"; + + public static class IME { + public static final String IME_STATUS = "ime_status"; + public static final String IME_INPUT_TEXT = "ime_inputText"; + public static final String IME_INPUT_TEXT_ENTER = "ime_inputTextEnter"; + public static final String IME_INPUT_KEY_CODE = "ime_inputKeyCode"; + public static final String IME_CLEAR_TEXT = "ime_clearText"; + public static final String IME_HIDE_IME = "ime_hideIme"; + public static final String IME_OPERATION_ACTION = "ime_operationAction"; + } } diff --git a/src/common/src/main/java/com/alipay/hulu/common/http/HttpServer.java b/src/common/src/main/java/com/alipay/hulu/common/http/HttpServer.java new file mode 100644 index 0000000..df16356 --- /dev/null +++ b/src/common/src/main/java/com/alipay/hulu/common/http/HttpServer.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.common.http; + +import android.util.Log; + +import com.alipay.hulu.common.utils.LogUtil; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import fi.iki.elonen.NanoHTTPD; + +public class HttpServer extends NanoHTTPD { + private static final String TAG = "Perf-HttpServer"; + private static final String FAIL_RESPONSE_NO_PROCESS = "{\"success\": false, \"error\": \"No Valid Listener\"}"; + + private List listeners; + + public HttpServer(int port) { + super(port); + listeners = new ArrayList<>(); + } + + @Override + public void start() throws IOException { + try { + super.start(); + } catch (IOException e) { + Log.e(TAG, "Fail to start", e); + throw e; + } + } + + public void addListener(OnUrlRequestListener listener) { + if (listeners.contains(listener)) { + return; + } + + LogUtil.i(TAG, "注册URL监听器"); + this.listeners.add(listener); + } + + public void removeListener(OnUrlRequestListener listener) { + listeners.remove(listener); + } + + /** + * 获取当前所有监听器 + * @return + */ + public List getAllListeners() { + return new ArrayList<>(listeners); + } + + /** + * 移除所有监听器 + */ + public void removeAllListeners() { + this.listeners.clear(); + } + + /** + * 添加所有监听器 + * @param listeners + */ + public void addAllListeners(List listeners) { + this.listeners.addAll(listeners); + } + + @Override + public Response serve(IHTTPSession session) { + String url = session.getUri(); + Map requestParam = session.getParms(); + /*我在这里做了一个限制,只接受POST请求。这个是项目需求。*/ + if (Method.POST.equals(session.getMethod())) { + Map files = new HashMap(); + /*获取header信息,NanoHttp的header不仅仅是HTTP的header,还包括其他信息。*/ + Map header = session.getHeaders(); + try { + /*这句尤为重要就是将将body的数据写入files中,大家可以看看parseBody具体实现,倒现在我也不明白为啥这样写。*/ + session.parseBody(files); + /*看就是这里,POST请教的body数据可以完整读出*/ + + String content = files.get("postData"); + + LogUtil.i(TAG, "Receive Post message:::url[%s], requestParam[%s], content=%s", url, requestParam, content); + + for (OnUrlRequestListener listener: listeners) { + Response res = listener.onPost(url, requestParam, content); + if (res != null) { + return res; + } + } + } catch (IOException e) { + Log.e(TAG, "Process request fail", e); + } catch (ResponseException e) { + Log.e(TAG, "Process request fail", e); + } + /*这里就是为客户端返回的信息了。我这里返回了一个200和一个HelloWorld*/ + return NanoHTTPD.newFixedLengthResponse(Response.Status.OK, "application/json", FAIL_RESPONSE_NO_PROCESS); + } else { + LogUtil.i(TAG, "Receive Get message:::url[%s], requestParam[%s]", url, requestParam); + for (OnUrlRequestListener listener: listeners) { + Response res = listener.onGet(url, requestParam); + if (res != null) { + return res; + } + } + return NanoHTTPD.newFixedLengthResponse(Response.Status.OK, "text/html", FAIL_RESPONSE_NO_PROCESS); + } + } + + public interface OnUrlRequestListener { + /** + * 处理POST请求 + * @param url + * @param content + * @return + */ + NanoHTTPD.Response onPost(String url, Map params, String content); + + /** + * 处理GET请求 + * @param url + * @return + */ + NanoHTTPD.Response onGet(String url, Map params); + } +} \ No newline at end of file diff --git a/src/common/src/main/java/com/alipay/hulu/common/scheme/SchemeActionResolver.java b/src/common/src/main/java/com/alipay/hulu/common/scheme/SchemeActionResolver.java index 7b1e20f..b0c11c2 100644 --- a/src/common/src/main/java/com/alipay/hulu/common/scheme/SchemeActionResolver.java +++ b/src/common/src/main/java/com/alipay/hulu/common/scheme/SchemeActionResolver.java @@ -17,11 +17,22 @@ import android.content.Context; +import com.alipay.hulu.common.utils.Callback; + import java.util.Map; /** * Created by qiaoruikai on 2019/11/8 11:01 PM. */ public interface SchemeActionResolver { - boolean processScheme(Context context, Map params); + + /** + * 处理scheme消息 + * @param context Activity上下文 + * @param params 请求参数 + * @param callback 响应结果回调 + * @return + */ + boolean processScheme(Context context, Map params, + Callback> callback); } diff --git a/src/common/src/main/java/com/alipay/hulu/common/scheme/SchemeActivity.java b/src/common/src/main/java/com/alipay/hulu/common/scheme/SchemeActivity.java index 26c7884..fcd960b 100644 --- a/src/common/src/main/java/com/alipay/hulu/common/scheme/SchemeActivity.java +++ b/src/common/src/main/java/com/alipay/hulu/common/scheme/SchemeActivity.java @@ -25,6 +25,7 @@ import com.alipay.hulu.common.R; import com.alipay.hulu.common.application.LauncherApplication; import com.alipay.hulu.common.tools.BackgroundExecutor; +import com.alipay.hulu.common.utils.Callback; import com.alipay.hulu.common.utils.LogUtil; import com.alipay.hulu.common.utils.MiscUtil; import com.alipay.hulu.common.utils.SortedList; @@ -101,12 +102,29 @@ private void doSchemeJump(Map> resolver return; } - SortedList resolverList = resolvers.get(action); - for (SchemeActionResolver realResolver: resolverList) { - if (realResolver.processScheme(this, params)) { - finish(); - return; + try { + SortedList resolverList = resolvers.get(action); + Callback> emptyCallback = new Callback>() { + @Override + public void onResult(Map item) { + LogUtil.i(TAG, "Receive callback: " + item); + } + + @Override + public void onFailed() { + LogUtil.w(TAG, "Receive fail callback"); + } + }; + if (resolverList != null) { + for (SchemeActionResolver realResolver : resolverList) { + if (realResolver.processScheme(this, params, emptyCallback)) { + LogUtil.i(TAG, "Processed by " + realResolver.getClass().getSimpleName()); + return; + } + } } + } catch (Throwable t) { + LogUtil.e(TAG, "handle resolver failed: " + t.getMessage(), t); } startOrigin(); diff --git a/src/common/src/main/java/com/alipay/hulu/common/scheme/SchemeHttpListener.java b/src/common/src/main/java/com/alipay/hulu/common/scheme/SchemeHttpListener.java new file mode 100644 index 0000000..a976a17 --- /dev/null +++ b/src/common/src/main/java/com/alipay/hulu/common/scheme/SchemeHttpListener.java @@ -0,0 +1,80 @@ +package com.alipay.hulu.common.scheme; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.alipay.hulu.common.application.LauncherApplication; +import com.alipay.hulu.common.http.HttpServer; +import com.alipay.hulu.common.utils.Callback; +import com.alipay.hulu.common.utils.LogUtil; +import com.alipay.hulu.common.utils.SortedList; +import com.alipay.hulu.common.utils.StringUtil; + +import java.util.Map; + +import fi.iki.elonen.NanoHTTPD; + +import static fi.iki.elonen.NanoHTTPD.newFixedLengthResponse; + +public class SchemeHttpListener implements HttpServer.OnUrlRequestListener { + private static final String TAG = SchemeHttpListener.class.getSimpleName(); + public static final String LISTEN_PATH = "/scheme/"; + + @Override + public NanoHTTPD.Response onPost(String url, Map params, String content) { + return null; + } + + @Override + public NanoHTTPD.Response onGet(String url, Map params) { + if (StringUtil.startWith(url, LISTEN_PATH)) { + String realPath = url.substring(LISTEN_PATH.length()); + return doSchemeJump(LauncherApplication.getInstance().getSchemeResolver(), realPath, params); + } + return null; + } + + /** + * 实际跳转 + * @param resolvers + * @param action + * @param params + */ + private NanoHTTPD.Response doSchemeJump(Map> resolvers, String action, Map params) { + if (resolvers == null || !resolvers.containsKey(action)) { + return null; + } + + LogUtil.i(TAG, "Begin to process scheme, action: %s, params: %s", action, params); + + try { + SortedList resolverList = resolvers.get(action); + final JSONObject callback = new JSONObject(); + Callback> emptyCallback = new Callback>() { + @Override + public void onResult(Map item) { + callback.putAll(item); + } + + @Override + public void onFailed() { + LogUtil.w(TAG, "Receive fail callback"); + } + }; + if (resolverList != null) { + for (SchemeActionResolver realResolver : resolverList) { + if (realResolver.processScheme(LauncherApplication.getContext(), params, emptyCallback)) { + LogUtil.i(TAG, "Processed by " + realResolver.getClass().getSimpleName()); + if (callback.isEmpty()) { + callback.put("status", "success"); + } + return newFixedLengthResponse(NanoHTTPD.Response.Status.OK, "application/json", JSON.toJSONString(callback)); + } + } + } + } catch (Throwable t) { + LogUtil.e(TAG, "handle resolver failed: " + t.getMessage(), t); + return newFixedLengthResponse(NanoHTTPD.Response.Status.OK, "application/json", "{\"success\": false, \"error\": \"Process failed, throw exception: " + t.getMessage() + "\"}"); + } + return null; + } +} diff --git a/src/common/src/main/java/com/alipay/hulu/common/service/SPService.java b/src/common/src/main/java/com/alipay/hulu/common/service/SPService.java index 428c5f4..d66796f 100644 --- a/src/common/src/main/java/com/alipay/hulu/common/service/SPService.java +++ b/src/common/src/main/java/com/alipay/hulu/common/service/SPService.java @@ -29,15 +29,18 @@ public class SPService { public static final String KEY_SOLOPI_PATH_NAME = "KEY_SOLOPI_PATH_NAME"; public static final String KEY_OUTPUT_CHARSET = "KEY_OUTPUT_CHARSET"; public static final String KEY_MAX_WAIT_TIME = "KEY_MAX_WAIT_TIME"; + public static final String KEY_MAX_SCROLL_FIND_COUNT = "KEY_MAX_SCROLL_FIND_COUNT"; public static final String KEY_ERROR_CHECK_TIME = "KEY_ERROR_CHECK_TIME"; public static final String KEY_GLOBAL_SETTINGS = "KEY_GLOBAL_SETTINGS"; public static final String KEY_PERFORMANCE_UPLOAD = "KEY_PERFORMANCE_UPLOAD"; public static final String KEY_DISPLAY_SYSTEM_APP = "KEY_DISPLAY_SYSTEM_APP"; public static final String KEY_BASE_DIR = "KEY_BASE_DIR"; public static final String KEY_CHECK_UPDATE = "KEY_CHECK_UPDATE"; + public static final String KEY_SHOULD_UPDATE_IN_APP = "KEY_SHOULD_UPDATE_IN_APP"; public static final String KEY_RECORD_SCREEN_UPLOAD = "KEY_RECORD_SCREEN_UPLOAD"; public static final String KEY_PATCH_URL = "KEY_PATCH_URL"; public static final String KEY_AES_KEY = "KEY_AES_KEY"; + public static final String KEY_RECORD_COVER_MODE = "KEY_RECORD_COVER_MODE"; public static final String KEY_REPLAY_AUTO_START = "KEY_REPLAY_AUTO_START"; public static final String KEY_RESTART_APP_ON_PLAY = "KEY_RESTART_APP_ON_PLAY"; public static final String KEY_SKIP_ACCESSIBILITY = "KEY_SKIP_ACCESSIBILITY"; @@ -56,6 +59,10 @@ public class SPService { public static final String KEY_INDEX_RECORD = "KEY_INDEX_RECORD"; public static final String KEY_ADB_SERVER = "KEY_ADB_SERVER"; + public static final String KEY_CONTROL_PORT = "KEY_CONTROL_PORT"; + + public static final String KEY_USE_EASY_MODE = "KEY_USE_EASY_MODE"; + public static final String KEY_EASY_MODE_NOTIFY_TIME = "KEY_EASY_MODE_NOTIFY_TIME"; private static SharedPreferences preferences; diff --git a/src/app/src/main/java/com/alipay/hulu/tools/IPerformStress.java b/src/common/src/main/java/com/alipay/hulu/common/service/base/AppGuardian.java similarity index 50% rename from src/app/src/main/java/com/alipay/hulu/tools/IPerformStress.java rename to src/common/src/main/java/com/alipay/hulu/common/service/base/AppGuardian.java index 78f6153..4865684 100644 --- a/src/app/src/main/java/com/alipay/hulu/tools/IPerformStress.java +++ b/src/common/src/main/java/com/alipay/hulu/common/service/base/AppGuardian.java @@ -13,11 +13,29 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.alipay.hulu.tools; +package com.alipay.hulu.common.service.base; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; -public interface IPerformStress { - public abstract void PerformEntry(int param); - public abstract void addOrReduceToTargetThread(int count); +public interface AppGuardian { + /** + * 当系统事件时调用 + */ + void onEventTrigger(ReceiveSystemEvent event); + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @interface AppGuardianEnable { + boolean value() default true; + } + + enum ReceiveSystemEvent { + SCREEN_LOCK, + SCREEN_UNLOCK, + APP_VISIBLE, + APP_INVISIBLE + } } diff --git a/src/common/src/main/java/com/alipay/hulu/common/tools/AdbIME.java b/src/common/src/main/java/com/alipay/hulu/common/tools/AdbIME.java new file mode 100644 index 0000000..a4f3912 --- /dev/null +++ b/src/common/src/main/java/com/alipay/hulu/common/tools/AdbIME.java @@ -0,0 +1,391 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.common.tools; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.Color; +import android.inputmethodservice.InputMethodService; +import android.os.Build; +import android.view.KeyEvent; +import android.view.View; +import android.view.WindowManager; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.ExtractedTextRequest; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputMethodManager; +import android.widget.TextView; + +import com.alipay.hulu.common.R; +import com.alipay.hulu.common.application.LauncherApplication; +import com.alipay.hulu.common.constant.Constant; +import com.alipay.hulu.common.injector.InjectorService; +import com.alipay.hulu.common.injector.param.RunningThread; +import com.alipay.hulu.common.injector.param.Subscriber; +import com.alipay.hulu.common.injector.provider.Param; +import com.alipay.hulu.common.injector.provider.Provider; +import com.alipay.hulu.common.utils.LogUtil; +import com.alipay.hulu.common.utils.MiscUtil; +import com.alipay.hulu.common.utils.StringUtil; + +/** + * Created by lezhou.wyl on 2018/2/8. + */ +@Provider(@Param(value = Constant.IME.IME_STATUS, type = Boolean.class)) +public class AdbIME extends InputMethodService { + private static final String TAG = "AdbIME"; + + private static final String IME_MESSAGE = "ADB_INPUT_TEXT"; + private static final String IME_SEARCH_MESSAGE = "ADB_SEARCH_TEXT"; + private static final String IME_CHARS = "ADB_INPUT_CHARS"; + private static final String IME_KEYCODE = "ADB_INPUT_CODE"; + private static final String IME_EDITORCODE = "ADB_EDITOR_CODE"; + private BroadcastReceiver mReceiver = null; + private InputMethodManager manager; + private TextView title; + + private String currentStatus = null; + + @Override + public void onCreate() { + try { + super.onCreate(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + getWindow().getWindow().setNavigationBarColor(Color.rgb(0x44, 0x44, 0x44)); + } + + // 输入法 + getWindow().getWindow().setType(WindowManager.LayoutParams.TYPE_INPUT_METHOD); + + this.manager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); + BackgroundExecutor.execute(new Runnable() { + @Override + public void run() { + while (!LauncherApplication.getInstance().hasFinishInit()) { + MiscUtil.sleep(50); + } + + // 初始化好后再注册 + InjectorService.g().register(AdbIME.this); + } + }); + } catch (Exception e) { + LogUtil.e(TAG, "Init ime failed", e); + } + } + + @Subscriber(value = @Param("RUNNING_STATUS"), thread = RunningThread.MAIN_THREAD) + public void setCurrentStatus(String currentStatus) { + this.currentStatus = currentStatus; + if (title != null) { + title.setText(StringUtil.equals(currentStatus, "record")? "点击输入": "点击切换输入法"); + } + } + + public boolean inputChars(int[] chars) { + if (chars != null) { + String msg = new String(chars, 0, chars.length); + InputConnection ic = getCurrentInputConnection(); + if (ic != null){ + ic.commitText(msg, 1); + return true; + } + } + return false; + } + + public boolean inputEditorCode(int code) { + if (code != -1) { + InputConnection ic = getCurrentInputConnection(); + if (ic != null) { + ic.performEditorAction(code); + return true; + } + } + return false; + } + + @Override + public View onCreateInputView() { + return getLayoutInflater().inflate(R.layout.adb_ime_input, null, false); + } + + @Override + public View onCreateCandidatesView() { + View mInputView = getLayoutInflater().inflate(R.layout.input_view, null); + + title = mInputView.findViewById(R.id.adb_ime_title); + if (StringUtil.equals(currentStatus, "record")) { + title.setText(R.string.ime__click_to_input); + } else { + title.setText(R.string.ime__click_change); + } + + if (mReceiver == null) { + IntentFilter filter = new IntentFilter(IME_MESSAGE); + filter.addAction(IME_SEARCH_MESSAGE); + filter.addAction(IME_CHARS); + filter.addAction(IME_KEYCODE); + filter.addAction(IME_EDITORCODE); + mReceiver = new AdbReceiver(); + registerReceiver(mReceiver, filter); + } + + // 当出现特殊情况,没有切换回系统输入法,需要用户手动点击切换 + mInputView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + manager.showInputMethodPicker(); + } + }); + + return mInputView; + } + + @Override + public void onFinishInput() { + try { + super.onFinishInput(); + setCandidatesViewShown(false); + } catch (Exception e) { + LogUtil.e(TAG, "What happened to finish input"); + } + InjectorService.g().pushMessage(Constant.IME.IME_STATUS, false); + } + + @Override + public void onStartInputView(EditorInfo info, boolean restarting) { + try { + super.onStartInputView(info, restarting); + setCandidatesViewShown(true); + } catch (Exception e) { + LogUtil.e(TAG, "What happened to start input"); + } + InjectorService.g().pushMessage(Constant.IME.IME_STATUS, true); + } + + public void onDestroy() { + if (mReceiver != null) { + unregisterReceiver(mReceiver); + } + + super.onDestroy(); + } + + + /** + * 输入文字 + * @param content + */ + @Subscriber(value = @Param(value = Constant.IME.IME_INPUT_TEXT, sticky = false), thread = RunningThread.MAIN_THREAD) + public boolean inputText(String content) { + if (StringUtil.isEmpty(content)) { + return false; + } + InputConnection ic = getCurrentInputConnection(); + if (ic != null) { + ic.commitText(content, 1); + return true; + } + return false; + } + + /** + * 输入文字并发送 + * @param content + */ + @Subscriber(value = @Param(value = Constant.IME.IME_INPUT_TEXT_ENTER, sticky = false), thread = RunningThread.MAIN_THREAD) + public boolean inputTextEnter(String content) { + if (content == null) { + return false; + } + InputConnection ic = getCurrentInputConnection(); + if (ic != null) { + ic.commitText(content, 1); + + // 需要额外点击发送 + pressEnter(ic); + return true; + } + return false; + } + + /** + * 输入keyCode + * @param keyCode + */ + @Subscriber(value = @Param(value = Constant.IME.IME_INPUT_KEY_CODE, sticky = false), thread = RunningThread.MAIN_THREAD) + public boolean inputKeyCode(int keyCode) { + if (keyCode != -1) { + InputConnection ic = getCurrentInputConnection(); + if (ic != null) { + ic.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode)); + return true; + } + } + return false; + } + + @Subscriber(value = @Param(value = Constant.IME.IME_CLEAR_TEXT, sticky = false), thread = RunningThread.MAIN_THREAD) + public void clearText() { + InputConnection ic = getCurrentInputConnection(); + CharSequence currentText = ic.getExtractedText(new ExtractedTextRequest(), 0).text; + CharSequence beforCursorText = ic.getTextBeforeCursor(currentText.length(), 0); + CharSequence afterCursorText = ic.getTextAfterCursor(currentText.length(), 0); + ic.deleteSurroundingText(beforCursorText.length(), afterCursorText.length()); + } + + @Subscriber(value = @Param(value = Constant.IME.IME_HIDE_IME, sticky = false), thread = RunningThread.MAIN_THREAD) + public void hideIme() { + requestHideSelf(0); + } + + + @Subscriber(value = @Param(value = Constant.IME.IME_OPERATION_ACTION, sticky = false), thread = RunningThread.MAIN_THREAD) + public void receiveImeOperation(KeyboardActionEnum action) { + if (action == null) { + LogUtil.e(TAG, "Keyboard action is null"); + return; + } + + InputConnection ic = getCurrentInputConnection(); + if (ic == null) { + LogUtil.e(TAG, "No valid input connection"); + return; + } + + switch (action) { + case ENTER: + pressEnter(ic); + break; + case BACKSPACE: + ic.deleteSurroundingText(1, 0); + break; + case LEFT: + int leftTextLength = getTextLengthBefore(ic); + ic.setSelection(leftTextLength - 1, leftTextLength - 1); + break; + case RIGHT: + leftTextLength = getTextLengthBefore(ic); + ic.setSelection(leftTextLength + 1, leftTextLength + 1); + break; + case CLEAR: + ic.deleteSurroundingText(Integer.MAX_VALUE, Integer.MAX_VALUE); + break; + } + } + + /** + * 获得左侧文字长度 + * @param ic + * @return + */ + private int getTextLengthBefore(InputConnection ic) { + CharSequence cs = ic.getTextBeforeCursor(Integer.MAX_VALUE, 0); + if (cs != null) { + return cs.length(); + } + + return 0; + } + + /** + * Enter键 + * @param ic + */ + private void pressEnter(InputConnection ic) { + if (ic != null) { + + // 需要额外点击发送 + EditorInfo editorInfo = getCurrentInputEditorInfo(); + if (editorInfo != null) { + int options = editorInfo.imeOptions; + final int actionId = options & EditorInfo.IME_MASK_ACTION; + + switch (actionId) { + case EditorInfo.IME_ACTION_SEARCH: + case EditorInfo.IME_ACTION_GO: + case EditorInfo.IME_ACTION_SEND: + sendDefaultEditorAction(true); + break; + default: + ic.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER)); + } + } + } + } + + class AdbReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + boolean sendFlag = false; + + if (intent.getAction().equals(IME_MESSAGE)) { + String msg = intent.getStringExtra("msg"); + sendFlag = inputText(msg); + } else if (intent.getAction().equals(IME_SEARCH_MESSAGE)) { + // 输入并搜索 + String msg = intent.getStringExtra("msg"); + clearText(); + sendFlag = inputTextEnter(msg); + } else if (intent.getAction().equals(IME_CHARS)) { + int[] chars = intent.getIntArrayExtra("chars"); + sendFlag = inputChars(chars); + } else if (intent.getAction().equals(IME_KEYCODE)) { + int code = intent.getIntExtra("code", -1); + sendFlag = inputKeyCode(code); + } else if (intent.getAction().equals(IME_EDITORCODE)) { + int code = intent.getIntExtra("code", -1); + sendFlag = inputEditorCode(code); + } + + // 进行了输入,发广播通知切换回原始输入法 + if (sendFlag) { + hideIme(); + } + } + } + + public enum KeyboardActionEnum { + ENTER("enter"), + BACKSPACE("backspace"), + LEFT("left"), + RIGHT("right"), + CLEAR("clear") + ; + private String name; + KeyboardActionEnum(String name) { + this.name = name; + } + + public static KeyboardActionEnum getActionByName(String name) { + if (StringUtil.isEmpty(name)) { + return null; + } + + for (KeyboardActionEnum action: values()) { + if (name.equalsIgnoreCase(action.name)) { + return action; + } + } + + return null; + } + } +} diff --git a/src/common/src/main/java/com/alipay/hulu/common/tools/CmdTools.java b/src/common/src/main/java/com/alipay/hulu/common/tools/CmdTools.java index 5c23ec4..426b984 100644 --- a/src/common/src/main/java/com/alipay/hulu/common/tools/CmdTools.java +++ b/src/common/src/main/java/com/alipay/hulu/common/tools/CmdTools.java @@ -33,6 +33,8 @@ import com.alipay.hulu.common.injector.param.Subscriber; import com.alipay.hulu.common.injector.provider.Param; import com.alipay.hulu.common.service.SPService; +import com.alipay.hulu.common.service.base.AppGuardian; +import com.alipay.hulu.common.trigger.Trigger; import com.alipay.hulu.common.utils.FileUtils; import com.alipay.hulu.common.utils.LogUtil; import com.alipay.hulu.common.utils.MiscUtil; @@ -90,8 +92,6 @@ public class CmdTools { private static String TAG = "CmdTools"; - private static final int MODE_APPEND = 0; - public static final String FATAL_ADB_CANNOT_RECOVER = "fatalAdbNotRecover"; public static final String ERROR_NO_CONNECTION = "HULU_ERROR_NO_CONNECTION"; @@ -116,6 +116,9 @@ public class CmdTools { private static PidWatcher watcher; + // ADB解锁后恢复工具 + private static volatile CmdToolsGuardianService guardian; + private static File currentLogFile; private static ConcurrentLinkedQueue logs = null; @@ -128,8 +131,6 @@ public static void cancelForceAdb(){ isRoot = null; } - public static long LAST_ADB_RETRY_TIME = 0; - /** * 开始adb日志记录 */ @@ -632,6 +633,7 @@ public static String _execAdbCmd(final String cmd, final int wait) { sb.append(new String(bytes)); } } + logcatCmd(stream.getLocalId() + "@" + "shell:->" + sb.toString()); streams.remove(stream); return sb.toString(); } catch (IllegalStateException e) { @@ -716,6 +718,7 @@ public static String execShellCmdWithTimeout(final String cmd, @IntRange(from = sb.append(new String(bytes)); } } + logcatCmd(stream.getLocalId() + "@" + "shell:->" + sb.toString()); streams.remove(stream); return sb.toString(); } catch (IllegalStateException e) { @@ -769,6 +772,7 @@ private static String retryExecAdb(String cmd, long wait) { sb.append(new String(bytes)); } } + logcatCmd(stream.getLocalId() + "@" + "shell:->" + sb.toString()); streams.remove(stream); return sb.toString(); } catch (IOException e) { @@ -817,6 +821,8 @@ private static String execAdbCmdWithStatus(final String cmd, final int wait) { sb.append(new String(bytes)); } } + + logcatCmd(stream.getLocalId() + "@" + "shell:->" + sb.toString()); streams.remove(stream); return sb.toString(); } catch (IllegalStateException e) { @@ -997,6 +1003,10 @@ public static synchronized boolean generateConnection() { // ADB成功连接后,开启ADB状态监测 startAdbStatusCheck(); + + // 触发ADB连接状态监听 + LauncherApplication.getInstance().triggerAtTime(Trigger.TRIGGER_TIME_ADB_CONNECT); + return true; } @@ -1143,7 +1153,7 @@ public static StringBuilder execRootCmd(String cmd, String log, Boolean ret, Con public static void writeFileData(String monkeyLog, String message, Context ct) { String time = ""; try { - FileOutputStream fout = ct.openFileOutput(monkeyLog, MODE_APPEND); + FileOutputStream fout = ct.openFileOutput(monkeyLog, Context.MODE_APPEND); SimpleDateFormat formatter = new SimpleDateFormat("+++ HH:mm:ss"); Date curDate = new Date(System.currentTimeMillis());//获取当前时间 @@ -1323,6 +1333,14 @@ public static String getTopActivity() { } } + /** + * 获取adb状态 + * @return + */ + public static boolean getConnectionStatus() { + return connection != null && connection.isFine(); + } + public static String getTopActivity(String pkg) { if (Build.VERSION.SDK_INT >= 29) { return filterBackActivity(execAdbCmd("window visible-apps | grep -e \"Activity #.*" + pkg + "\"", 1000)); @@ -1435,6 +1453,10 @@ private static void startAdbStatusCheck() { scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); } + if (guardian == null) { + guardian = new CmdToolsGuardianService(); + } + scheduledExecutorService.schedule(new Runnable() { @Override public void run() { @@ -1445,77 +1467,89 @@ public void run() { } LAST_RUNNING_TIME = currentTime; - String result = null; - try { - result = execAdbCmd("echo '1'", 5000); - } catch (Exception e) { - LogUtil.e(TAG, "Check adb status throw :" + e.getMessage(), e); + int result = checkAdbStatus(); + if (result < 0) { + return; } - if (!StringUtil.equals("1", StringUtil.trim(result))) { - // 等2s再检验一次 - MiscUtil.sleep(2000); + // 15S 检查一次 + scheduledExecutorService.schedule(this, 15, TimeUnit.SECONDS); + } + }, 15, TimeUnit.SECONDS); + } - boolean genResult = false; + /** + * 检查并尝试恢复ADB连接 + * @return + */ + private static int checkAdbStatus() { + String result = null; + try { + result = execAdbCmd("echo '1'", 5000); + } catch (Exception e) { + LogUtil.e(TAG, "Check adb status throw :" + e.getMessage(), e); + } - // double check机制,防止单次偶然失败带来重连 - String doubleCheck = null; - try { - doubleCheck = execAdbCmd("echo '1'", 5000); - } catch (Exception e) { - LogUtil.e(TAG, "Check adb status throw :" + e.getMessage(), e); - } - if (!StringUtil.equals("1", StringUtil.trim(doubleCheck))) { - // 尝试恢复3次 - for (int i = 0; i < 3; i++) { - // 关停无用连接 - if (connection != null && connection.isFine()) { - try { - connection.close(); - } catch (IOException e) { - LogUtil.e(TAG, "Catch java.io.IOException: " + e.getMessage(), e); - } finally { - connection = null; - } - } - - // 清理下当前已连接进程 - clearProcesses(); - - // 尝试重连 - genResult = generateConnection(); - if (genResult) { - break; - } + if (!StringUtil.equals("1", StringUtil.trim(result))) { + // 等2s再检验一次 + MiscUtil.sleep(2000); + + boolean genResult = false; + + // double check机制,防止单次偶然失败带来重连 + String doubleCheck = null; + try { + doubleCheck = execAdbCmd("echo '1'", 5000); + } catch (Exception e) { + LogUtil.e(TAG, "Check adb status throw :" + e.getMessage(), e); + } + if (!StringUtil.equals("1", StringUtil.trim(doubleCheck))) { + // 尝试恢复3次 + for (int i = 0; i < 3; i++) { + // 关停无用连接 + if (connection != null && connection.isFine()) { + try { + connection.close(); + } catch (IOException e) { + LogUtil.e(TAG, "Catch java.io.IOException: " + e.getMessage(), e); + } finally { + connection = null; } + } - // 恢复失败 - if (!genResult) { - Context con = LauncherApplication.getInstance().loadActivityOnTop(); - if (con == null) { - con = LauncherApplication.getInstance().loadRunningService(); - } + // 清理下当前已连接进程 + clearProcesses(); - if (con == null) { - LauncherApplication.getInstance().showToast(StringUtil.getString(R.string.cmd__adb_break)); - return; - } + // 尝试重连 + genResult = generateConnection(); + if (genResult) { + break; + } + } - // 回首页 - LauncherApplication.getInstance().showDialog(con, StringUtil.getString(R.string.cmd__adb_break), StringUtil.getString(R.string.constant__sure), null); + // 恢复失败 + if (!genResult) { + // 通知各个功能ADB挂了 + InjectorService.g().pushMessage(FATAL_ADB_CANNOT_RECOVER); - // 通知各个功能ADB挂了 - InjectorService.g().pushMessage(FATAL_ADB_CANNOT_RECOVER); + Context con = LauncherApplication.getInstance().loadActivityOnTop(); + if (con == null) { + con = LauncherApplication.getInstance().loadRunningService(); + } - return; - } + if (con == null) { + LauncherApplication.getInstance().showToast(StringUtil.getString(R.string.cmd__adb_break)); + return -1; } - } - // 15S 检查一次 - scheduledExecutorService.schedule(this, 15, TimeUnit.SECONDS); + // 回首页 + LauncherApplication.getInstance().showDialog(con, StringUtil.getString(R.string.cmd__adb_break), StringUtil.getString(R.string.constant__sure), null); + return -1; + } } - }, 15, TimeUnit.SECONDS); + } + + return 0; } /** @@ -1675,6 +1709,18 @@ public static String cpExecutable(File sourceF) { return exec; } + private static class CmdToolsGuardianService { + private CmdToolsGuardianService() { + InjectorService.g().register(this); + } + @Subscriber(value = @Param(value = LauncherApplication.SYSTEM_GUARDIAN_EVENT, sticky = false), thread = RunningThread.BACKGROUND) + public void onSystemEvent(AppGuardian.ReceiveSystemEvent event) { + if (event == AppGuardian.ReceiveSystemEvent.SCREEN_UNLOCK && LAST_RUNNING_TIME > 0 && connection == null) { + // 解锁前可能adb连接被强制中断,解锁后立刻尝试恢复一次 + checkAdbStatus(); + } + } + } /** *

Reverses the order of the given array.

diff --git a/src/common/src/main/java/com/alipay/hulu/common/trigger/ScreenOrientationChangeTrigger.java b/src/common/src/main/java/com/alipay/hulu/common/trigger/ScreenOrientationChangeTrigger.java new file mode 100644 index 0000000..b3aa5b3 --- /dev/null +++ b/src/common/src/main/java/com/alipay/hulu/common/trigger/ScreenOrientationChangeTrigger.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.common.trigger; + +import android.app.Activity; +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.view.WindowManager; + +import com.alipay.hulu.common.application.LauncherApplication; +import com.alipay.hulu.common.injector.InjectorService; +import com.alipay.hulu.common.utils.LogUtil; +import com.alipay.hulu.common.utils.StringUtil; + +import static com.alipay.hulu.common.constant.Constant.SCREEN_ORIENTATION; + +@Trigger(Trigger.TRIGGER_TIME_HOME_PAGE) +public class ScreenOrientationChangeTrigger implements Runnable { + private static final String TAG = ScreenOrientationChangeTrigger.class.getSimpleName(); + + @Override + public void run() { + // 注册屏幕旋转方向监听器 + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); + LauncherApplication.getInstance().registerReceiver(new ScreenOrientationChangeEventListener(),filter); + } + + + + /** + * 屏幕方向旋转监听器 + */ + private static class ScreenOrientationChangeEventListener extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (intent == null) { + return; + } + + String action = intent.getAction(); + // 屏幕旋转 + if (StringUtil.equals(action, Intent.ACTION_CONFIGURATION_CHANGED)) { + LogUtil.i(TAG, "Intent extras: " + intent.getExtras()); + LogUtil.i(TAG, "Intent data: " + intent.getData()); + LogUtil.i(TAG, "Intent scheme: " + intent.getScheme()); + + // 等Injector初始化 + if (InjectorService.g() == null) { + return; + } + + // 如果有Activity + Activity activity = (Activity) LauncherApplication.getInstance().loadActivityOnTop(); + if (activity != null) { + InjectorService.g().pushMessage(SCREEN_ORIENTATION, activity.getWindowManager().getDefaultDisplay().getRotation()); + return; + } + + // 从Service获取 + Service service = (Service) LauncherApplication.getInstance().loadRunningService(); + + // 没有就只能忽略 + if (service == null) { + LogUtil.w(TAG, "No Foreground service or activity"); + return; + } + + // 找WindowManager + WindowManager wm = (WindowManager) service.getSystemService(Context.WINDOW_SERVICE); + int rotation = wm.getDefaultDisplay().getRotation(); + + // 推送屏幕方向 + InjectorService.g().pushMessage(SCREEN_ORIENTATION, rotation); + } + } + } +} diff --git a/src/common/src/main/java/com/alipay/hulu/common/trigger/Trigger.java b/src/common/src/main/java/com/alipay/hulu/common/trigger/Trigger.java new file mode 100644 index 0000000..6fba8cd --- /dev/null +++ b/src/common/src/main/java/com/alipay/hulu/common/trigger/Trigger.java @@ -0,0 +1,28 @@ +package com.alipay.hulu.common.trigger; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Trigger { + String TRIGGER_TIME_START = "TRIGGER_TIME_START"; + String TRIGGER_TIME_START_FINISH = "TRIGGER_TIME_START_FINISH"; + String TRIGGER_TIME_HOME_PAGE = "TRIGGER_TIME_HOME_PAGE"; + String TRIGGER_TIME_SCHEME_INIT = "TRIGGER_TIME_SCHEME_INIT"; + String TRIGGER_TIME_ADB_CONNECT = "TRIGGER_TIME_ADB_CONNECT"; + + /** + * 触发环节 + * @return + */ + String[] value() default ""; + + /** + * 触发器级别,越大越优先触发 + * @return + */ + int level() default 1; +} diff --git a/src/common/src/main/java/com/alipay/hulu/common/utils/ContextUtil.java b/src/common/src/main/java/com/alipay/hulu/common/utils/ContextUtil.java index dff92a9..ffd07d5 100644 --- a/src/common/src/main/java/com/alipay/hulu/common/utils/ContextUtil.java +++ b/src/common/src/main/java/com/alipay/hulu/common/utils/ContextUtil.java @@ -19,8 +19,17 @@ import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.os.Build; +import android.os.LocaleList; + import androidx.appcompat.view.ContextThemeWrapper; +import com.alipay.hulu.common.application.LauncherApplication; + +import java.util.Locale; + /** * Context工具类 * Created by cathor on 2017/12/15. @@ -74,4 +83,16 @@ public static PackageInfo getPackageInfoByName(Context context, String packageNa } return packageInfo; } + + public static void updateResources(Context context) { + Resources resources = context.getResources(); + Configuration configuration = resources.getConfiguration(); + Locale locale = LauncherApplication.getInstance().getLanguageLocale(); + configuration.setLocale(locale); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + configuration.setLocales(new LocaleList(locale)); + context.createConfigurationContext(configuration); + } + resources.updateConfiguration(configuration, resources.getDisplayMetrics()); + } } diff --git a/src/common/src/main/java/com/alipay/hulu/common/utils/DecompressUtil.java b/src/common/src/main/java/com/alipay/hulu/common/utils/DecompressUtil.java index 36a303f..d88d66d 100644 --- a/src/common/src/main/java/com/alipay/hulu/common/utils/DecompressUtil.java +++ b/src/common/src/main/java/com/alipay/hulu/common/utils/DecompressUtil.java @@ -61,12 +61,30 @@ public static File decompressZip(File originFile, File targetFolder) { targetFolder.mkdirs(); } + String originCanonicalPath; + try { + originCanonicalPath = originFile.getCanonicalPath(); + } catch (IOException e) { + LogUtil.e(TAG, "Get Canonical Path Failed, throw exception", e); + originCanonicalPath = originFile.getPath(); + } + ZipInputStream zip = new ZipInputStream(inputStream); ZipEntry zipEntry; String szName; try { while ((zipEntry = zip.getNextEntry()) != null) { szName = zipEntry.getName(); + + + File f = new File(originFile, zipEntry.getName()); + String canonicalPath = f.getCanonicalPath(); + if (!canonicalPath.startsWith(originCanonicalPath)) { + LogUtil.e(TAG, "Zip Entry: " + szName + ", not belong to zip file " + canonicalPath); + // 忽略该zip压缩包 + continue; + } + if (zipEntry.isDirectory()) { //获取部件的文件夹名 szName = szName.substring(0, szName.length() - 1); diff --git a/src/common/src/main/java/com/alipay/hulu/common/utils/DeviceInfoUtil.java b/src/common/src/main/java/com/alipay/hulu/common/utils/DeviceInfoUtil.java index 5e8519e..cc7d09b 100644 --- a/src/common/src/main/java/com/alipay/hulu/common/utils/DeviceInfoUtil.java +++ b/src/common/src/main/java/com/alipay/hulu/common/utils/DeviceInfoUtil.java @@ -28,7 +28,9 @@ import java.net.InetAddress; import java.net.NetworkInterface; import java.net.SocketException; +import java.util.Arrays; import java.util.Enumeration; +import java.util.List; /** @@ -99,6 +101,27 @@ public static String getCPUABI() { } } + /** + * 兼容X86的arm64-v8a或armeabi-v7a,兜底armeabi + * @return + */ + public static String getCPUABIInArm() { + List supportAbis; + if (Build.VERSION.SDK_INT < 21) { + String originAbi = Build.CPU_ABI; + String secondAbi = Build.CPU_ABI2; + supportAbis = Arrays.asList(originAbi, secondAbi); + } else { + supportAbis = Arrays.asList(Build.SUPPORTED_ABIS); + } + if (supportAbis.contains("arm64-v8a")) { + return "arm64-v8a"; + } else if (supportAbis.contains("armeabi-v7a")) { + return "armeabi-v7a"; + } + return "armeabi"; + } + public static String getIP() { InetAddress ip = getLocalInetAddress(); return ip == null ? "" : ip.getHostAddress(); diff --git a/src/common/src/main/java/com/alipay/hulu/common/utils/FileUtils.java b/src/common/src/main/java/com/alipay/hulu/common/utils/FileUtils.java index 0341696..7034b23 100644 --- a/src/common/src/main/java/com/alipay/hulu/common/utils/FileUtils.java +++ b/src/common/src/main/java/com/alipay/hulu/common/utils/FileUtils.java @@ -335,7 +335,10 @@ private static String getImagePath(Uri uri, String selection) { .query(uri, null, selection, null, null); if (cursor != null) { if (cursor.moveToFirst()) { - path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA)); + int columnIndex = cursor.getColumnIndex(MediaStore.Images.Media.DATA); + if (columnIndex >= 0) { + path = cursor.getString(columnIndex); + } } cursor.close(); } diff --git a/src/common/src/main/java/com/alipay/hulu/common/utils/PermissionUtil.java b/src/common/src/main/java/com/alipay/hulu/common/utils/PermissionUtil.java index 20fe47d..ae3b28a 100644 --- a/src/common/src/main/java/com/alipay/hulu/common/utils/PermissionUtil.java +++ b/src/common/src/main/java/com/alipay/hulu/common/utils/PermissionUtil.java @@ -29,6 +29,9 @@ import com.alipay.hulu.common.R; import com.alipay.hulu.common.application.LauncherApplication; +import com.alipay.hulu.common.constant.Constant; +import com.alipay.hulu.common.injector.InjectorService; +import com.alipay.hulu.common.injector.param.SubscribeParamEnum; import com.alipay.hulu.common.tools.BackgroundExecutor; import com.alipay.hulu.common.tools.CmdTools; import com.alipay.hulu.common.utils.activity.PermissionDialogActivity; @@ -66,7 +69,7 @@ public class PermissionUtil { * @param activity * @param callback */ - public static void requestPermissions(@NonNull final List permissions, final Activity activity, @NonNull final OnPermissionCallback callback) { + public static void requestPermissions(@NonNull final List permissions, final Context activity, @NonNull final OnPermissionCallback callback) { // 可能悬浮窗Dialog还没关闭,延后一下权限申请任务 LauncherApplication.getInstance().runOnUiThread(new Runnable() { @Override @@ -100,6 +103,11 @@ public void run() { intent.putStringArrayListExtra(PermissionDialogActivity.PERMISSIONS_KEY, new ArrayList<>(permissions)); } + // 非activity启动 + if (!(activity instanceof Activity)) { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } + // 设置回调idz int currentIdx = callbackCount.getAndIncrement(); intent.putExtra(PermissionDialogActivity.PERMISSION_IDX_KEY, currentIdx); @@ -227,6 +235,37 @@ public Boolean call() { } } + + + /** + * 获取权限状态 + * @param context + * @param permission + * @return + */ + public static boolean getPermissionStatus(Context context, String permission) { + if ("float".equals(permission)) { + return FloatWindowManager.getInstance().checkPermission(context); + } else if ("root".equals(permission)) { + return CmdTools.isRooted(); + } else if ("background".equals(permission)) { + return true; + } else if ("adb".equals(permission)) { + return CmdTools.getConnectionStatus(); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && Settings.ACTION_USAGE_ACCESS_SETTINGS.equals(permission)) { + // 4.4及以上直接申请Usage Access权限 + return PermissionUtil.isUsageStatPermissionOn(context); + } else if (Settings.ACTION_ACCESSIBILITY_SETTINGS.equals(permission)) { + // 没有注册上AccessibilityService,需要开辅助功能 + return InjectorService.g().getMessage(SubscribeParamEnum.ACCESSIBILITY_SERVICE, null) != null; + // 其他权限记录在需要申请的权限中,如果版本在6.0及以上再进行申请 + } else if (StringUtil.equals("screenRecord", permission)) { + return InjectorService.g().getMessage(Constant.EVENT_RECORD_SCREEN_CODE, Intent.class) != null; + } else { + return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED; + } + } + /** * 检查需动态申请的权限,对未获取的权限进行申请 * @param activity diff --git a/src/common/src/main/java/com/alipay/hulu/common/utils/ProcessUtil.java b/src/common/src/main/java/com/alipay/hulu/common/utils/ProcessUtil.java new file mode 100644 index 0000000..890721b --- /dev/null +++ b/src/common/src/main/java/com/alipay/hulu/common/utils/ProcessUtil.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.common.utils; + +import android.app.ActivityManager; +import android.content.Context; + +import com.alipay.hulu.common.application.LauncherApplication; + +public class ProcessUtil { + /** + * 获取当前进程名 + */ + public static String getCurrentProcessName() { + int pid = android.os.Process.myPid(); + String processName = ""; + ActivityManager manager = (ActivityManager) LauncherApplication.getContext().getApplicationContext().getSystemService + (Context.ACTIVITY_SERVICE); + for (ActivityManager.RunningAppProcessInfo process : manager.getRunningAppProcesses()) { + if (process.pid == pid) { + processName = process.processName; + } + } + return processName; + } + + /** + * 检测进程是否在前台 + * @return + */ + public static boolean isProgressForeground() { + int pid = android.os.Process.myPid(); + ActivityManager.RunningAppProcessInfo targetProcess = null; + ActivityManager manager = (ActivityManager) LauncherApplication.getContext().getApplicationContext().getSystemService + (Context.ACTIVITY_SERVICE); + for (ActivityManager.RunningAppProcessInfo process : manager.getRunningAppProcesses()) { + if (process.pid == pid) { + targetProcess = process; + } + } + if (targetProcess != null) { + return targetProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; + } + return false; + } + + /** + * 判断当前进程是否是主进程 + * @return + */ + public static boolean isMainProcess() { + return StringUtil.equals(LauncherApplication.getContext().getApplicationContext().getPackageName(), getCurrentProcessName()); + } +} diff --git a/src/common/src/main/java/com/alipay/hulu/common/utils/StringUtil.java b/src/common/src/main/java/com/alipay/hulu/common/utils/StringUtil.java index 147af60..ae69cfc 100644 --- a/src/common/src/main/java/com/alipay/hulu/common/utils/StringUtil.java +++ b/src/common/src/main/java/com/alipay/hulu/common/utils/StringUtil.java @@ -367,7 +367,7 @@ public static String getString(Context context, @StringRes int res, Object... ar * @param contents * @return */ - public static String join(CharSequence joiner, List contents) { + public static String join(CharSequence joiner, List contents) { if (contents == null || contents.size() == 0) { return ""; } @@ -385,7 +385,7 @@ public static String join(CharSequence joiner, List contents) { * @param contents * @return */ - public static String join(CharSequence joiner, CharSequence... contents) { + public static String join(CharSequence joiner, T... contents) { if (contents == null || contents.length == 0) { return ""; } diff --git a/src/common/src/main/java/com/alipay/hulu/common/utils/activity/PermissionDialogActivity.java b/src/common/src/main/java/com/alipay/hulu/common/utils/activity/PermissionDialogActivity.java index e94bd3d..d6b958d 100644 --- a/src/common/src/main/java/com/alipay/hulu/common/utils/activity/PermissionDialogActivity.java +++ b/src/common/src/main/java/com/alipay/hulu/common/utils/activity/PermissionDialogActivity.java @@ -17,21 +17,17 @@ import android.Manifest; import android.accessibilityservice.AccessibilityService; -import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.Configuration; -import android.content.res.Resources; import android.media.projection.MediaProjectionManager; +import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.os.LocaleList; +import android.os.PowerManager; import android.provider.Settings; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.app.ActivityCompat; import android.text.Html; import android.view.Display; import android.view.Gravity; @@ -40,6 +36,11 @@ import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.app.ActivityCompat; import com.alipay.hulu.common.R; import com.alipay.hulu.common.application.LauncherApplication; @@ -59,13 +60,13 @@ import com.android.permission.FloatWindowManager; import com.android.permission.rom.MiuiUtils; import com.android.permission.rom.RomUtils; +import com.android.permission.rom.VivoUtils; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -93,6 +94,7 @@ public class PermissionDialogActivity extends Activity implements View.OnClickLi public static final int PERMISSION_ANDROID = 8; public static final int PERMISSION_DYNAMIC = 9; public static final int PERMISSION_BACKGROUND = 10; + public static final int PERMISSION_POWER_SAVE = 11; public static volatile boolean runningStatus = false; @@ -259,6 +261,9 @@ private void groupPermissions() { case "background": group = PERMISSION_BACKGROUND; break; + case "powerSave": + group = PERMISSION_POWER_SAVE; + break; default: if (permission.startsWith("Android=")) { group = PERMISSION_ANDROID; @@ -365,6 +370,11 @@ private void processSinglePermission() { return; } break; + case PERMISSION_POWER_SAVE: + if (!processPowerSavePermission()) { + return; + } + break; } // 成功的直接processed @@ -828,6 +838,58 @@ public void run() { }); return false; } + } else if (RomUtils.isVivoSystem()) { + final String content = "Vivo请在开启后台弹出界面权限"; + if (!SPService.getBoolean(content, false)) { + showAction(content, "我已开启", new Runnable() { + @Override + public void run() { + SPService.putBoolean(content, true); + processedAction(); + } + }, "前往开启", new Runnable() { + @Override + public void run() { + VivoUtils.applyBackgroundPermission(PermissionDialogActivity.this); + } + }); + return false; + } + } + return true; + } + + /** + * 处理省电优化权限 + * @return + */ + private boolean processPowerSavePermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + final PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE); + if (!powerManager.isIgnoringBatteryOptimizations(getPackageName())) { + showAction(getString(R.string.permission__ignore_battery_optimization_permission), getString(R.string.permission__go_to_open), new Runnable() { + @Override + public void run() { + try { + Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); + intent.setData(Uri.parse("package:" + getPackageName())); + startActivity(intent); + } catch (Exception e) { + e.printStackTrace(); + } + } + }, getString(R.string.permission__opened), new Runnable() { + @Override + public void run() { + if (powerManager.isIgnoringBatteryOptimizations(getPackageName())) { + processedAction(); + } else { + Toast.makeText(PermissionDialogActivity.this, "暂未加入后台白名单", Toast.LENGTH_SHORT).show(); + } + } + }); + return false; + } } return true; } @@ -1139,22 +1201,14 @@ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permis @Override protected void attachBaseContext(Context newBase) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - newBase = updateResources(newBase); - } super.attachBaseContext(newBase); + ContextUtil.updateResources(this); } - @TargetApi(Build.VERSION_CODES.N) - private static Context updateResources(Context context) { - - Resources resources = context.getResources(); - Locale locale = LauncherApplication.getInstance().getLanguageLocale(); - - Configuration configuration = resources.getConfiguration(); - configuration.setLocale(locale); - configuration.setLocales(new LocaleList(locale)); - return context.createConfigurationContext(configuration); + @Override + public void onConfigurationChanged(@NonNull Configuration newConfig) { + super.onConfigurationChanged(newConfig); + ContextUtil.updateResources(this); } /** diff --git a/src/common/src/main/res/layout/adb_ime_input.xml b/src/common/src/main/res/layout/adb_ime_input.xml new file mode 100644 index 0000000..9f79f64 --- /dev/null +++ b/src/common/src/main/res/layout/adb_ime_input.xml @@ -0,0 +1,24 @@ + + + + + \ No newline at end of file diff --git a/src/app/src/main/res/layout/input_view.xml b/src/common/src/main/res/layout/input_view.xml similarity index 91% rename from src/app/src/main/res/layout/input_view.xml rename to src/common/src/main/res/layout/input_view.xml index ee11b44..1fd9bee 100644 --- a/src/app/src/main/res/layout/input_view.xml +++ b/src/common/src/main/res/layout/input_view.xml @@ -18,8 +18,11 @@ android:layout_width="fill_parent" android:layout_height="wrap_content" android:background="#444" + android:paddingTop="4dp" + android:paddingBottom="4dp" > ) 选择目录 MIUI请开启后台弹出界面权限 + 请允许SoloPi忽略省电优化策略,以确保ADB、录屏等服务能正常运行 我已开启 前往开启 读取日历 @@ -79,4 +80,6 @@ 开启辅助功能 尝试关闭现有UIAutomator与Instrument进程 强制关闭 + 点击切换输入法 + 点击输入 diff --git a/src/common/src/main/res/values/strings.xml b/src/common/src/main/res/values/strings.xml index 07689af..51f3374 100644 --- a/src/common/src/main/res/values/strings.xml +++ b/src/common/src/main/res/values/strings.xml @@ -49,6 +49,7 @@ ) Select Folder Please grant \"Display pop-up window\" permission in MIUI + Please grant \"Ignore battery optimization\" permission Go to Grant Granted Read Calendar @@ -79,4 +80,6 @@ Starting Accessibility Service Try to kill uiautomator and instrument processes Force stop + Click to input + Change IME diff --git a/src/app/src/main/res/xml/methods.xml b/src/common/src/main/res/xml/methods.xml similarity index 100% rename from src/app/src/main/res/xml/methods.xml rename to src/common/src/main/res/xml/methods.xml diff --git a/src/gradle.properties b/src/gradle.properties index 1257a29..6c7f74b 100644 --- a/src/gradle.properties +++ b/src/gradle.properties @@ -20,12 +20,21 @@ org.gradle.configureondemand=true org.gradle.parallel=true ## d8模式 -android.enableD8 = true +android.enableD8=true ## R8模式 -android.enableR8 = true +android.enableR8=true #指定SoloPi控制的类包名 SCAN_LIST=com.alipay.hulu android.useAndroidX=true -android.enableJetifier=true \ No newline at end of file +android.enableJetifier=true + +ANDROIDX_APPCOMPAT_VERSION=1.3.1 +ANDROIDX_RECYCLERVIEW_VERSION=1.2.1 +ANDROIDX_MATERIAL_VERSION=1.4.0 +ANDROIDX_MULTIDEX_VERSION=2.0.1 +FASTJSON_VERSION=1.2.79 +COMMON_IO_VERSION=2.6 +ANDROIDX_SUPPORT_V4_VERSION=1.0.0 +ANDROIDX_SUPPORT_CORE_UTILS_VERSION=1.0.0 diff --git a/src/mdlibrary/build.gradle b/src/mdlibrary/build.gradle index 6e8bae3..34385e2 100755 --- a/src/mdlibrary/build.gradle +++ b/src/mdlibrary/build.gradle @@ -24,5 +24,5 @@ android { dependencies { implementation 'com.linkedin.dexmaker:dexmaker:2.19.1' - compileOnly 'androidx.appcompat:appcompat:1.0.0' + compileOnly "androidx.appcompat:appcompat:${ANDROIDX_APPCOMPAT_VERSION}" } diff --git a/src/permission/build.gradle b/src/permission/build.gradle index 5684b55..9247d3a 100644 --- a/src/permission/build.gradle +++ b/src/permission/build.gradle @@ -13,6 +13,9 @@ android { versionName "1.0" testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' } + lintOptions { + abortOnError false + } buildTypes { release { minifyEnabled false diff --git a/src/permission/src/main/java/com/android/permission/rom/VivoUtils.java b/src/permission/src/main/java/com/android/permission/rom/VivoUtils.java index b432695..83fac6d 100644 --- a/src/permission/src/main/java/com/android/permission/rom/VivoUtils.java +++ b/src/permission/src/main/java/com/android/permission/rom/VivoUtils.java @@ -120,6 +120,47 @@ public static int getFloatPermissionStatus(Context context) { } } + /** + * 去i管家申请页面 + */ + public static void applyBackgroundPermission(final Context context) { + Intent appIntent = context.getPackageManager().getLaunchIntentForPackage("com.iqoo.secure"); + if(appIntent != null){ + try { + context.startActivity(appIntent); + if (context instanceof Activity) { + ((Activity) context).runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(context, R.string.vivo__open_background, Toast.LENGTH_LONG).show(); + } + }); + } + } catch (Exception e) { + e.printStackTrace(); + + if (context instanceof Activity) { + ((Activity) context).runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(context, R.string.vivo__open_background_noopen, Toast.LENGTH_LONG).show(); + } + }); + } + } + } else { + if (context instanceof Activity) { + ((Activity) context).runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(context, R.string.vivo__open_background_noopen, Toast.LENGTH_LONG).show(); + } + }); + } + } + } + + /** * vivo比较新的系统获取方法 * diff --git a/src/permission/src/main/res/values/strings.xml b/src/permission/src/main/res/values/strings.xml index d9f9d16..d6a1b11 100644 --- a/src/permission/src/main/res/values/strings.xml +++ b/src/permission/src/main/res/values/strings.xml @@ -5,5 +5,7 @@ 权限管理->悬浮窗\"页面开启权限]]> 权限管理->悬浮窗\"页面开启权限]]> 权限管理->悬浮窗\"页面开启权限]]> + 权限管理->后台弹出界面\"页面开启权限]]> + 权限管理->后台弹出界面\"页面开启权限]]> 进入设置页面失败,请手动设置 \ No newline at end of file diff --git a/src/portal/build.gradle b/src/portal/build.gradle index b0b3ad1..448451b 100644 --- a/src/portal/build.gradle +++ b/src/portal/build.gradle @@ -67,10 +67,14 @@ android { dexOptions { javaMaxHeapSize "4g" } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } } dependencies { - implementation 'androidx.multidex:multidex:2.0.0' + implementation "androidx.multidex:multidex:${ANDROIDX_MULTIDEX_VERSION}" implementation project(':app') } diff --git a/src/portal/consumer-rules.pro b/src/portal/consumer-rules.pro deleted file mode 100644 index e69de29..0000000 diff --git a/src/portal/proguard-rules.pro b/src/portal/proguard-rules.pro index 36c5949..83d578b 100644 --- a/src/portal/proguard-rules.pro +++ b/src/portal/proguard-rules.pro @@ -28,6 +28,7 @@ # hide the original source file name. -renamesourcefileattribute SourceFile +-printconfiguration "build/outputs/mapping/configuration.txt" #==================================【基本配置】================================== # 代码混淆压缩比,在0~7之间,默认为5,一般不下需要修改 -optimizationpasses 5 @@ -72,220 +73,4 @@ native ; # 对R文件下的所有类及其方法,都不能被混淆 -keepclasseswithmembernames class **.R$* { ; -} - -# 保留了继承自Activity、Application这些类的子类 --keep public class * extends android.app.Activity --keep public class * extends android.app.Application --keep public class * extends android.app.Service --keep public class * extends android.content.BroadcastReceiver --keep public class * extends android.content.ContentProvider --keep public class * extends android.preference.Preference --keep public class * extends android.view.View --keep public class * extends android.database.sqlite.SQLiteOpenHelper{*;} -# 如果有引用android-support-v4.jar包,可以添加下面这行 --keep public class com.null.test.ui.fragment.** {*;} -#如果引用了v4或者v7包 --dontwarn android.support.** - -# AndroidX 方法类 -#-keep class com.google.android.material.** {*;} -#-keep class androidx.** {*;} --keep public class * extends androidx.** --keep interface androidx.** {*;} --dontwarn com.google.android.material.** --dontnote com.google.android.material.** --dontwarn androidx.** -# 保留Activity中的方法参数是view的方法, --keepclassmembers class * extends android.app.Activity { -public void * (android.view.View); -} -# 枚举类不能被混淆 --keepclassmembers enum * { -public static **[] values(); -public static ** valueOf(java.lang.String); -} - --keep enum ** {*;} -# 保留自定义控件(继承自View)不能被混淆 --keep public class * extends android.view.View { -public (android.content.Context); -public (android.content.Context, android.util.AttributeSet); -public (android.content.Context, android.util.AttributeSet, int); -public void set*(***); -*** get* (); -} -# 保留Parcelable序列化的类不能被混淆 --keep class * implements android.os.Parcelable{ -public static final android.os.Parcelable$Creator *; -} -# 保留Serializable 序列化的类不被混淆 --keepclassmembers class * implements java.io.Serializable { -static final long serialVersionUID; -private static final java.io.ObjectStreamField[] serialPersistentFields; -!static !transient ; -private void writeObject(java.io.ObjectOutputStream); -private void readObject(java.io.ObjectInputStream); -java.lang.Object writeReplace(); -java.lang.Object readResolve(); -} -# 对于带有回调函数onXXEvent的,不能混淆 --keepclassmembers class ** { -void *(**On*Event); -} -#实体类 --keep class com.alipay.hulu.bean.** { *; } --keep class com.alipay.hulu.activity.CaseReplayResultActivity$ScreenshotBean {*;} - -#Patch相关类 --keep class com.alipay.hulu.upgrade.PatchResponse { *; } --keep class com.alipay.hulu.upgrade.PatchResponse$DataBean { *; } --keep class com.alipay.hulu.common.utils.ClassUtil$PatchVersionInfo { *; } --keep class com.alipay.hulu.common.utils.patch.PatchDescription {*;} - - -#内部方法 --keepattributes EnclosingMethod -#==================================【三方配置】================================== -#环信混淆-------------------------------------------- --keep class org.apache.** {*;} -#okhttp -# JSR 305 annotations are for embedding nullability information. --dontwarn javax.annotation.** - -# A resource is loaded with a relative path so the package of this class must be preserved. --keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase - -# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. --dontwarn org.codehaus.mojo.animal_sniffer.* - -# OkHttp platform used only on JVM and when Conscrypt dependency is available. --dontwarn okhttp3.internal.platform.ConscryptPlatform - -# injector --keepclassmembers class ** { -@com.alipay.hulu.common.injector.param.Subscriber ; -} --keepclassmembers class ** { -@com.alipay.hulu.common.injector.provider.Provider ; -} - -# ActionProvider --keep @com.alipay.hulu.common.annotation.Enable class * - -#PrepareWorker --keep interface com.alipay.hulu.shared.node.utils.prepare.PrepareWorker { *; } --keep @com.alipay.hulu.shared.node.utils.prepare.PrepareWorker$PrepareTool class * implements com.alipay.hulu.shared.node.utils.prepare.PrepareWorker { *; } - -# SchemeResolver --keep interface com.alipay.hulu.common.scheme.SchemeActionResolver { *; } --keep @com.alipay.hulu.common.scheme.SchemeResolver class * implements com.alipay.hulu.common.scheme.SchemeActionResolver { *; } - -#Github Replease --keep class com.alipay.hulu.bean.GithubReleaseBean { *; } --keep class com.alipay.hulu.bean.GithubReleaseBean$AuthorBean { *; } --keep class com.alipay.hulu.bean.GithubReleaseBean$AssetsBean { *; } --keep class com.alipay.hulu.bean.GithubReleaseBean$AssetsBean$UploaderBean { *; } - -# 三方库 --keep class com.cgutman.adblib.** {*;} --keep class com.mdit.library.** {*;} --keep class com.android.permission.** {*;} --keep class com.codebutler.android_websockets.** {*;} - -# greeendao --keep class com.alipay.hulu.shared.io.bean.** {*;} --keep class com.alipay.hulu.shared.io.db.** {*;} -### greenDAO 3 --keepclassmembers class * extends org.greenrobot.greendao.AbstractDao { -public static java.lang.String TABLENAME; -} --keep class **$Properties - -# If you do not use SQLCipher: --dontwarn org.greenrobot.greendao.database.** -# If you do not use RxJava: --dontwarn rx.** - - --keep class com.alipay.hulu.shared.node.tree.export.bean.** {*;} --keep class com.alipay.hulu.shared.node.action.OperationMethod {*;} --keep class com.alipay.hulu.shared.node.tree.OperationNode {*;} --keep class com.alipay.hulu.shared.node.tree.OperationNode$AssistantNode {*;} --keep class com.alipay.hulu.shared.node.tree.OperationNode$ParentNode {*;} --keep class com.alipay.hulu.shared.node.tree.AbstractNodeTree { *; } --keep class com.alipay.hulu.shared.node.tree.FakeNodeTree { *; } --keep class com.alipay.hulu.shared.node.tree.accessibility.tree.AccessibilityNodeTree { *; } --keep class * extends com.alipay.hulu.shared.node.tree.AbstractNodeTree { *; } - --keep class com.alipay.hulu.common.bean.** {*;} - --keep interface com.alipay.hulu.common.tools.AbstCmdLine {*;} - --keep class com.alipay.hulu.common.utils.patch.PatchContext {*;} - -# Glide --keep class com.alipay.hulu.common.utils.Glide* { *; } --keep public class * implements com.bumptech.glide.module.GlideModule --keep public class * extends com.bumptech.glide.module.AppGlideModule --keep public enum com.bumptech.glide.load.ImageHeaderParser$** { - **[] $VALUES; - public *; -} - --keep class ** implements com.alipay.hulu.shared.display.items.base.Displayable { -public void clear(); -} - --keep interface com.alipay.hulu.common.service.base.ExportService { *; } --keep @interface com.alipay.hulu.common.service.base.LocalService {*;} --keep class com.alipay.hulu.common.utils.patch.PatchClassLoader { -public com.alipay.hulu.common.utils.patch.PatchContext getContext(); -} --keep class ** implements com.alipay.hulu.common.service.base.ExportService { *; } - --keep interface ** extends com.alipay.hulu.common.service.base.ExportService { *; } - --dontwarn android.support.v4.** -#-keep class android.support.** {*;} --keepattributes Exceptions,InnerClasses,Signature - -#fastjson --dontwarn com.alibaba.fastjson.** --keep class com.alibaba.fastjson.** { *; } - -# 性能数据上报混淆 --keep class com.alipay.hulu.util.RecordUtil$RecordUploadData { *; } --keep class com.alipay.hulu.util.RecordUtil$UploadData { *; } - --keep class com.alipay.hulu.common.utils.DeviceInfoUtil {*;} - --keep class com.alipay.hulu.common.injector.aidl.InjectAidlInterface { *; } --keep class com.alipay.hulu.common.injector.aidl.InjectMessageListener { *; } --keep class com.alipay.hulu.common.injector.bean.InjectMessage { *; } --keep class com.alipay.hulu.shared.io.socket.LocalNetworkBroadcastService$SlaveInfo { *; } - -# fresco --dontwarn javax.annotation.** -#保留混淆mapping文件 --printmapping build/outputs/mapping/mapping.txt - --keepnames class * extends android.view.View --keep class * extends android.app.Fragment { -public void setUserVisibleHint(boolean); -public void onHiddenChanged(boolean); -public void onResume(); -public void onPause(); -} --keep class android.support.v4.app.Fragment { -public void setUserVisibleHint(boolean); -public void onHiddenChanged(boolean); -public void onResume(); -public void onPause(); -} --keep class * extends android.support.v4.app.Fragment { -public void setUserVisibleHint(boolean); -public void onHiddenChanged(boolean); -public void onResume(); -public void onPause(); -} +} \ No newline at end of file diff --git a/src/shared/build.gradle b/src/shared/build.gradle index 89d8840..1411dcd 100644 --- a/src/shared/build.gradle +++ b/src/shared/build.gradle @@ -21,6 +21,10 @@ android { buildToolsVersion rootProject.ext.buildToolsVersion publishNonDefault true + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } defaultConfig { minSdkVersion 18 @@ -33,7 +37,12 @@ android { } buildTypes { - + release { + consumerProguardFiles 'proguard-rules.pro' + } + debug { + consumerProguardFiles 'proguard-rules.pro' + } } @@ -52,13 +61,13 @@ greendao{ dependencies { compileOnly "com.google.zxing:core:3.4.0" - compileOnly 'com.alibaba:fastjson:1.2.73' - compileOnly 'androidx.appcompat:appcompat:1.0.0' - compileOnly 'androidx.legacy:legacy-support-v4:1.0.0' + compileOnly "com.alibaba:fastjson:${FASTJSON_VERSION}" + compileOnly "androidx.appcompat:appcompat:${ANDROIDX_APPCOMPAT_VERSION}" + compileOnly "androidx.legacy:legacy-support-v4:${ANDROIDX_SUPPORT_V4_VERSION}" compileOnly 'com.liulishuo.filedownloader:library:1.7.7' compileOnly 'com.squareup.okhttp3:okhttp:3.12.3' compileOnly 'org.greenrobot:greendao:3.3.0' - compileOnly group: 'commons-io', name: 'commons-io', version: '2.6' + compileOnly "commons-io:commons-io:${COMMON_IO_VERSION}" api project(':common') } diff --git a/src/shared/proguard-rules.pro b/src/shared/proguard-rules.pro index f1b4245..5d514f4 100644 --- a/src/shared/proguard-rules.pro +++ b/src/shared/proguard-rules.pro @@ -19,3 +19,38 @@ # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile + +# BroadcastPackage +-keep class com.alipay.hulu.shared.io.socket.LocalNetworkBroadcastService$BroadcastPackage { *; } +-keep enum com.alipay.hulu.shared.io.socket.enums.BroadcastCommandEnum { *; } + +#PrepareWorker +-keep interface com.alipay.hulu.shared.node.utils.prepare.PrepareWorker { *; } +-keep @com.alipay.hulu.shared.node.utils.prepare.PrepareWorker$PrepareTool class * implements com.alipay.hulu.shared.node.utils.prepare.PrepareWorker { *; } + +# greeendao +-keep class com.alipay.hulu.shared.io.bean.** {*;} +-keep class com.alipay.hulu.shared.io.db.** {*;} +### greenDAO 3 +-keepclassmembers class * extends org.greenrobot.greendao.AbstractDao { +public static java.lang.String TABLENAME; +} +-keep class **$Properties + +# If you do not use SQLCipher: +-dontwarn org.greenrobot.greendao.database.** +# If you do not use RxJava: +-dontwarn rx.** + +-keep class com.alipay.hulu.shared.node.tree.export.bean.** {*;} +-keep class com.alipay.hulu.shared.node.action.OperationMethod {*;} +-keep class com.alipay.hulu.shared.node.tree.OperationNode {*;} +-keep class com.alipay.hulu.shared.node.tree.OperationNode$AssistantNode {*;} +-keep class com.alipay.hulu.shared.node.tree.AbstractNodeTree { *; } +-keep class com.alipay.hulu.shared.node.tree.FakeNodeTree { *; } +-keep class com.alipay.hulu.shared.node.tree.accessibility.tree.AccessibilityNodeTree { *; } +-keep class * extends com.alipay.hulu.shared.node.tree.AbstractNodeTree { *; } + +-keep class ** implements com.alipay.hulu.shared.display.items.base.Displayable { +public void clear(); +} diff --git a/src/shared/src/main/AndroidManifest.xml b/src/shared/src/main/AndroidManifest.xml index 8330b94..7206cf4 100644 --- a/src/shared/src/main/AndroidManifest.xml +++ b/src/shared/src/main/AndroidManifest.xml @@ -18,6 +18,7 @@ package="com.alipay.hulu.shared"> + diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/display/DisplayProvider.java b/src/shared/src/main/java/com/alipay/hulu/shared/display/DisplayProvider.java index 51ddb5f..fd19467 100644 --- a/src/shared/src/main/java/com/alipay/hulu/shared/display/DisplayProvider.java +++ b/src/shared/src/main/java/com/alipay/hulu/shared/display/DisplayProvider.java @@ -24,7 +24,6 @@ import com.alipay.hulu.common.utils.ClassUtil; import com.alipay.hulu.common.utils.LogUtil; import com.alipay.hulu.common.utils.PermissionUtil; -import com.alipay.hulu.common.utils.StringUtil; import com.alipay.hulu.shared.display.items.base.DisplayItem; import com.alipay.hulu.shared.display.items.base.Displayable; import com.alipay.hulu.shared.display.items.base.RecordPattern; @@ -143,13 +142,13 @@ private Map loadDisplayItem() { if (annotation != null) { DisplayItemInfo info = new DisplayItemInfo(annotation, clazz); - DisplayItemInfo origin = infoMap.get(info.getName()); + DisplayItemInfo origin = infoMap.get(info.getKey()); if (origin == null) { - infoMap.put(info.getName(), info); + infoMap.put(info.getKey(), info); } else { // 如果level高于原有的level if (origin.level < info.level) { - infoMap.put(info.getName(), info); + infoMap.put(info.getKey(), info); } } } @@ -310,38 +309,22 @@ public void checkPermission(String name, Activity activity, PermissionUtil.OnPer * 通过工具类与参数反射生成显示工具并配置参数 * 工具类需事先 {@link Displayable} 接口,并对需要注入的依赖实现public的设置方法,并在相关方法使用{@link Subscriber}注解 * - * @param name 工具类名称 + * @param key 工具类名称 * @return 显示名称与显示工具 */ - public boolean startDisplay(String name) { - DisplayItemInfo displayItemInfo = allDisplayItems.get(name); - - // 实际启动 - return startDisplay(displayItemInfo); - } - - /** - * 通过工具类与参数反射生成显示工具并配置参数 - * 工具类需事先 {@link Displayable} 接口,并对需要注入的依赖实现public的设置方法,并在相关方法使用{@link Subscriber}注解 - * - * @param name 工具类名称 - * @return 显示名称与显示工具 - */ - public boolean startDisplayByKey(String key) { - DisplayItemInfo target = null; - for (DisplayItemInfo info: allDisplayItems.values()) { - if (StringUtil.equals(key, info.getKey())) { - target = info; - break; + public boolean startDisplay(String key) { + DisplayItemInfo displayItemInfo = allDisplayItems.get(key); + if (displayItemInfo == null) { + for (DisplayItemInfo info: allDisplayItems.values()) { + if (info.getName().equals(key)) { + displayItemInfo = info; + break; + } } } - if (target == null) { - LogUtil.w(TAG, "未能找到key[%s]关联的显示项", key); - } - // 实际启动 - return startDisplay(target); + return startDisplay(displayItemInfo); } private boolean startDisplay(DisplayItemInfo displayItemInfo) { diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/display/items/BatteryInfo.java b/src/shared/src/main/java/com/alipay/hulu/shared/display/items/BatteryInfo.java index 3c4e1bc..2840912 100644 --- a/src/shared/src/main/java/com/alipay/hulu/shared/display/items/BatteryInfo.java +++ b/src/shared/src/main/java/com/alipay/hulu/shared/display/items/BatteryInfo.java @@ -15,15 +15,12 @@ */ package com.alipay.hulu.shared.display.items; -import android.app.ActivityManager; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.os.BatteryManager; import android.os.Build; -import android.os.IBinder; -import android.os.MemoryFile; -import android.os.Parcel; -import android.os.ParcelFileDescriptor; -import android.os.Parcelable; import com.alipay.hulu.common.application.LauncherApplication; import com.alipay.hulu.common.tools.CmdTools; @@ -36,13 +33,8 @@ import com.alipay.hulu.shared.display.items.util.FinalR; import java.io.File; -import java.io.FileDescriptor; import java.io.FileFilter; import java.io.FileInputStream; -import java.io.IOException; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -55,9 +47,14 @@ public class BatteryInfo implements Displayable{ private final static String TAG = "BatteryInfo"; + private BatteryVoltageListener mListener; + + private Object m_Instance; private static Long startTime = 0L; + private static volatile int mVoltage = 0; + private static final String[] acceptFiles = {"batt_current", "current_now", "batt_current_adc", "current_avg", "BatteryAverageCurrent"}; @@ -65,62 +62,116 @@ public class BatteryInfo implements Displayable{ private static List avgBattery; + private static List currentW; + + private static List avgW; + /** 能否正常从系统调用获取数据 */ private static boolean implementInterface = true; + private int mStatsType = STATS_CURRENT; + + public static final int STATS_CURRENT = 2; + public static boolean needProcess = false; + private static float lastCurrent = 0L; + private static float lastWCurrent = 0L; private static float point = 0; private static long loop = 0; + private static float wPoint = 0; + private static long wLoop = 0; @Override public String getCurrentInfo() { - float current = getCurrent(LauncherApplication.getContext()); - if (current != -1) { - return String.format("瞬时电流:%.1fmA/均值:%.1fmA", current, getAvg()); + float[] current = getCurrent(LauncherApplication.getContext()); + if (current[0] != -1) { + return StringUtil.getString(R.string.display_battery__current_info, + current[0], current[1]); } else { - return "数据获取失败"; + return StringUtil.getString(R.string.display_battery__load_fail); + } + } + + private static class BatteryVoltageListener extends BroadcastReceiver { + private BatteryVoltageListener() { + } + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (StringUtil.equals(action, Intent.ACTION_BATTERY_CHANGED)) { + int voltage = intent.getIntExtra(BatteryManager.EXTRA_VOLTAGE, -1); + LogUtil.i(TAG, "Get battery voltage, %d", voltage); + mVoltage = voltage; + } } } @Override public void start() { + if (mListener != null) { + LauncherApplication.getInstance().unregisterReceiver(mListener); + mListener = null; + } + mListener = new BatteryVoltageListener(); + IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); + LauncherApplication.getInstance().registerReceiver(mListener, filter); } @Override public void stop() { - + if (mListener != null) { + LauncherApplication.getInstance().unregisterReceiver(mListener); + mListener = null; + } } @Override public void startRecord() { startTime = System.currentTimeMillis(); + trigger(); avgBattery = new ArrayList<>(); currentBattery = new ArrayList<>(); + avgW = new ArrayList<>(); + currentW = new ArrayList<>(); } @Override public void record() { - currentBattery.add(new RecordPattern.RecordItem(System.currentTimeMillis(), getCurrent(LauncherApplication.getContext()), "")); - avgBattery.add(new RecordPattern.RecordItem(System.currentTimeMillis(), getAvg(), "")); + float[] result = getCurrent(LauncherApplication.getContext()); + float[] avg = getAvg(); + currentBattery.add(new RecordPattern.RecordItem(System.currentTimeMillis(), result[0], "")); + currentW.add(new RecordPattern.RecordItem(System.currentTimeMillis(), result[1], "")); + avgBattery.add(new RecordPattern.RecordItem(System.currentTimeMillis(), avg[0], "")); + avgW.add(new RecordPattern.RecordItem(System.currentTimeMillis(), avg[1], "")); } @Override public Map> stopRecord() { Long endTime = System.currentTimeMillis(); Map> result = new HashMap<>(); - RecordPattern pattern = new RecordPattern("实时电流", "mA", "Battery"); + RecordPattern pattern = new RecordPattern(StringUtil.getString(R.string.display_battery__real_time_current), "mA", "Battery"); pattern.setEndTime(endTime); pattern.setStartTime(startTime); result.put(pattern, currentBattery); - pattern = new RecordPattern("平均电流", "mA", "Battery"); + pattern = new RecordPattern(StringUtil.getString(R.string.display_battery__avg_current), "mA", "Battery"); pattern.setEndTime(endTime); pattern.setStartTime(startTime); result.put(pattern, avgBattery); + pattern = new RecordPattern(StringUtil.getString(R.string.display_battery__real_time_power), "mW", "Battery"); + pattern.setEndTime(endTime); + pattern.setStartTime(startTime); + result.put(pattern, currentW); + pattern = new RecordPattern(StringUtil.getString(R.string.display_battery__avg_power), "mW", "Battery"); + pattern.setEndTime(endTime); + pattern.setStartTime(startTime); + result.put(pattern, avgW); avgBattery = null; currentBattery = null; + currentW = null; + avgW = null; return result; } @@ -134,8 +185,8 @@ public long getRefreshFrequency() { return 250; } - public static float getAvg() { - return loop == 0 ? 0 : (point / loop); + public static float[] getAvg() { + return new float[] {loop == 0 ? 0 : (point / loop), wLoop == 0? 0: (wPoint / wLoop)}; } public static void clearData() @@ -155,18 +206,18 @@ private static boolean containsBattery(String filename) { return false; } - public static float getCurrent(Context context) { + public static float[] getCurrent(Context context) { float current = 0; // Android 5.0及以上接口正常可以直接通过系统调用获取当前电流值 if (!implementInterface && Build.VERSION.SDK_INT >= 26) { - return -1; + return new float[] {-1, -1}; } if (implementInterface && android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && context != null) { BatteryManager mBatteryManager = (BatteryManager) context.getSystemService(Context.BATTERY_SERVICE); if (mBatteryManager == null) { LogUtil.e(TAG, "Can't get batteryManager"); implementInterface = false; - return -1; + return new float[] {-1, -1}; } current = mBatteryManager.getLongProperty(BatteryManager.BATTERY_PROPERTY_CURRENT_NOW); @@ -178,7 +229,7 @@ public static float getCurrent(Context context) { if (current > 1000000000 || current < -1000000000) { LogUtil.e(TAG, "can't parse current for: %f", current); implementInterface = false; - return 0; + return new float[] {0, 0}; } // 保证用电时为正,充电时为负 @@ -225,7 +276,7 @@ public boolean accept(File pathname) { } } StringBuilder sb = CmdTools.execCmd("cat " + batteryPath); - LogUtil.d(TAG, "[CurrentInfo] BatteryCurrent: " + sb.toString()); + LogUtil.i(TAG, "[BatteryTools] BatteryCurrent: " + sb.toString()); current = Math.abs(Integer.parseInt(sb.toString().trim())); if (current > 100000 || current < -100000) { needProcess = true; @@ -245,12 +296,43 @@ public boolean accept(File pathname) { point += current; loop++; } - return current; + float w = current * mVoltage / 1000; + if (w != lastWCurrent) { + lastWCurrent = w; + wPoint += w; + wLoop++; + } + //data.add((int)current); + return new float[] {current, w}; + } + + private static byte[] readFully(FileInputStream stream, int avail) throws java.io.IOException { + int pos = 0; + byte[] data = new byte[avail]; + while (true) { + int amt = stream.read(data, pos, data.length-pos); + LogUtil.i(TAG, "Read " + amt + " bytes at " + pos + + " of avail " + data.length); + if (amt <= 0) { + LogUtil.i(TAG, "**** FINISHED READING: pos=" + pos + + " len=" + data.length); + return data; + } + pos += amt; + avail = stream.available(); + if (avail > data.length-pos) { + byte[] newData = new byte[pos+avail]; + System.arraycopy(data, 0, newData, 0, pos); + data = newData; + } + } } @Override public void trigger() { point = 0f; loop = 0; + wPoint = 0f; + wLoop = 0; } } diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/display/items/CPUTools.java b/src/shared/src/main/java/com/alipay/hulu/shared/display/items/CPUTools.java index 90a3399..f99c572 100644 --- a/src/shared/src/main/java/com/alipay/hulu/shared/display/items/CPUTools.java +++ b/src/shared/src/main/java/com/alipay/hulu/shared/display/items/CPUTools.java @@ -276,7 +276,7 @@ public Map> stopRecord() { if (appCurrents != null && appCurrents.size() > 0) { for (String processName : appCurrents.keySet()) { List appCurrent = appCurrents.get(processName); - pattern = new RecordPattern("应用进程-" + processName, "%", "CPU"); + pattern = new RecordPattern(StringUtil.getString(R.string.display_cpu__app_process) + processName, "%", "CPU"); pattern.setStartTime(startTime); pattern.setEndTime(endTime); result.put(pattern, appCurrent); @@ -285,7 +285,7 @@ public Map> stopRecord() { cachedData.clear(); } - pattern = new RecordPattern("全局占用", "%", "CPU"); + pattern = new RecordPattern(StringUtil.getString(R.string.display_cpu__global_usage), "%", "CPU"); pattern.setStartTime(startTime); pattern.setEndTime(endTime); result.put(pattern, totalCurrent); diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/display/items/FpsTools.java b/src/shared/src/main/java/com/alipay/hulu/shared/display/items/FpsTools.java index dfc9847..01c1d5f 100644 --- a/src/shared/src/main/java/com/alipay/hulu/shared/display/items/FpsTools.java +++ b/src/shared/src/main/java/com/alipay/hulu/shared/display/items/FpsTools.java @@ -20,6 +20,7 @@ import com.alipay.hulu.common.injector.param.Subscriber; import com.alipay.hulu.common.injector.provider.Param; import com.alipay.hulu.common.utils.StringUtil; +import com.alipay.hulu.shared.R; import com.alipay.hulu.shared.display.items.base.DisplayItem; import com.alipay.hulu.shared.display.items.base.Displayable; import com.alipay.hulu.shared.display.items.base.RecordPattern; @@ -120,19 +121,19 @@ public void record() { public Map> stopRecord() { Map> result = new HashMap<>(); Long endTime = System.currentTimeMillis(); - RecordPattern pattern = new RecordPattern("帧率", "帧", "FPS"); + RecordPattern pattern = new RecordPattern(StringUtil.getString(R.string.display_fps__framerate), StringUtil.getString(R.string.display_fps__frame), "FPS"); pattern.setStartTime(startTime); pattern.setEndTime(endTime); result.put(pattern, fpsCurrent); - pattern = new RecordPattern("延迟次数", "次", "FPS"); + pattern = new RecordPattern(StringUtil.getString(R.string.display_fps__jank_time), StringUtil.getString(R.string.display_fps__count), "FPS"); pattern.setStartTime(startTime); pattern.setEndTime(endTime); result.put(pattern, jankCurrent); - pattern = new RecordPattern("最长延迟时间", "ms", "FPS"); + pattern = new RecordPattern(StringUtil.getString(R.string.display_fps__max_jank_time), "ms", "FPS"); pattern.setStartTime(startTime); pattern.setEndTime(endTime); result.put(pattern, maxJankCurrent); - pattern = new RecordPattern("延迟占比", "%", "FPS"); + pattern = new RecordPattern(StringUtil.getString(R.string.display_fps__jank_percentage), "%", "FPS"); pattern.setStartTime(startTime); pattern.setEndTime(endTime); result.put(pattern, jankPercentCurrent); @@ -154,11 +155,11 @@ public String getCurrentInfo() { if (dataWrapper.fps > 0) { if (displayExtra && !StringUtil.isEmpty(dataWrapper.activity)) { - return String.format("帧率:%d/延迟数:%d/最长延迟:%dms/延迟占比:%.2f%%\n%s", + return StringUtil.getString(R.string.display_fps__current_info_activity, dataWrapper.fps, dataWrapper.junkCount, dataWrapper.maxJunk, dataWrapper.junkPercent, dataWrapper.activity.substring(dataWrapper.activity.indexOf('/') + 1)); } - return String.format("帧率:%d/延迟数:%d/最长延迟:%dms/延迟占比:%.2f%%", dataWrapper.fps, + return StringUtil.getString(R.string.display_fps__current_info, dataWrapper.fps, dataWrapper.junkCount, dataWrapper.maxJunk, dataWrapper.junkPercent); } return "-"; diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/display/items/MemoryTools.java b/src/shared/src/main/java/com/alipay/hulu/shared/display/items/MemoryTools.java index 9d38ec3..e611579 100644 --- a/src/shared/src/main/java/com/alipay/hulu/shared/display/items/MemoryTools.java +++ b/src/shared/src/main/java/com/alipay/hulu/shared/display/items/MemoryTools.java @@ -30,6 +30,7 @@ import com.alipay.hulu.common.tools.CmdTools; import com.alipay.hulu.common.utils.LogUtil; import com.alipay.hulu.common.utils.StringUtil; +import com.alipay.hulu.shared.R; import com.alipay.hulu.shared.display.items.base.DisplayItem; import com.alipay.hulu.shared.display.items.base.Displayable; import com.alipay.hulu.shared.display.items.base.FixedLengthCircularArray; @@ -278,7 +279,7 @@ private int[] getPssAndPDList(List processes) { public Map> stopRecord() { Long endTime = System.currentTimeMillis(); Map> result = new HashMap<>(); - RecordPattern pattern = new RecordPattern("全局占用", "MB", "Memory"); + RecordPattern pattern = new RecordPattern(StringUtil.getString(R.string.display_memory__total_usage), "MB", "Memory"); pattern.setStartTime(startTime); pattern.setEndTime(endTime); result.put(pattern, usedMemory); @@ -287,11 +288,11 @@ public Map> stopRecord() { for (String pid : appMemory.keySet()) { ArrayList[] pidRecord = appMemory.get(pid); - pattern = new RecordPattern("PSS内存-" + pid, "MB", "Memory"); + pattern = new RecordPattern("PSS-" + pid, "MB", "Memory"); pattern.setStartTime(startTime); pattern.setEndTime(endTime); result.put(pattern, pidRecord[0]); - pattern = new RecordPattern("PrivateDirty内存-" + pid, "MB", "Memory"); + pattern = new RecordPattern("PrivateDirty-" + pid, "MB", "Memory"); pattern.setStartTime(startTime); pattern.setEndTime(endTime); result.put(pattern, pidRecord[1]); @@ -314,7 +315,7 @@ public String getCurrentInfo() { if (totalMemory == null) { totalMemory = getTotalMemory(); } - return "可用内存:" + getAvailMemory(context) + "MB/总内存:" + totalMemory + "MB"; + return StringUtil.getString(R.string.display_memory__current_info, getAvailMemory(context), totalMemory); } @Override diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/display/items/NetworkTools.java b/src/shared/src/main/java/com/alipay/hulu/shared/display/items/NetworkTools.java index 7f2cd26..7728ac2 100644 --- a/src/shared/src/main/java/com/alipay/hulu/shared/display/items/NetworkTools.java +++ b/src/shared/src/main/java/com/alipay/hulu/shared/display/items/NetworkTools.java @@ -33,7 +33,6 @@ import com.alipay.hulu.shared.display.items.util.FinalR; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -96,7 +95,7 @@ public String getCurrentInfo() { triggerReload = false; } float[] value = getProcessData(new int[] {currentProcess.getPid()}); - return String.format("%s:下%.1fK/累计%.1fK\n%s:上%.1fK/累计%.1fK", currentProcess.getProcessName(), value[0], value[1], currentProcess.getProcessName(), value[2], value[3]); + return StringUtil.getString(R.string.display_network__current_info, currentProcess.getProcessName(), value[0], value[1], currentProcess.getProcessName(), value[2], value[3]); } if (triggerReload) { @@ -105,7 +104,7 @@ public String getCurrentInfo() { triggerReload = false; } - return String.format("total:下%.1fK/累计%.1fK\ntotal:上%.1fK/累计%.1fK", getRxTotal(), getRxAll(), getTxTotal(), getTxAll()); + return StringUtil.getString(R.string.display_network__app_current_info, getRxTotal(), getRxAll(), getTxTotal(), getTxAll()); } @Override @@ -193,17 +192,17 @@ public void record() { public Map> stopRecord() { Long endTime = System.currentTimeMillis(); Map> result = new HashMap<>(); - RecordPattern pattern = new RecordPattern("累计全局下行流量", "KB", "Network"); + RecordPattern pattern = new RecordPattern(StringUtil.getString(R.string.display_network__total_download_flow), "KB", "Network"); pattern.setEndTime(endTime); pattern.setStartTime(startTime); result.put(pattern, downloadRecordAll); - pattern = new RecordPattern("全局下行速率", "KB", "Network"); + pattern = new RecordPattern(StringUtil.getString(R.string.display_network__global_download_speed), "KB", "Network"); pattern.setEndTime(endTime); pattern.setStartTime(startTime); result.put(pattern, downloadRecord); if (downloadSizeProcessRecords != null && downloadSizeProcessRecords.size() > 0) { for (String name: downloadSizeProcessRecords.keySet()) { - pattern = new RecordPattern("进程下行流量-" + name, "KB", "Network"); + pattern = new RecordPattern(StringUtil.getString(R.string.display_network__process_download_flow) + name, "KB", "Network"); pattern.setEndTime(endTime); pattern.setStartTime(startTime); result.put(pattern, downloadSizeProcessRecords.get(name)); @@ -212,24 +211,24 @@ public Map> stopRecord() { if (downloadSpeedProcessRecords != null && downloadSpeedProcessRecords.size() > 0) { for (String name: downloadSpeedProcessRecords.keySet()) { - pattern = new RecordPattern("进程下行速率-" + name, "KB/S", "Network"); + pattern = new RecordPattern(StringUtil.getString(R.string.display_network__process_download_speed) + name, "KB/S", "Network"); pattern.setEndTime(endTime); pattern.setStartTime(startTime); result.put(pattern, downloadSpeedProcessRecords.get(name)); } } - pattern = new RecordPattern("累计全局上行流量", "KB", "Network"); + pattern = new RecordPattern(StringUtil.getString(R.string.display_network__global_upload_flow), "KB", "Network"); pattern.setEndTime(endTime); pattern.setStartTime(startTime); result.put(pattern, uploadRecordAll); - pattern = new RecordPattern("全局上行速率", "KB", "Network"); + pattern = new RecordPattern(StringUtil.getString(R.string.display_network__global_upload_speed), "KB", "Network"); pattern.setEndTime(endTime); pattern.setStartTime(startTime); result.put(pattern, uploadRecord); if (uploadSizeProcessRecords != null && uploadSizeProcessRecords.size() > 0) { for (String name: uploadSizeProcessRecords.keySet()) { - pattern = new RecordPattern("进程上行流量-" + name, "KB", "Network"); + pattern = new RecordPattern(StringUtil.getString(R.string.display_network__process_upload_flow) + name, "KB", "Network"); pattern.setEndTime(endTime); pattern.setStartTime(startTime); result.put(pattern, uploadSizeProcessRecords.get(name)); @@ -237,7 +236,7 @@ public Map> stopRecord() { } if (uploadSpeedProcessRecords != null && uploadSpeedProcessRecords.size() > 0) { for (String name: uploadSpeedProcessRecords.keySet()) { - pattern = new RecordPattern("进程上行速率-" + name, "KB/S", "Network"); + pattern = new RecordPattern(StringUtil.getString(R.string.display_network__process_upload_speed) + name, "KB/S", "Network"); pattern.setEndTime(endTime); pattern.setStartTime(startTime); result.put(pattern, uploadSpeedProcessRecords.get(name)); diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/display/items/ResponseTools.java b/src/shared/src/main/java/com/alipay/hulu/shared/display/items/ResponseTools.java index bc6dd38..fd9a77c 100644 --- a/src/shared/src/main/java/com/alipay/hulu/shared/display/items/ResponseTools.java +++ b/src/shared/src/main/java/com/alipay/hulu/shared/display/items/ResponseTools.java @@ -104,11 +104,11 @@ public void stop() { public Map> stopRecord() { Long endTime = System.currentTimeMillis(); Map> result = new HashMap<>(); - RecordPattern pattern = new RecordPattern("响应耗时", "ms", "Response"); + RecordPattern pattern = new RecordPattern(StringUtil.getString(R.string.display_response__response_time), "ms", "Response"); pattern.setEndTime(endTime); pattern.setStartTime(startTime); result.put(pattern, responseList); - pattern = new RecordPattern("刷新耗时", "ms", "Response"); + pattern = new RecordPattern(StringUtil.getString(R.string.display_response__refresh_time), "ms", "Response"); pattern.setEndTime(endTime); pattern.setStartTime(startTime); result.put(pattern, refreshList); @@ -129,11 +129,11 @@ public void receiveAccessibilityEvent(UniversalEventBean eventBean) { } if (!StringUtil.equals((String) eventBean.getParam(Constant.KEY_ACCESSIBILITY_SOURCE), app)) { - LogUtil.d(TAG, "收到其他来源:%s", eventBean.getParam(Constant.KEY_ACCESSIBILITY_SOURCE)); + LogUtil.d(TAG, "收到其他来源:%s", (Object) eventBean.getParam(Constant.KEY_ACCESSIBILITY_SOURCE)); return; } - LogUtil.d(TAG, "收到辅助功能事件, %s", eventBean); + LogUtil.v(TAG, "收到辅助功能事件, %s", eventBean); switch ((Integer) eventBean.getParam(Constant.KEY_ACCESSIBILITY_TYPE)) { case AccessibilityEvent.TYPE_VIEW_CLICKED: @@ -202,8 +202,9 @@ private void processContentChange(long changeTime) { @Override public String getCurrentInfo() { - return "响应耗时: " + (eventResponse.getResponsDate() - eventResponse.getClickDate()) + "ms/刷新耗时: " - + (eventResponse.getRefreshDate() - eventResponse.getClickDate()) + "ms"; + return StringUtil.getString(R.string.display_response__current_info, + eventResponse.getResponsDate() - eventResponse.getClickDate(), + eventResponse.getRefreshDate() - eventResponse.getClickDate()); } @Override diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/display/items/StatusTools.java b/src/shared/src/main/java/com/alipay/hulu/shared/display/items/StatusTools.java index f3b7531..c1cc1b9 100644 --- a/src/shared/src/main/java/com/alipay/hulu/shared/display/items/StatusTools.java +++ b/src/shared/src/main/java/com/alipay/hulu/shared/display/items/StatusTools.java @@ -23,6 +23,8 @@ import com.alipay.hulu.common.injector.provider.Param; import com.alipay.hulu.common.tools.CmdTools; import com.alipay.hulu.common.utils.LogUtil; +import com.alipay.hulu.common.utils.StringUtil; +import com.alipay.hulu.shared.R; import com.alipay.hulu.shared.display.items.base.DisplayItem; import com.alipay.hulu.shared.display.items.base.Displayable; import com.alipay.hulu.shared.display.items.base.FixedLengthCircularArray; @@ -297,7 +299,7 @@ public Map> stopRecord() { if (appVmSize != null && appVmSize.size() > 0) { for (String processName : appVmSize.keySet()) { List appCurrent = appVmSize.get(processName); - pattern = new RecordPattern("应用进程-" + processName, "MB", "VmSize"); + pattern = new RecordPattern(StringUtil.getString(R.string.display_status__app_process) + processName, "MB", "VmSize"); pattern.setStartTime(startTime); pattern.setEndTime(endTime); result.put(pattern, appCurrent); @@ -309,7 +311,7 @@ public Map> stopRecord() { if (appVmRSS != null && appVmRSS.size() > 0) { for (String processName : appVmRSS.keySet()) { List appCurrent = appVmRSS.get(processName); - pattern = new RecordPattern("应用进程-" + processName, "MB", "VmRSS"); + pattern = new RecordPattern(StringUtil.getString(R.string.display_status__app_process) + processName, "MB", "VmRSS"); pattern.setStartTime(startTime); pattern.setEndTime(endTime); result.put(pattern, appCurrent); @@ -321,7 +323,7 @@ public Map> stopRecord() { if (appThreadCount != null && appThreadCount.size() > 0) { for (String processName : appThreadCount.keySet()) { List appCurrent = appThreadCount.get(processName); - pattern = new RecordPattern("应用进程-" + processName, "个", "ThreadCount"); + pattern = new RecordPattern(StringUtil.getString(R.string.display_status__app_process) + processName, "个", "ThreadCount"); pattern.setStartTime(startTime); pattern.setEndTime(endTime); result.put(pattern, appCurrent); diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/display/items/TemperatureTools.java b/src/shared/src/main/java/com/alipay/hulu/shared/display/items/TemperatureTools.java new file mode 100644 index 0000000..41666cb --- /dev/null +++ b/src/shared/src/main/java/com/alipay/hulu/shared/display/items/TemperatureTools.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.shared.display.items; + +import com.alipay.hulu.common.tools.CmdTools; +import com.alipay.hulu.common.utils.LogUtil; +import com.alipay.hulu.common.utils.StringUtil; +import com.alipay.hulu.shared.R; +import com.alipay.hulu.shared.display.items.base.DisplayItem; +import com.alipay.hulu.shared.display.items.base.Displayable; +import com.alipay.hulu.shared.display.items.base.RecordPattern; +import com.alipay.hulu.shared.display.items.util.FinalR; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +@DisplayItem(key = "Temperature", nameRes = FinalR.TEMPERATURE, permissions = "adb") +public class TemperatureTools implements Displayable { + private static final String TAG = TemperatureTools.class.getSimpleName(); + private static final String[] TEMPERATURE_FILE_LIST = new String[] { + "/sys/devices/system/cpu/cpu0/cpufreq/cpu_temp", + "/sys/devices/system/cpu/cpu0/cpufreq/FakeShmoo_cpu_temp", + "/sys/class/thermal/thermal_zone0/temp", + "/sys/class/thermal/thermal_zone1/temp", + "/sys/class/i2c-adapter/i2c-4/4-004c/temperature", + "/sys/devices/platform/tegra-i2c.3/i2c-4/4-004c/temperature", + "/sys/devices/platform/omap/omap_temp_sensor.0/temperature", + "/sys/devices/platform/tegra_tmon/temp1_input", + "/sys/kernel/debug/tegra_thermal/temp_tj", + "/sys/devices/platform/s5p-tmu/temperature", + "/sys/devices/virtual/thermal/thermal_zone0/temp", + "/sys/class/hwmon/hwmon0/device/temp1_input", + "/sys/devices/virtual/thermal/thermal_zone1/temp", + "/sys/devices/platform/s5p-tmu/curr_temp" + }; + private static String TARGET_FILE_DIR = null; + + private long startRecordTime = -1L; + private List cpuRecord; + + private static String getTargetFileIdx() { + String pathFromThermal = getPathFromThermal(); + if (StringUtil.isNotEmpty(pathFromThermal)) { + return pathFromThermal; + } + for (int i = 0; i < TEMPERATURE_FILE_LIST.length; i++) { + String file = TEMPERATURE_FILE_LIST[i]; + String content = CmdTools.execHighPrivilegeCmd("cat " + file); + if (StringUtil.isInteger(content.trim()) && Integer.parseInt(content.trim()) > 0) { + return TEMPERATURE_FILE_LIST[i]; + } + } + return ""; + } + + /** + * 从Thermal文件夹中读取类型 + * @return + */ + private static String getPathFromThermal() { + String result = CmdTools.execHighPrivilegeCmd("for f in /sys/class/thermal/thermal_zone*/type\ndo\necho \"$f:$(cat $f)\"\ndone"); + LogUtil.i(TAG, "Read temperature types:" + result); + if (StringUtil.contains(result, "/sys/class/thermal/thermal_zone0/type")) { + String[] lines = StringUtil.split(result, "\n"); + for (String line: lines) { + if (line.contains(":cpu-0-0")) { + String prefix = line.split(":")[0]; + if (prefix.endsWith("/type")) { + return prefix.substring(0, prefix.length() - 5) + "/temp"; + } + } + } + } + + return null; + } + private static float readCurrentCpuTemperature() { + if ("".equals(TARGET_FILE_DIR)) { + return -1; + } + if (TARGET_FILE_DIR == null) { + TARGET_FILE_DIR = getTargetFileIdx(); + } + if ("".equals(TARGET_FILE_DIR)) { + return -1; + } + + String content = CmdTools.execHighPrivilegeCmd("cat " + TARGET_FILE_DIR); + return Integer.parseInt(content.trim()) / 1000F; + } + + @Override + public void start() { + + } + + @Override + public void stop() { + + } + + @Override + public String getCurrentInfo() throws Exception { + float temperature = readCurrentCpuTemperature(); + return StringUtil.getString(R.string.display_temperature__cpu, temperature); + } + + @Override + public long getRefreshFrequency() { + return 500; + } + + @Override + public void clear() { + } + + @Override + public void startRecord() { + cpuRecord = new ArrayList<>(); + startRecordTime = System.currentTimeMillis(); + } + + @Override + public void record() { + long time = System.currentTimeMillis(); + float temperature = readCurrentCpuTemperature(); + cpuRecord.add(new RecordPattern.RecordItem(time, temperature, null)); + } + + @Override + public void trigger() { + + } + + @Override + public Map> stopRecord() { + RecordPattern pattern = new RecordPattern("CPU温度", "度", "Temperature"); + pattern.setStartTime(startRecordTime); + pattern.setEndTime(System.currentTimeMillis()); + startRecordTime = -1L; + Map> records = Collections.singletonMap(pattern, cpuRecord); + cpuRecord = null; + return records; + } +} diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/display/items/base/DisplayItem.java b/src/shared/src/main/java/com/alipay/hulu/shared/display/items/base/DisplayItem.java index 08f06a4..b2d4537 100644 --- a/src/shared/src/main/java/com/alipay/hulu/shared/display/items/base/DisplayItem.java +++ b/src/shared/src/main/java/com/alipay/hulu/shared/display/items/base/DisplayItem.java @@ -15,8 +15,6 @@ */ package com.alipay.hulu.shared.display.items.base; -import androidx.annotation.StringRes; - import com.alipay.hulu.shared.display.items.util.FinalR; import java.lang.annotation.ElementType; @@ -41,7 +39,7 @@ * 显示名称 * @return */ - String key(); + String key() default ""; // @StringRes FinalR nameRes() default FinalR.NULL; diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/display/items/util/FinalR.java b/src/shared/src/main/java/com/alipay/hulu/shared/display/items/util/FinalR.java index f011de4..2e16380 100644 --- a/src/shared/src/main/java/com/alipay/hulu/shared/display/items/util/FinalR.java +++ b/src/shared/src/main/java/com/alipay/hulu/shared/display/items/util/FinalR.java @@ -30,6 +30,7 @@ public enum FinalR { MEMORY(R.string.performance__memory), NETWORK(R.string.performance__network), PROCESS_STATUS(R.string.performance__process_status), + TEMPERATURE(R.string.performance__temperature), NULL(-1) ; diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/event/EventService.java b/src/shared/src/main/java/com/alipay/hulu/shared/event/EventService.java index 97f3e3e..904f4bf 100644 --- a/src/shared/src/main/java/com/alipay/hulu/shared/event/EventService.java +++ b/src/shared/src/main/java/com/alipay/hulu/shared/event/EventService.java @@ -18,9 +18,9 @@ import android.content.Context; import android.content.Intent; import android.provider.Settings; -import android.widget.Toast; import com.alipay.hulu.common.application.LauncherApplication; +import com.alipay.hulu.common.service.base.AppGuardian; import com.alipay.hulu.common.service.base.ExportService; import com.alipay.hulu.common.service.base.LocalService; import com.alipay.hulu.common.utils.PermissionUtil; @@ -33,7 +33,8 @@ * Created by qiaoruikai on 2018/10/9 11:08 PM. */ @LocalService -public class EventService implements ExportService { +@AppGuardian.AppGuardianEnable +public class EventService implements ExportService, AppGuardian { private TouchEventTracker touchTracker; private AccessibilityEventTracker accessibilityTracker; private EventProxy proxy; @@ -95,6 +96,16 @@ public void stopTrackAccessibilityEvent() { } } + @Override + public void onEventTrigger(ReceiveSystemEvent event) { + if (event == ReceiveSystemEvent.SCREEN_UNLOCK) { + if (touchTracker != null && !touchTracker.isTouchTrackRunning()) { + touchTracker.registerTouchListener(proxy); + touchTracker.startTrackTouch(); + } + } + } + @Override public void onCreate(Context context) { this.contextRef = new WeakReference<>(context); diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/event/accessibility/AccessibilityServiceImpl.java b/src/shared/src/main/java/com/alipay/hulu/shared/event/accessibility/AccessibilityServiceImpl.java index efce537..2f14166 100644 --- a/src/shared/src/main/java/com/alipay/hulu/shared/event/accessibility/AccessibilityServiceImpl.java +++ b/src/shared/src/main/java/com/alipay/hulu/shared/event/accessibility/AccessibilityServiceImpl.java @@ -18,6 +18,7 @@ import android.accessibilityservice.AccessibilityService; import android.accessibilityservice.AccessibilityServiceInfo; import android.content.Intent; +import android.os.Build; import android.view.accessibility.AccessibilityEvent; import com.alipay.hulu.common.application.LauncherApplication; @@ -27,13 +28,15 @@ import com.alipay.hulu.common.injector.param.Subscriber; import com.alipay.hulu.common.injector.provider.Param; import com.alipay.hulu.common.injector.provider.Provider; +import com.alipay.hulu.common.tools.BackgroundExecutor; import com.alipay.hulu.common.utils.LogUtil; -import com.alipay.hulu.common.utils.MiscUtil; import com.alipay.hulu.common.utils.StringUtil; import com.alipay.hulu.shared.event.constant.Constant; import java.lang.ref.WeakReference; +import androidx.annotation.NonNull; + /** * Created by qiaoruikai on 2018/10/9 4:35 PM. */ @@ -64,7 +67,8 @@ public void onStart(Intent intent, int startId) { @Override public boolean onUnbind(Intent intent) { - LogUtil.d(TAG, "Service on unbind"); + LogUtil.i(TAG, "Service on unbind"); + LauncherApplication.getInstance().prepareInMain(); InjectorService service = LauncherApplication.getInstance() .findServiceByName(InjectorService.class.getName()); @@ -75,7 +79,8 @@ public boolean onUnbind(Intent intent) { if (this.accessibilityEventTrackerRef != null) { this.accessibilityEventTrackerRef.clear(); - }return super.onUnbind(intent); + } + return super.onUnbind(intent); } @Override @@ -96,19 +101,17 @@ protected void onServiceConnected() { super.onServiceConnected(); // 如果初始化完成但没有注册过 - if (!LauncherApplication.getInstance().getAccessibilityState() && LauncherApplication.getInstance().hasFinishInit()) { - // 注册自身到注入服务 - InjectorService service = LauncherApplication.getInstance() - .findServiceByName(InjectorService.class.getName()); - if (service != null) { - service.register(this); - } - // 防止之前block - setServiceToNormalMode(); - - // 设置为已注册 - LauncherApplication.getInstance().setAccessibilityState(true); + if (Build.VERSION.SDK_INT >= 24) { + SoftKeyboardController controller = getSoftKeyboardController(); + controller.addOnShowModeChangedListener(new SoftKeyboardController.OnShowModeChangedListener() { + @Override + public void onShowModeChanged(@NonNull SoftKeyboardController controller, int showMode) { + LogUtil.i(TAG, "Soft keyboard show mode changed, to= " + (showMode == SHOW_MODE_AUTO? "自动": "隐藏")); + } + }); + controller.setShowMode(SHOW_MODE_AUTO); } + registerSelf(); } @Override @@ -123,6 +126,8 @@ public void setAccessibilityEventTracker(AccessibilityEventTracker tracker) { @Override public void onAccessibilityEvent(AccessibilityEvent event) { + LogUtil.v(TAG, "收到辅助功能事件:" + event.getEventType()); + // SoloPi的窗口事件,不处理 if (StringUtil.equals(event.getPackageName(), getPackageName())) { return; @@ -131,16 +136,7 @@ public void onAccessibilityEvent(AccessibilityEvent event) { // 如果没有注册过 if (!LauncherApplication.getInstance().getAccessibilityState() && LauncherApplication.getInstance().hasFinishInit()) { // 注册自身到注入服务 - InjectorService service = LauncherApplication.getInstance() - .findServiceByName(InjectorService.class.getName()); - if (service != null) { - service.register(this); - } - // 防止之前block - setServiceToNormalMode(); - - // 设置为已注册 - LauncherApplication.getInstance().setAccessibilityState(true); + registerSelf(); } if (accessibilityEventTrackerRef == null) { @@ -170,6 +166,23 @@ protected boolean onGesture(int gestureId) { return super.onGesture(gestureId); } + private void registerSelf() { + BackgroundExecutor.execute(new Runnable() { + @Override + public void run() { + LauncherApplication.getInstance().prepareInMain(); + InjectorService.g().register(AccessibilityServiceImpl.this); + LauncherApplication.getInstance().setAccessibilityState(true); + LauncherApplication.getInstance().runOnUiThread(new Runnable() { + @Override + public void run() { + setServiceToNormalMode(); + } + }); + } + }); + } + @Override public void onInterrupt() { LogUtil.e(TAG, "服务被Interrupt"); @@ -184,6 +197,18 @@ public void setAccessonilityMode(int mode) { } } + private AccessibilityServiceInfo _getServiceInfo() { + AccessibilityServiceInfo serviceInfo = getServiceInfo(); + if (serviceInfo == null) { + serviceInfo = new AccessibilityServiceInfo(); + serviceInfo.eventTypes = AccessibilityEvent.TYPES_ALL_MASK; + serviceInfo.notificationTimeout = 100; + serviceInfo.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC; + } + + return serviceInfo; + } + private void setServiceInfoToTouchBlockMode() { AccessibilityServiceInfo info = getServiceInfo(); if (info == null) { diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/event/constant/Constant.java b/src/shared/src/main/java/com/alipay/hulu/shared/event/constant/Constant.java index b590905..1ff4e5c 100644 --- a/src/shared/src/main/java/com/alipay/hulu/shared/event/constant/Constant.java +++ b/src/shared/src/main/java/com/alipay/hulu/shared/event/constant/Constant.java @@ -93,4 +93,6 @@ public class Constant { * 触摸位置 */ public static final String KEY_TOUCH_POINT = "touchPoint"; + + public static final String RUNNING_STATUS = "RUNNING_STATUS"; } diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/event/touch/CmdTouchService.java b/src/shared/src/main/java/com/alipay/hulu/shared/event/touch/CmdTouchService.java index e446c33..5338c3c 100644 --- a/src/shared/src/main/java/com/alipay/hulu/shared/event/touch/CmdTouchService.java +++ b/src/shared/src/main/java/com/alipay/hulu/shared/event/touch/CmdTouchService.java @@ -24,6 +24,7 @@ import com.alipay.hulu.common.service.base.LocalService; import com.alipay.hulu.common.tools.CmdTools; import com.alipay.hulu.common.utils.LogUtil; +import com.alipay.hulu.common.utils.MiscUtil; /** * Created by qiaoruikai on 2019/12/2 2:22 PM. @@ -44,11 +45,16 @@ public void press(int x, int y, int pressTime) { @Override public void scroll(int x1, int y1, int x2, int y2, int scrollTime) { + long currentTime = System.currentTimeMillis(); if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR2) { CmdTools.execHighPrivilegeCmd("input swipe " + x1 + " " + y1 + " " + x2 + " " + y2); } else { CmdTools.execHighPrivilegeCmd("input swipe " + x1 + " " + y1 + " " + x2 + " " + y2 + " " + scrollTime); } + long timeToSleep = currentTime + scrollTime - System.currentTimeMillis(); + if (timeToSleep > 0) { + MiscUtil.sleep(timeToSleep); + } } @Override diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/event/touch/TouchEventTracker.java b/src/shared/src/main/java/com/alipay/hulu/shared/event/touch/TouchEventTracker.java index d8ed5c2..fde2f72 100644 --- a/src/shared/src/main/java/com/alipay/hulu/shared/event/touch/TouchEventTracker.java +++ b/src/shared/src/main/java/com/alipay/hulu/shared/event/touch/TouchEventTracker.java @@ -230,7 +230,7 @@ public void stopCmdLine() { cmdLine = null; } - @Subscriber(@Param(LauncherApplication.SCREEN_ORIENTATION)) + @Subscriber(@Param(com.alipay.hulu.common.constant.Constant.SCREEN_ORIENTATION)) public void setScreenOrientation(int orientation) { this.currentOrientation = (orientation + defaultScreenRotation) % 4; @@ -548,7 +548,7 @@ private void parseSingleLine(String line, TouchEventTracker tracker) { lastUpActionTime = upTime; - LogUtil.w(TAG, "Tracking line: " + line); + LogUtil.d(TAG, "Tracking line: " + line); tracker.receiveTouchUp(upTime); } else { // 根据DOWN消息来确定是否有点击 @@ -580,7 +580,7 @@ private void parseSingleLine(String line, TouchEventTracker tracker) { lastUpActionTime = upTime; - LogUtil.w(TAG, "Tracking line: " + line); + LogUtil.d(TAG, "Tracking line: " + line); tracker.receiveTouchUp(upTime); } else if (line.contains("DOWN")) { long downTime = getEventMicroSecond(line); @@ -594,20 +594,20 @@ private void parseSingleLine(String line, TouchEventTracker tracker) { if (xFactor == 0f || yFactor == 0f) { reloadFactor(line); } - LogUtil.i(TAG, line); + LogUtil.d(TAG, line); waitForXY = new boolean[]{true, true}; tracker.receiveTouchDown(downTime); } } else if (waitForXY[0] && line.contains("ABS_MT_POSITION_X")) { - LogUtil.i(TAG, line); + LogUtil.d(TAG, line); String[] splited = line.split("ABS_MT_POSITION_X"); String x = splited[splited.length - 1].trim(); xy[0] = (int) (Integer.parseInt(x, 16) * xFactor); waitForXY[0] = false; - LogUtil.w("lezhou", "xfactor:" + xFactor); + LogUtil.d("lezhou", "xfactor:" + xFactor); - LogUtil.w("lezhou", "x: " + (xy[0])); + LogUtil.d("lezhou", "x: " + (xy[0])); // 如果xy都找到了,发送消息 sendIfPossible(line); @@ -618,7 +618,7 @@ private void parseSingleLine(String line, TouchEventTracker tracker) { xy[1] = (int) (Integer.parseInt(y, 16) * yFactor); waitForXY[1] = false; - LogUtil.w("lezhou", "y: " + xy[1]); + LogUtil.d("lezhou", "y: " + xy[1]); // 如果xy都找到了,发送消息 sendIfPossible(line); @@ -723,6 +723,10 @@ private void sendIfPossible(String line) { // 获取毫秒级时间 long eventTime = getEventMicroSecond(line); handlerRef.get().receiveNewTouch(eventTime, p); + + // 接收下一次事件 + waitForXY[0] = true; + waitForXY[1] = true; } } } diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/event/touch/TouchWrapper.java b/src/shared/src/main/java/com/alipay/hulu/shared/event/touch/TouchWrapper.java new file mode 100644 index 0000000..391b028 --- /dev/null +++ b/src/shared/src/main/java/com/alipay/hulu/shared/event/touch/TouchWrapper.java @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.shared.event.touch; + +import android.graphics.Point; + +import com.alipay.hulu.common.injector.InjectorService; +import com.alipay.hulu.common.injector.param.Subscriber; +import com.alipay.hulu.common.injector.provider.Param; +import com.alipay.hulu.common.utils.LogUtil; +import com.alipay.hulu.shared.event.bean.UniversalEventBean; + +import java.util.HashSet; +import java.util.Set; + +import static com.alipay.hulu.shared.event.constant.Constant.EVENT_TOUCH_DOWN; +import static com.alipay.hulu.shared.event.constant.Constant.EVENT_TOUCH_POSITION; +import static com.alipay.hulu.shared.event.constant.Constant.EVENT_TOUCH_UP; +import static com.alipay.hulu.shared.event.constant.Constant.KEY_TOUCH_POINT; + +public class TouchWrapper { + private static final String TAG = TouchWrapper.class.getSimpleName(); + + private static volatile TouchWrapper _INSTANCE; + + /** + * 操作100px范围内认为是点击 + */ + private static final int CLICK_RANGE = 100; + + /** + * 500毫秒以上点击为长按 + */ + private static final long LONG_CLICK_RANGE = 500; + + /** + * 起始点击位置 + */ + private Point startPos; + + /** + * 起始时间 + */ + private long startTime; + + /** + * 当前点击位置 + */ + private Point currentPos; + + /** + * 当前时间 + */ + private long curTime; + + private boolean listening; + + /** + * 是否正在运行 + */ + public volatile boolean isRunning; + + /** + * 手势监听器 + */ + private Set gestureListeners; + + /** + * 获取单例 + * @return + */ + public static TouchWrapper getInstance() { + if (_INSTANCE == null) { + synchronized (TouchWrapper.class) { + if (_INSTANCE == null) { + _INSTANCE = new TouchWrapper(); + } + } + } + + return _INSTANCE; + } + + private TouchWrapper() { + isRunning = false; + gestureListeners = new HashSet<>(); + listening = false; + } + + public void start() { + InjectorService.g().register(this); + isRunning = true; + } + + public void stop() { + InjectorService.g().unregister(this); + isRunning = false; + } + + public boolean isRunning() { + return isRunning; + } + + @Subscriber(@Param(value = EVENT_TOUCH_DOWN, sticky = false)) + public void receiveTouchDown(UniversalEventBean event) { + receiveTouchDown(event.getTime()); + } + + /** + * 收到手指放下事件 + * @param time + */ + public void receiveTouchDown(long time) { + // 只关心单点操作 + if (listening) { + return; + } + + LogUtil.i(TAG, "Receive Touch down at " + time); + startPos = null; + startTime = time; + listening =true; + } + + @Subscriber(@Param(value = EVENT_TOUCH_POSITION, sticky = false)) + public void receiveTouchPos(UniversalEventBean event) { + receiveTouchPosition((Point) event.getParam(KEY_TOUCH_POINT), event.getTime()); + } + + /** + * 收到touch坐标 + * @param p + * @param time + */ + public void receiveTouchPosition(Point p, long time) { + LogUtil.i(TAG, "Receive Touch position %s at %d", p, time); + if (!listening) { + return; + } + + if (startPos == null) { + startPos = p; + startTime = time; + } + + currentPos = p; + curTime = time; + + if (curTime - startTime >= LONG_CLICK_RANGE) { + double distance = calDistance(startPos, currentPos); + if (distance < CLICK_RANGE) { + for (GestureListener listener: gestureListeners) { + listener.receiveLongClick(currentPos, curTime - startTime); + } + listening = false; + } + } + } + + @Subscriber(@Param(value = EVENT_TOUCH_UP, sticky = false)) + public void receiveTouchUp(UniversalEventBean event) { + receiveTouchUp(event.getTime()); + } + + /** + * 收到手指抬起事件 + * @param endTime + */ + public void receiveTouchUp(long endTime) { + if (!listening) { + return; + } + + LogUtil.i(TAG, "Receive up at " + endTime); + double distance = calDistance(startPos, currentPos); + if (distance < CLICK_RANGE) { + if (endTime - startTime < LONG_CLICK_RANGE) { + for (GestureListener listener: gestureListeners) { + listener.receiveClick(currentPos); + } + } else { + for (GestureListener listener: gestureListeners) { + listener.receiveLongClick(currentPos, endTime - startTime); + } + } + } else { + for (GestureListener listener: gestureListeners) { + listener.receiveScroll(startPos, currentPos, endTime - startTime); + } + } + + listening = false; + } + + public void listen(GestureListener listener) { + gestureListeners.add(listener); + } + + public void cancelListen(GestureListener listener) { + gestureListeners.remove(listener); + } + + /** + * 计算 + * @param a + * @param b + * @return + */ + private double calDistance(Point a, Point b) { + return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2) + 1); + } + + /** + * 手势监听器 + */ + public interface GestureListener { + void receiveClick(Point p); + + void receiveLongClick(Point p, long time); + + void receiveScroll(Point start, Point end, long time); + } +} diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/node/NodeTreeGenerator.java b/src/shared/src/main/java/com/alipay/hulu/shared/node/NodeTreeGenerator.java index b393043..94e43de 100644 --- a/src/shared/src/main/java/com/alipay/hulu/shared/node/NodeTreeGenerator.java +++ b/src/shared/src/main/java/com/alipay/hulu/shared/node/NodeTreeGenerator.java @@ -49,13 +49,17 @@ public class NodeTreeGenerator { * 构建当前Provider的树 * @return */ - public AbstractNodeTree generateNodeTree() { + public AbstractNodeTree generateNodeTree(NodeContext superContext) { // 没有处理器,无法生成 if (nodeProvider == null || nodeProcessors == null || nodeProcessors.size() == 0) { return null; } // 初始化上下文 - context = new NodeContext(); + if (superContext == null) { + context = new NodeContext(); + } else { + context = superContext; + } long startTime = System.currentTimeMillis(); LogUtil.d(TAG, "开始加载树结构"); @@ -91,7 +95,7 @@ public AbstractNodeTree generateNodeTree() { if (AbstractProvider.class.isAssignableFrom((Class) target)) { // 重开一次加载树流程 - targetTree = this.manager.get().startLoadProvider((Class) target, loadCurrentProviderClass()); + targetTree = this.manager.get().startLoadProvider((Class) target, loadCurrentProviderClass(), context); // 生成成功,结束处理 if (targetTree != null) { @@ -148,6 +152,10 @@ private List> loadCurrentProviderClass() return providerClasses; } + public NodeContext getContext() { + return context; + } + public NodeTreeGenerator(List nodeProcessors, AbstractProvider nodeProvider, OperationService manager) { this.nodeProcessors = nodeProcessors; this.nodeProvider = nodeProvider; diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/node/OperationService.java b/src/shared/src/main/java/com/alipay/hulu/shared/node/OperationService.java index 6237cd0..4d07e25 100644 --- a/src/shared/src/main/java/com/alipay/hulu/shared/node/OperationService.java +++ b/src/shared/src/main/java/com/alipay/hulu/shared/node/OperationService.java @@ -22,6 +22,7 @@ import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.alipay.hulu.common.application.LauncherApplication; +import com.alipay.hulu.common.constant.Constant; import com.alipay.hulu.common.injector.InjectorService; import com.alipay.hulu.common.injector.param.Subscriber; import com.alipay.hulu.common.injector.provider.Param; @@ -36,9 +37,13 @@ import com.alipay.hulu.shared.node.action.OperationMethod; import com.alipay.hulu.shared.node.action.provider.ActionProviderManager; import com.alipay.hulu.shared.node.tree.AbstractNodeTree; +import com.alipay.hulu.shared.node.tree.FakeNodeTree; +import com.alipay.hulu.shared.node.tree.accessibility.AccessibilityNodeProcessor; +import com.alipay.hulu.shared.node.tree.accessibility.AccessibilityProvider; import com.alipay.hulu.shared.node.tree.annotation.NodeProcessor; import com.alipay.hulu.shared.node.tree.annotation.NodeProvider; import com.alipay.hulu.shared.node.tree.export.BaseStepExporter; +import com.alipay.hulu.shared.node.utils.NodeContext; import java.util.ArrayList; import java.util.Arrays; @@ -67,6 +72,7 @@ public class OperationService implements ExportService { private List> defaultProcessors; private volatile AbstractNodeTree currentRoot; + private volatile NodeContext context; private ActionProviderManager actionProviderMng; @@ -120,7 +126,7 @@ public void stopExtraActionHandle() { actionProviderMng.stop(LauncherApplication.getContext()); } - @Subscriber(@Param(LauncherApplication.SCREEN_ORIENTATION)) + @Subscriber(@Param(Constant.SCREEN_ORIENTATION)) public void setScreenOrientation(int orientation) { if (currentOrientation != orientation) { currentOrientation = orientation; @@ -180,7 +186,10 @@ public void invalidRoot() { } if (tmp != null) { + long startTime = System.currentTimeMillis(); tmp.recycle(); + + LogUtil.d(TAG, "节点回收耗时%dms", System.currentTimeMillis() - startTime); } } @@ -234,6 +243,23 @@ public AbstractNodeTree getCurrentRoot() { return currentRoot; } + /** + * 获取当前根节点 + * + * @return + */ + public AbstractNodeTree getBaseCurrentRoot() { + LogUtil.w(TAG, "开始加载跟结构"); + synchronized (this) { + if (currentRoot != null) { + currentRoot.recycle(); + } + LogUtil.w(TAG, "重载树结构"); + currentRoot = startLoadProvider(AccessibilityProvider.class, Collections.>singletonList(AccessibilityNodeProcessor.class)); + return currentRoot; + } + } + /** * 刷新并返回根节点 * @@ -243,7 +269,10 @@ public AbstractNodeTree refreshCurrentRoot() { // 先回收下 synchronized (this) { if (currentRoot != null) { + long startTime = System.currentTimeMillis(); currentRoot.recycle(); + + LogUtil.d(TAG, "节点回收耗时%dms", System.currentTimeMillis() - startTime); currentRoot = null; } } @@ -259,6 +288,18 @@ public AbstractNodeTree refreshCurrentRoot() { * @return */ public AbstractNodeTree startLoadProvider(Class providerClass, List> processorClasses) { + return startLoadProvider(providerClass, processorClasses, null); + } + + /** + * 加载Provider树 + * @param providerClass + * @param processorClasses + * @return + */ + public AbstractNodeTree startLoadProvider(Class providerClass, + List> processorClasses, + NodeContext superContext) { if (providerClass == null) { return null; } @@ -313,13 +354,15 @@ public AbstractNodeTree startLoadProvider(Class prov AbstractNodeTree tree; try { // 构造树结构 - tree = generator.generateNodeTree(); + tree = generator.generateNodeTree(superContext); + context = generator.getContext(); } catch (Exception e) { LogUtil.e(TAG, "构建树抛出异常,可能是正在切换页面,稍等片刻重试", e); MiscUtil.sleep(1000); try { - tree = generator.generateNodeTree(); + tree = generator.generateNodeTree(superContext); + context = generator.getContext(); } catch (Exception innerE) { LogUtil.e(TAG, "暂时无法生成树结构", e); tree = null; @@ -416,6 +459,10 @@ public void putTemporaryParam(String key, Object value) { temporaryVariables.put(key, value); } + public NodeContext getNodeContext() { + return context; + } + /** * 移除临时变量 * 内部用 @@ -538,6 +585,23 @@ public synchronized Object getRuntimeParam(String key) { return null; } + /** + * 返回实际值 + * @param val + * @return + */ + private Object returnRealVal(Object val) { + if (val == null) { + return null; + } + + if (val instanceof String) { + return OperationExecutor.getMappedContent((String) val, this); + } else { + return val; + } + } + public ActionProviderManager getActionProviderMng() { return actionProviderMng; } diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/node/action/CmdExecutor.java b/src/shared/src/main/java/com/alipay/hulu/shared/node/action/CmdExecutor.java index 7a596f3..3335bae 100644 --- a/src/shared/src/main/java/com/alipay/hulu/shared/node/action/CmdExecutor.java +++ b/src/shared/src/main/java/com/alipay/hulu/shared/node/action/CmdExecutor.java @@ -27,6 +27,7 @@ import java.util.Locale; import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; @@ -322,7 +323,7 @@ public String executeCmdSync(final String cmd, int maxTime) { * 执行runnable * @param runnable */ - public void execute(Runnable runnable) { - executorService.execute(runnable); + public Future execute(Runnable runnable) { + return executorService.submit(runnable); } } diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/node/action/Constant.java b/src/shared/src/main/java/com/alipay/hulu/shared/node/action/Constant.java index ea6de11..debef4a 100644 --- a/src/shared/src/main/java/com/alipay/hulu/shared/node/action/Constant.java +++ b/src/shared/src/main/java/com/alipay/hulu/shared/node/action/Constant.java @@ -35,4 +35,6 @@ public class Constant { public static final String ASSERT_XIAOYU = "assert_xiaoyu"; public static final String ASSERT_XIAOYUANDEQUAL = "assert_xiaoyuAndEqual"; public static final String KEY_CURRENT_MODE = "KEY_CURRENT_MODE"; + + public static final String TRIGGER_INPUT_METHOD = "TRIGGER_INPUT_METHOD"; } diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/node/action/NodeKeyBoard.java b/src/shared/src/main/java/com/alipay/hulu/shared/node/action/NodeKeyBoard.java new file mode 100644 index 0000000..f2fca0a --- /dev/null +++ b/src/shared/src/main/java/com/alipay/hulu/shared/node/action/NodeKeyBoard.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.shared.node.action; + +import static com.alipay.hulu.common.constant.Constant.IME.IME_CLEAR_TEXT; +import static com.alipay.hulu.common.constant.Constant.IME.IME_HIDE_IME; +import static com.alipay.hulu.common.constant.Constant.IME.IME_INPUT_KEY_CODE; +import static com.alipay.hulu.common.constant.Constant.IME.IME_INPUT_TEXT; +import static com.alipay.hulu.common.constant.Constant.IME.IME_INPUT_TEXT_ENTER; +import static com.alipay.hulu.common.constant.Constant.IME.IME_STATUS; + +import com.alipay.hulu.common.injector.InjectorService; +import com.alipay.hulu.common.injector.param.Subscriber; +import com.alipay.hulu.common.injector.provider.Param; +import com.alipay.hulu.common.tools.CmdTools; +import com.alipay.hulu.common.utils.MiscUtil; +import com.alipay.hulu.common.utils.StringUtil; + +/** + * 封装键盘 + */ +public class NodeKeyBoard { + + private volatile boolean keyBoardShown = false; + private CmdExecutor executor; + + @Subscriber(@Param(IME_STATUS)) + public void setKeyBoardShown(boolean keyBoardShown) { + this.keyBoardShown = keyBoardShown; + } + + public NodeKeyBoard(CmdExecutor executor) { + InjectorService.g().register(this); + this.executor = executor; + } + + public void disconnect() { + InjectorService.g().unregister(this); + } + + /** + * 在激活的输入窗口输入内容 + * @param text + */ + public void inputInActiveIme(String text) { + // 如果不是AdbIME,切换到对应IDE中 + String defaultIme = getCurrentIme(); + if (!StringUtil.equals(defaultIme, "com.alipay.hulu/.common.tools.AdbIME") && + !StringUtil.equals(defaultIme, "com.alipay.hulu/com.alipay.hulu.common.tools.AdbIME")) { + CmdTools.switchToIme("com.alipay.hulu/.common.tools.AdbIME"); + MiscUtil.sleep(500); + } + + // 有键盘显示,输入内容 + if (keyBoardShown) { + InjectorService.g().pushMessage(IME_CLEAR_TEXT); + MiscUtil.sleep(500); + InjectorService.g().pushMessage(IME_INPUT_TEXT, text); + MiscUtil.sleep(500); + } + + // 切换回原始IME + if (!StringUtil.equals(defaultIme, "com.alipay.hulu/.common.tools.AdbIME") && + !StringUtil.equals(defaultIme, "com.alipay.hulu/com.alipay.hulu.common.tools.AdbIME")) { + CmdTools.switchToIme(defaultIme); + } + } + + /** + * 输入 + * @param text 待输入文字 + * @param x y 输入区域 + */ + public void inputText(String text, int x, int y) { + String defaultIme = getCurrentIme(); + if (!StringUtil.equals(defaultIme, "com.alipay.hulu/.common.tools.AdbIME") && + !StringUtil.equals(defaultIme, "com.alipay.hulu/com.alipay.hulu.common.tools.AdbIME")) { + CmdTools.switchToIme("com.alipay.hulu/.common.tools.AdbIME"); + MiscUtil.sleep(500); + } + + // 点击特定区域触发IME显示 + executor.executeClick(x, y); + MiscUtil.sleep(1000); + + if (keyBoardShown) { + InjectorService.g().pushMessage(IME_CLEAR_TEXT); + MiscUtil.sleep(500); + InjectorService.g().pushMessage(IME_INPUT_TEXT, text); + MiscUtil.sleep(500); + InjectorService.g().pushMessage(IME_HIDE_IME); + MiscUtil.sleep(500); + } else { + // 还是没有IME显示,直接Input输入 + executor.executeCmdSync("input text " + text); + MiscUtil.sleep(1500); + } + + if (!StringUtil.equals(defaultIme, "com.alipay.hulu/.common.tools.AdbIME") && + !StringUtil.equals(defaultIme, "com.alipay.hulu/com.alipay.hulu.common.tools.AdbIME")) { + CmdTools.switchToIme(defaultIme); + } + } + + /** + * 输入并搜索 + * @param text 待输入文字 + */ + public void inputTextSearch(String text, int x, int y) { + String defaultIme = getCurrentIme(); + if (!StringUtil.equals(defaultIme, "com.alipay.hulu/.common.tools.AdbIME") && + !StringUtil.equals(defaultIme, "com.alipay.hulu/com.alipay.hulu.common.tools.AdbIME")) { + CmdTools.switchToIme("com.alipay.hulu/.common.tools.AdbIME"); + } + MiscUtil.sleep(500); + executor.executeClick(x, y); + MiscUtil.sleep(1000); + + if (keyBoardShown) { + InjectorService.g().pushMessage(IME_CLEAR_TEXT); + MiscUtil.sleep(500); + InjectorService.g().pushMessage(IME_INPUT_TEXT_ENTER, text); + MiscUtil.sleep(500); + InjectorService.g().pushMessage(IME_HIDE_IME); + MiscUtil.sleep(500); + } else { + executor.executeCmd("input text " + text); + MiscUtil.sleep(1500); + executor.executeCmd("input keyevent 66"); + MiscUtil.sleep(500); + } + + if (!StringUtil.equals(defaultIme, "com.alipay.hulu/.common.tools.AdbIME") && + !StringUtil.equals(defaultIme, "com.alipay.hulu/com.alipay.hulu.common.tools.AdbIME")) { + CmdTools.switchToIme(defaultIme); + } + } + + /** + * 输入keyCode + * @param keyCode + */ + public void inputKeyCode(int keyCode) { + if (keyBoardShown) { + InjectorService.g().pushMessage(IME_INPUT_KEY_CODE, keyCode); + } else { + executor.executeCmd("input keyevent " + keyCode); + MiscUtil.sleep(1500); + } + } + + /** + * 获取当前输入法 + * @return + */ + private String getCurrentIme() { + return StringUtil.trim(executor.executeCmdSync("settings get secure default_input_method")); + } +} diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/node/action/OperationContext.java b/src/shared/src/main/java/com/alipay/hulu/shared/node/action/OperationContext.java index 21a68e4..1d1f3de 100644 --- a/src/shared/src/main/java/com/alipay/hulu/shared/node/action/OperationContext.java +++ b/src/shared/src/main/java/com/alipay/hulu/shared/node/action/OperationContext.java @@ -1,7 +1,23 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.alipay.hulu.shared.node.action; import com.alipay.hulu.common.utils.LogUtil; +import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -13,10 +29,17 @@ public class OperationContext { public CmdExecutor executor; private OperationListener listener; public OperationExecutor opExecutor; + public NodeKeyBoard keyBoard; + private volatile Thread currentThread; + private volatile Future future; public int screenWidth; public int screenHeight; + public OperationContext() { + currentThread = Thread.currentThread(); + } + /** * 后台执行并通知完成 * @param runnable 待执行任务 @@ -28,7 +51,11 @@ public void run() { try { runnable.run(); } catch (Throwable e) { - LogUtil.e("OperationContext", "execute background action throw " + e.getMessage(), e); + if (e instanceof InterruptedException) { + LogUtil.i("OperationContext", "任务强制中断"); + } else { + LogUtil.e("OperationContext", "execute background action throw " + e.getMessage(), e); + } } finally { // 清理当前结构 opExecutor.invalidRoot(); @@ -37,29 +64,52 @@ public void run() { } }; - executor.execute(wrapper); + future = executor.execute(wrapper); + } + + public void cancelRunning() { + currentThread.interrupt(); + if (future != null) { + future.cancel(true); + } + notifyOperationFinish(); } /** * 通知执行完成 */ public void notifyOperationFinish() { + if (future != null) { + future = null; + } if (listener != null && !notifyStatus.getAndSet(true)) { listener.notifyOperationFinish(); } + if (keyBoard != null) { + keyBoard.disconnect(); + } } public void setListener(OperationListener listener) { this.listener = listener; + if (listener != null) { + listener.onContextReceive(this); + } } /** * 操作执行回调 */ public interface OperationListener { + void onContextReceive(OperationContext context); /** * 通知执行完成 */ void notifyOperationFinish(); } + + public static abstract class BaseOperationListener implements OperationListener { + public void onContextReceive(OperationContext context) { + } + } } diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/node/action/OperationExecutor.java b/src/shared/src/main/java/com/alipay/hulu/shared/node/action/OperationExecutor.java index 309913c..53d7e48 100644 --- a/src/shared/src/main/java/com/alipay/hulu/shared/node/action/OperationExecutor.java +++ b/src/shared/src/main/java/com/alipay/hulu/shared/node/action/OperationExecutor.java @@ -46,6 +46,8 @@ import com.alipay.hulu.common.utils.StringUtil; import com.alipay.hulu.shared.node.AbstractNodeProcessor; import com.alipay.hulu.shared.node.OperationService; +import com.alipay.hulu.shared.node.locater.OperationNodeLocator; +import com.alipay.hulu.shared.node.locater.comparator.ItemComparator; import com.alipay.hulu.shared.node.tree.AbstractNodeTree; import com.alipay.hulu.shared.node.tree.accessibility.AccessibilityNodeProcessor; import com.alipay.hulu.shared.node.tree.accessibility.AccessibilityProvider; @@ -314,6 +316,7 @@ public OperationContext wrapOpContext(OperationContext.OperationListener listene context.setListener(listener); context.opExecutor = this; + context.keyBoard = new NodeKeyBoard(executor); context.executor = executor; context.screenHeight = height; context.screenWidth = width; @@ -412,23 +415,27 @@ public void run() { } } - boolean result; - if (operationFlag) { - // 由节点自身去处理 - result = targetNode.performAction(method, opContext); - } else { - // 强制执行 - result = forceAction(method, node, opContext); + int result = -1; + try { + if (operationFlag) { + // 由节点自身去处理 + result = targetNode.performAction(method, opContext); + } else { + // 强制执行 + result = forceAction(method, node, opContext); + } + } catch (Exception e) { + LogUtil.e(TAG, "执行控件操作失败", e); } operationManagerRef.get().invalidRoot(); // 通知完成,失败就不通知 - if (result) { + if (result == 0) { opContext.notifyOperationFinish(); } - return result; + return result >= 0; } /** @@ -437,13 +444,13 @@ public void run() { * @param node * @return */ - private boolean forceAction(OperationMethod method, AbstractNodeTree node, OperationContext listener) { + private int forceAction(OperationMethod method, AbstractNodeTree node, OperationContext listener) { if (method.getActionEnum() == PerformActionEnum.INPUT) { inputText(method.getParam(OperationExecutor.INPUT_TEXT_KEY), node.getNodeBound(), listener); - return true; + return 1; } - return false; + return -1; } /** @@ -490,20 +497,11 @@ public void run() { * @param text * @param rect */ - private void inputText(final String text, final Rect rect, OperationContext listener) { + private void inputText(final String text, final Rect rect, final OperationContext listener) { listener.notifyOnFinish(new Runnable() { @Override public void run() { - LogUtil.e(TAG, "Start Input"); - try { - CmdTools.switchToIme("com.alipay.hulu/.tools.AdbIME"); - executor.executeClick(rect.centerX(), rect.centerY()); - MiscUtil.sleep(1500); - InjectorService.g().pushMessage("ADB_INPUT_TEXT", text); - } catch (Exception e) { - LogUtil.e(TAG, "Input throw Exception:" + e.getLocalizedMessage(), e); - } - LogUtil.e(TAG, "Finish Input"); + listener.keyBoard.inputText(text, rect.centerX(), rect.centerY()); } }); } @@ -593,6 +591,36 @@ public void run() { } }); return true; + case KEYBOARD_INPUT: + final String keyboardContent = method.getParam(INPUT_TEXT_KEY); + if (StringUtil.isEmpty(keyboardContent)) { + return false; + } + + Pattern pattern = Pattern.compile("[^0-9a-zA-Z]"); + if (pattern.matcher(keyboardContent).find()) { + return false; + } + opContext.notifyOnFinish(new Runnable() { + @Override + public void run() { + inputKeyboard(opContext, keyboardContent); + } + }); + return true; + case INPUT_GLOBAL: + final String input = method.getParam(INPUT_TEXT_KEY); + if (StringUtil.isEmpty(input)) { + return false; + } + + opContext.notifyOnFinish(new Runnable() { + @Override + public void run() { + opContext.keyBoard.inputInActiveIme(input); + } + }); + break; case GLOBAL_PINCH_IN: int x = width / 2; int y = height / 2; @@ -703,24 +731,28 @@ public void run() { opContext.notifyOnFinish(new Runnable() { @Override public void run() { - // 生成二维码 - Bitmap bitmap = BitmapUtil.generateCode(qrCode, format, 512, Color.WHITE, Color.BLACK); - - File targetDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM); - targetDir = new File(targetDir, "solopi"); - targetDir.mkdir(); - File saveImg = new File(targetDir, "image-" + System.currentTimeMillis() + ".jpg"); try { - FileOutputStream stream = new FileOutputStream(saveImg); - bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream); - stream.flush(); - stream.close(); - } catch (IOException e) { - LogUtil.e(TAG, "Fail to export to " + saveImg); - } + // 生成二维码 + Bitmap bitmap = BitmapUtil.generateCode(qrCode, format, 512, Color.WHITE, Color.BLACK); + + File targetDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM); + targetDir = new File(targetDir, "solopi"); + targetDir.mkdir(); + File saveImg = new File(targetDir, "image-" + System.currentTimeMillis() + ".jpg"); + try { + FileOutputStream stream = new FileOutputStream(saveImg); + bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream); + stream.flush(); + stream.close(); + } catch (IOException e) { + LogUtil.e(TAG, "Fail to export to " + saveImg); + } - BitmapUtil.notifyNewImage(saveImg); - MiscUtil.sleep(500); + BitmapUtil.notifyNewImage(saveImg); + MiscUtil.sleep(500); + } catch (Exception e) { + LogUtil.w(TAG, "Fail to generate qr code", e); + } } }); return true; @@ -800,9 +832,10 @@ public void run() { long startTime = System.currentTimeMillis(); long sleeper; // 防止sleep不够 - while ((sleeper = System.currentTimeMillis() - startTime) < finalCount - 10) { + if ((sleeper = System.currentTimeMillis() - startTime) < finalCount - 10) { MiscUtil.sleep(finalCount - sleeper); } + LogUtil.i(TAG, "Sleep time: %dms", (System.currentTimeMillis() - startTime)); } }); return true; @@ -852,7 +885,7 @@ private boolean executeDeviceAction(OperationMethod method, final OperationConte injectorService.pushMessage(null, message, false); break; case SCREENSHOT: - String screenShot = method.getParam(INPUT_TEXT_KEY); + final String screenShot = method.getParam(INPUT_TEXT_KEY); if (StringUtil.isEmpty(screenShot)) { return false; } @@ -874,6 +907,8 @@ public void run() { opContext.screenHeight, opContext.screenWidth, opContext.screenHeight); if (bit != null) { return; + } else { + screenshot.delete(); } } @@ -967,6 +1002,118 @@ public void run() { return true; } + /** + * 键盘输入 + * @param opContext + * @param content + */ + private void inputKeyboard(OperationContext opContext, String content) { + int width = opContext.screenWidth; + int height = opContext.screenHeight; + OperationService operationService = operationManagerRef.get(); + if (operationService == null) { + return; + } + + // 一个字符一个字符输入 + for (char item: content.toCharArray()) { + AbstractNodeTree root = operationService.refreshCurrentRoot(); + final String target = item + ""; + if (item >= '0' && item <= '9') { + boolean result = findAndClickKeyboardKey(root, operationService, target, width, height); + if (!result) { + // 从字母键盘切换到数字键盘 + result = findAndClickKeyboardKey(root, operationService, "123", width, height); + if (!result) { + return; + } + + MiscUtil.sleep(1000); + root = operationService.refreshCurrentRoot(); + + result = findAndClickKeyboardKey(root, operationService, target, width, height); + if (!result) { + return; + } + } + } else { + boolean result = findAndClickKeyboardKey(root, operationService, target, width, height); + if (!result) { + // 从数字键盘切换到字母键盘 + result = findAndClickKeyboardKey(root, operationService, "ABC", width, height); + if (!result) { + return; + } + + MiscUtil.sleep(1000); + root = operationService.refreshCurrentRoot(); + + result = findAndClickKeyboardKey(root, operationService, target, width, height); + if (!result) { + return; + } + } + } + } + } + + /** + * 查找并点击键盘按键 + * @param root + * @param service + * @param text + * @param width + * @param height + * @return + */ + private boolean findAndClickKeyboardKey(AbstractNodeTree root, OperationService service, + final String text, int width, int height) { + if (StringUtil.isEmpty(text)) { + return false; + } + List result; + if (text.length() > 1) { + result = OperationNodeLocator.findAbstractNodeBySomething(root, new ItemComparator() { + @Override + public boolean isEqual(AbstractNodeTree item) { + return item != null && (StringUtil.contains(item.getText(), text) || StringUtil.contains(item.getDescription(), text)); + } + }); + } else { + result = OperationNodeLocator.findAbstractNodeBySomething(root, new ItemComparator() { + @Override + public boolean isEqual(AbstractNodeTree item) { + return item != null && (text.equalsIgnoreCase(item.getText()) || text.equalsIgnoreCase(item.getDescription())); + } + }); + } + + if (result.size() == 1) { + service.doSomeAction(new OperationMethod(PerformActionEnum.CLICK), result.get(0)); + return true; + } else if (result.size() > 1) { + AbstractNodeTree aim = null; + for (AbstractNodeTree node: result) { + Rect rect = node.getNodeBound(); + if (rect.width() >= width / 3 + 10 || rect.centerY() > height / 2) { + continue; + } + + aim = node; + break; + } + + if (aim == null) { + aim = result.get(0); + } + + service.doSomeAction(new OperationMethod(PerformActionEnum.CLICK), aim); + return true; + } else { + return false; + } + } + /** * 处理弹窗 * diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/node/action/OperationMethod.java b/src/shared/src/main/java/com/alipay/hulu/shared/node/action/OperationMethod.java index 267b56d..cbce6fe 100644 --- a/src/shared/src/main/java/com/alipay/hulu/shared/node/action/OperationMethod.java +++ b/src/shared/src/main/java/com/alipay/hulu/shared/node/action/OperationMethod.java @@ -38,6 +38,8 @@ public class OperationMethod implements Parcelable { private static final String TAG = OperationMethod.class.getSimpleName(); private boolean encrypt = true; + private Boolean safeEncrypt = false; + /** * 操作方法 */ diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/node/action/PerformActionEnum.java b/src/shared/src/main/java/com/alipay/hulu/shared/node/action/PerformActionEnum.java index 5b2e110..3883e09 100644 --- a/src/shared/src/main/java/com/alipay/hulu/shared/node/action/PerformActionEnum.java +++ b/src/shared/src/main/java/com/alipay/hulu/shared/node/action/PerformActionEnum.java @@ -40,6 +40,7 @@ public enum PerformActionEnum { MULTI_CLICK("multiClick", R.string.action__multi_click, 1, 0, R.drawable.dialog_action_drawable_multi_click, MULTICLICK_PARAMS), CLICK_IF_EXISTS("clickIfExists", R.string.action__find_click, 1, 0, R.drawable.dialog_action_drawable_click_if_exists), CLICK_QUICK("clickQuick", R.string.action__quick_click, 1, 0, R.drawable.dialog_action_drawable_quick_click_2), + CLICK_AND_INPUT("clickAndInput", R.string.action__click_and_input, 1, 0, R.drawable.dialog_action_drawable_click), INPUT_SEARCH("inputSearch", R.string.action__input_search, 1, 0, R.drawable.dialog_action_drawable_search, SEARCH_PARAMS), SCROLL_TO_BOTTOM("scrollToBottom", R.string.action__scroll_to_bottom, 1, 0, R.drawable.dialog_action_drawable_scroll_up, SCROLL_PARAMS), SCROLL_TO_TOP("scrollToTop", R.string.action_scroll_to_top, 1, 0, R.drawable.dialog_action_drawable_scroll_down, SCROLL_PARAMS), @@ -62,6 +63,8 @@ public enum PerformActionEnum { GLOBAL_SCROLL_TO_TOP("globalScrollToTop", R.string.action__global_scroll_up, 2, 0, R.drawable.dialog_action_drawable_scroll_down), GLOBAL_SCROLL_TO_RIGHT("globalScrollToRight", R.string.action__global_scroll_right, 2, 0, R.drawable.dialog_action_drawable_scroll_left), GLOBAL_SCROLL_TO_LEFT("globalScrollToLeft", R.string.action__global_scroll_left, 2, 0, R.drawable.dialog_action_drawable_scroll_right), + KEYBOARD_INPUT("keyboardInput", R.string.action__global_keyboard_input, 2, 0, R.drawable.dialog_action_drawable_keyboard), + INPUT_GLOBAL("inputGlobal", R.string.action__global_global_input, 3, 0, R.drawable.dialog_action_drawable_input, INPUT_PARAMS), GLOBAL_PINCH_OUT("globalPinchOut", R.string.action__pinch_out, 2, 0, R.drawable.dialog_action_drawable_pinch_out), GLOBAL_PINCH_IN("globalPinchIn", R.string.action__pinch_in, 2, 0, R.drawable.dialog_action_drawable_pinch_in), GLOBAL_GESTURE("globalGesture", R.string.action__gesture, 2, 0, R.drawable.dialog_action_drawable_gesture, GESTURE_PARAMS), @@ -96,6 +99,8 @@ public enum PerformActionEnum { /** * 本地模式专用 5 */ + FORCE_STOP("forceStop", R.string.action__force_stop, 6, 2, R.drawable.dialog_action_drawable_cancel), + NORMAL_EXIT("slaveExit", R.string.action__normal_exit, 6, 2, R.drawable.dialog_action_drawable_cancel), /** * 内部操作,不对外 diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/node/locater/PositionLocator.java b/src/shared/src/main/java/com/alipay/hulu/shared/node/locater/PositionLocator.java index ce350d0..1f5d4c4 100644 --- a/src/shared/src/main/java/com/alipay/hulu/shared/node/locater/PositionLocator.java +++ b/src/shared/src/main/java/com/alipay/hulu/shared/node/locater/PositionLocator.java @@ -20,6 +20,7 @@ import com.alipay.hulu.common.utils.LogUtil; import com.alipay.hulu.shared.node.tree.AbstractNodeTree; import com.alipay.hulu.shared.node.tree.FakeNodeTree; +import com.alipay.hulu.shared.node.tree.InputWindowTree; import java.util.ArrayList; import java.util.LinkedList; @@ -55,6 +56,13 @@ public static AbstractNodeTree findDeepestNode(AbstractNodeTree root, int x, int List windows = root.getChildrenNodes(); int maxIdx = -1; for (AbstractNodeTree window: windows) { + // 点到输入法框就是输入法 + if (window instanceof InputWindowTree) { + if (window.getNodeBound().contains(x, y)) { + return window; + } + } + // 找包含该坐标的最上层window if (window.getDrawingOrder() > maxIdx && window.getNodeBound().contains(x, y)) { maxIdx = window.getDrawingOrder(); diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/node/tree/AbstractNodeTree.java b/src/shared/src/main/java/com/alipay/hulu/shared/node/tree/AbstractNodeTree.java index 74aec8a..72e252d 100644 --- a/src/shared/src/main/java/com/alipay/hulu/shared/node/tree/AbstractNodeTree.java +++ b/src/shared/src/main/java/com/alipay/hulu/shared/node/tree/AbstractNodeTree.java @@ -20,6 +20,7 @@ import androidx.annotation.NonNull; +import com.alibaba.fastjson.JSONObject; import com.alipay.hulu.common.injector.InjectorService; import com.alipay.hulu.common.tools.CmdTools; import com.alipay.hulu.common.utils.ClassUtil; @@ -134,7 +135,7 @@ public abstract class AbstractNodeTree implements Iterable { * @param context 执行上下文 * @return */ - public boolean performAction(OperationMethod method, final OperationContext context) { + public int performAction(OperationMethod method, final OperationContext context) { final Rect rect = getNodeBound(); int x = rect.centerX(); int y = rect.centerY(); @@ -156,6 +157,8 @@ public boolean performAction(OperationMethod method, final OperationContext cont y = (int) (rect.top + factorY * rect.height()); } } + final int finalX = x; + final int finalY = y; PerformActionEnum action = method.getActionEnum(); @@ -168,176 +171,156 @@ public boolean performAction(OperationMethod method, final OperationContext cont clickDuration = Integer.parseInt(longClickTime); } - context.executor.executePress(x, y, clickDuration); - break; + final int finalClickDuration = clickDuration; + context.notifyOnFinish(new Runnable() { + @Override + public void run() { + context.executor.executePress(finalX, finalY, finalClickDuration); + } + }); + return 1; +// break; case ASSERT: LogUtil.i(TAG, "Start Assert"); - return NodeTreeUtil.performAssertAction(this, method); + return NodeTreeUtil.performAssertAction(this, method)? 0: -1; case CLICK: case CLICK_IF_EXISTS: case CLICK_QUICK: LogUtil.i(TAG, "Start ADB click " + x + "," + y); context.executor.executeClick(x, y); break; + case CLICK_AND_INPUT: + final String input = method.getParam(OperationExecutor.INPUT_TEXT_KEY); + if (StringUtil.isEmpty(input)) { + break; + } + context.notifyOnFinish(new Runnable() { + @Override + public void run() { + context.keyBoard.inputText(input, finalX, finalY); + } + }); + return 1; case INPUT_SEARCH: final String text = method.getParam(OperationExecutor.INPUT_TEXT_KEY); context.notifyOnFinish(new Runnable() { @Override public void run() { - LogUtil.e(TAG, "Start Input"); - try { - CmdTools.switchToIme("com.alipay.hulu/.tools.AdbIME"); - context.executor.executeClick(rect.centerX(), rect.centerY()); - MiscUtil.sleep(1500); - InjectorService.g().pushMessage("ADB_SEARCH_TEXT", text); - } catch (Exception e) { - LogUtil.e(TAG, "Input throw Exception:" + e.getLocalizedMessage(), e); - } - LogUtil.e(TAG, "Finish Input"); + context.keyBoard.inputTextSearch(text, finalX, finalY); } }); - break; + return 1; case MULTI_CLICK: - LogUtil.i(TAG, "Start ADB multi click " + x + "," + y); + LogUtil.w(TAG, "Start ADB multi click " + x + "," + y); String clickText = method.getParam(OperationExecutor.INPUT_TEXT_KEY); - int clickTime; + final int clickTime; try { clickTime = Integer.parseInt(clickText); // 数量小于一,无法执行 if (clickTime < 1) { - return false; + return -1; } } catch (NumberFormatException e) { LogUtil.e(TAG, e, "无法解析文字【%s】为数字", clickText); - return false; + return -1; } // 通过&实现短时间内多次点击 - for (int i = 0; i < clickTime; i++) { - context.executor.executeClickAsync(x, y); - MiscUtil.sleep(100); - } - break; - case SCROLL_TO_TOP: - LogUtil.i(TAG, "Start ADB scroll " + x + "," + y); - int fromHeight = y; - if (fromHeight > screenHeight - 40) { - fromHeight = screenHeight - 40; - - // 控件不可见 - if (fromHeight <= rect.top) { - return false; - } - } - - int scroll = height; - if (method.containsParam(OperationExecutor.INPUT_TEXT_KEY)) { - String content = method.getParam(OperationExecutor.INPUT_TEXT_KEY); - if (!StringUtil.isEmpty(content)) { - try { - scroll = (int) (Integer.parseInt(content) / 100f * height); - } catch (NumberFormatException e) { - LogUtil.e(TAG, "Content %s is not integer", content); + context.notifyOnFinish(new Runnable() { + @Override + public void run() { + for (int i = 0; i < clickTime; i++) { + context.executor.executeClickAsync(finalX, finalY); + MiscUtil.sleep(100); } } - } - - int toHeight = fromHeight - scroll; - if (toHeight < 40) { - toHeight = 40; - } - context.executor.executeScroll(x, fromHeight, x, toHeight, 300);; - break; + }); + return 1; + case SCROLL_TO_TOP: case SCROLL_TO_BOTTOM: - LogUtil.i(TAG, "Start ADB scroll " + x + "," + y); - fromHeight = y; - if (fromHeight < 40) { - fromHeight = 40; - - // 控件不可见 - if (fromHeight >= rect.bottom) { - return false; - } - } - - scroll = height; - if (method.containsParam(OperationExecutor.INPUT_TEXT_KEY)) { - String content = method.getParam(OperationExecutor.INPUT_TEXT_KEY); - if (!StringUtil.isEmpty(content)) { - try { - scroll = (int) (Integer.parseInt(content) / 100f * height); - } catch (NumberFormatException e) { - LogUtil.e(TAG, "Content %s is not integer", content); - } - } - } - - toHeight = fromHeight + scroll; - if (toHeight > screenHeight - 40) { - toHeight = screenHeight - 40; - } - - context.executor.executeScrollSync(x, y, x, toHeight, 300); - break; + case SCROLL_TO_LEFT: case SCROLL_TO_RIGHT: LogUtil.i(TAG, "Start ADB scroll " + x + "," + y); + int fromY = y; int fromX = x; - if (fromX < 10) { - fromX = 10; - if (fromX >= rect.right) { - return false; - } + if (fromY > screenHeight - 40) { + fromY = screenHeight - 40; } - - scroll = width; - if (method.containsParam(OperationExecutor.INPUT_TEXT_KEY)) { - String content = method.getParam(OperationExecutor.INPUT_TEXT_KEY); - if (!StringUtil.isEmpty(content)) { - try { - scroll = (int) (Integer.parseInt(content) / 100f * width); - } catch (NumberFormatException e) { - LogUtil.e(TAG, "Content %s is not integer", content); - } - } + if (fromY < 40) { + fromY = 40; } - - int toX = fromX + scroll; - if (toX > screenWidth - 10) { - toX = screenWidth - 10; + if (fromX < 10) { + fromX = 10; } - context.executor.executeScrollSync(fromX, y, toX, y, 300); - break; - case SCROLL_TO_LEFT: - LogUtil.i(TAG, "Start ADB scroll " + x + "," + y); - fromX = x; if (fromX > screenWidth - 10) { fromX = screenWidth - 10; - if (fromX <= rect.left) { - return false; - } } - scroll = width; + int toX = fromX, toY = fromY; + + float scrollPercent = 1F; if (method.containsParam(OperationExecutor.INPUT_TEXT_KEY)) { String content = method.getParam(OperationExecutor.INPUT_TEXT_KEY); if (!StringUtil.isEmpty(content)) { try { - scroll = (int) (Integer.parseInt(content) / 100f * width); + scrollPercent = Integer.parseInt(content) / 100f; } catch (NumberFormatException e) { LogUtil.e(TAG, "Content %s is not integer", content); } } } + if (action == PerformActionEnum.SCROLL_TO_BOTTOM || action == PerformActionEnum.SCROLL_TO_TOP) { + int scroll = (int) (scrollPercent * height); + if (action == PerformActionEnum.SCROLL_TO_TOP) { + toY = fromY - scroll; + } else { + toY = fromY + scroll; + } + } else { + int scroll = (int) (scrollPercent * width); + if (action == PerformActionEnum.SCROLL_TO_LEFT) { + toX = fromX - scroll; + } else { + toX = fromX + scroll; + } + } - toX = fromX - scroll; + if (toY > screenHeight - 40) { + toY = screenHeight - 40; + } + if (toY < 40) { + toY = 40; + } if (toX < 10) { toX = 10; } - context.executor.executeScrollSync(fromX, y, toX, y, 300); - break; + if (toX > screenWidth - 10) { + toX = screenWidth - 10; + } + final int fx = fromX, fy = fromY, tx = toX, ty = toY; + context.notifyOnFinish(new Runnable() { + @Override + public void run() { + context.executor.executeScroll(fx, fy, tx, ty, 300);; + } + }); + return 1; + case INPUT: + final String inputText = method.getParam(OperationExecutor.INPUT_TEXT_KEY); + if (StringUtil.isEmpty(inputText)) { + return -1; + } + context.notifyOnFinish(new Runnable() { + @Override + public void run() { + context.keyBoard.inputText(inputText, finalX, finalY); + waitInputMethodHide(); + } + }); + return 1; } - return true; + return 0; } /** @@ -376,6 +359,38 @@ public boolean hasParent(AbstractNodeTree nodeTree) { */ public abstract StringBuilder printTrace(StringBuilder builder); + + /** + * 打印树结构 + * @return + */ + public JSONObject exportToJsonObject() { + JSONObject obj = new JSONObject(); + obj.put("depth", depth); + obj.put("className", className); + obj.put("nodeBound", nodeBound); + obj.put("text", text); + obj.put("description", description); + obj.put("resourceId", resourceId); + obj.put("id", id); + obj.put("packageName", packageName); + obj.put("visible", visible); + obj.put("type", getClass().getSimpleName()); + + if (childrenNodes != null) { + List children = new ArrayList<>(childrenNodes.size() + 1); + for (AbstractNodeTree child : getChildrenNodes()) { + JSONObject childObj = child.exportToJsonObject(); + if (childObj != null) { + children.add(childObj); + } + } + obj.put("children", children); + } + + return obj; + } + /** * 自身是否可用于辅助定位 * @return diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/node/tree/FakeNodeTree.java b/src/shared/src/main/java/com/alipay/hulu/shared/node/tree/FakeNodeTree.java index 9b1f818..7d06677 100644 --- a/src/shared/src/main/java/com/alipay/hulu/shared/node/tree/FakeNodeTree.java +++ b/src/shared/src/main/java/com/alipay/hulu/shared/node/tree/FakeNodeTree.java @@ -15,8 +15,12 @@ */ package com.alipay.hulu.shared.node.tree; +import com.alibaba.fastjson.JSONObject; import com.alipay.hulu.shared.node.action.PerformActionEnum; +import java.util.ArrayList; +import java.util.List; + /** * Created by qiaoruikai on 2018/11/5 12:23 PM. */ @@ -36,6 +40,23 @@ public StringBuilder printTrace(StringBuilder builder) { return builder; } + @Override + public JSONObject exportToJsonObject() { + JSONObject obj = new JSONObject(4); + obj.put("type", getClass().getSimpleName()); + if (childrenNodes != null) { + List children = new ArrayList<>(childrenNodes.size() + 1); + for (AbstractNodeTree child : getChildrenNodes()) { + JSONObject childObj = child.exportToJsonObject(); + if (childObj != null) { + children.add(childObj); + } + } + obj.put("children", children); + } + return obj; + } + @Override public boolean isSelfUsableForLocating() { return false; diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/node/tree/InputWindowTree.java b/src/shared/src/main/java/com/alipay/hulu/shared/node/tree/InputWindowTree.java new file mode 100644 index 0000000..23abf53 --- /dev/null +++ b/src/shared/src/main/java/com/alipay/hulu/shared/node/tree/InputWindowTree.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.shared.node.tree; + +import com.alibaba.fastjson.JSONObject; +import com.alipay.hulu.shared.node.action.PerformActionEnum; + +import java.util.ArrayList; +import java.util.List; + +public class InputWindowTree extends AbstractNodeTree { + @Override + public boolean canDoAction(PerformActionEnum action) { + return false; + } + + @Override + public StringBuilder printTrace(StringBuilder builder) { + if (childrenNodes != null) { + for (AbstractNodeTree child : getChildrenNodes()) { + child.printTrace(builder); + } + } + return builder; + } + + @Override + public JSONObject exportToJsonObject() { + JSONObject obj = new JSONObject(4); + obj.put("type", getClass().getSimpleName()); + if (childrenNodes != null) { + List children = new ArrayList<>(childrenNodes.size() + 1); + for (AbstractNodeTree child : getChildrenNodes()) { + JSONObject childObj = child.exportToJsonObject(); + if (childObj != null) { + children.add(childObj); + } + } + obj.put("children", children); + } + return obj; + } + + @Override + public boolean isSelfUsableForLocating() { + return true; + } + + @Override + public int getDepth() { + return 0; + } + + @Override + public int getIndex() { + return 0; + } +} diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/node/tree/accessibility/AccessibilityNodeProcessor.java b/src/shared/src/main/java/com/alipay/hulu/shared/node/tree/accessibility/AccessibilityNodeProcessor.java index ab3da05..12cab59 100644 --- a/src/shared/src/main/java/com/alipay/hulu/shared/node/tree/accessibility/AccessibilityNodeProcessor.java +++ b/src/shared/src/main/java/com/alipay/hulu/shared/node/tree/accessibility/AccessibilityNodeProcessor.java @@ -22,11 +22,12 @@ import com.alipay.hulu.shared.node.AbstractNodeProcessor; import com.alipay.hulu.shared.node.tree.AbstractNodeTree; import com.alipay.hulu.shared.node.tree.FakeNodeTree; +import com.alipay.hulu.shared.node.tree.InputWindowTree; import com.alipay.hulu.shared.node.tree.accessibility.util.AccessibilityUtil; import com.alipay.hulu.shared.node.tree.annotation.NodeProcessor; import com.alipay.hulu.shared.node.utils.NodeContext; -@NodeProcessor(acceptNodes = { AccessibilityNodeInfo.class, FakeNodeTree.class }) +@NodeProcessor(acceptNodes = { AccessibilityNodeInfo.class, FakeNodeTree.class, InputWindowTree.class }) public class AccessibilityNodeProcessor implements AbstractNodeProcessor { private static final String TAG = "AccessibilityNodeProcessor"; @@ -40,6 +41,8 @@ public AbstractNodeTree loadNode(Object source, AbstractNodeTree parent, NodeCon } } else if (source instanceof FakeNodeTree) { return (FakeNodeTree) source; + } else if (source instanceof InputWindowTree) { + return (InputWindowTree) source; } return null; } diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/node/tree/accessibility/AccessibilityProvider.java b/src/shared/src/main/java/com/alipay/hulu/shared/node/tree/accessibility/AccessibilityProvider.java index fee3a3c..e9eaa17 100644 --- a/src/shared/src/main/java/com/alipay/hulu/shared/node/tree/accessibility/AccessibilityProvider.java +++ b/src/shared/src/main/java/com/alipay/hulu/shared/node/tree/accessibility/AccessibilityProvider.java @@ -15,6 +15,8 @@ */ package com.alipay.hulu.shared.node.tree.accessibility; +import static com.alipay.hulu.common.utils.activity.PermissionDialogActivity.cleanInstrumentationAndUiAutomator; + import android.accessibilityservice.AccessibilityService; import android.graphics.Rect; import android.os.Build; @@ -34,9 +36,10 @@ import com.alipay.hulu.common.utils.MiscUtil; import com.alipay.hulu.common.utils.StringUtil; import com.alipay.hulu.shared.R; -import com.alipay.hulu.shared.event.accessibility.AccessibilityServiceImpl; import com.alipay.hulu.shared.node.AbstractProvider; +import com.alipay.hulu.shared.node.action.Constant; import com.alipay.hulu.shared.node.tree.FakeNodeTree; +import com.alipay.hulu.shared.node.tree.InputWindowTree; import com.alipay.hulu.shared.node.tree.MetaTree; import com.alipay.hulu.shared.node.tree.annotation.NodeProvider; import com.alipay.hulu.shared.node.utils.NodeContext; @@ -49,8 +52,6 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import static com.alipay.hulu.common.utils.activity.PermissionDialogActivity.cleanInstrumentationAndUiAutomator; - /** * Created by qiaoruikai on 2018/10/8 12:46 PM. */ @@ -97,21 +98,37 @@ private void checkFrontActivityAccessibility() { waitToFind = true; LogUtil.i(TAG, "目标检查窗口:" + topPkg); - List windowInfos = service.getWindows(); - for (AccessibilityWindowInfo win : windowInfos) { - AccessibilityNodeInfo root = win.getRoot(); + int retryCount = 3; + while (waitToFind && retryCount-- > 0) { + List windowInfos = service.getWindows(); + for (AccessibilityWindowInfo win : windowInfos) { + AccessibilityNodeInfo root = win.getRoot(); - // 不包含子节点,直接跳过 - if (root == null) { - continue; + // 不包含子节点,直接跳过 + if (root == null) { + continue; + } + + LogUtil.i(TAG, "当前访问窗口:" + root.getPackageName()); + + // 是否能够找到目标应用 + if (StringUtil.contains(root.getPackageName(), topPkg)) { + waitToFind = false; + break; + } } - LogUtil.i(TAG, "当前访问窗口:" + root.getPackageName()); + // 此次查找未找到目标控件,等待处理一下 + if (waitToFind) { + if (windowInfos != null && windowInfos.size() > 0) { + for (AccessibilityWindowInfo windowInfo: windowInfos) { + windowInfo.recycle(); + } + windowInfos.clear(); + } - // 是否能够找到目标应用 - if (StringUtil.contains(root.getPackageName(), topPkg)) { - waitToFind = false; - break; + // 等300毫秒再操作 + MiscUtil.sleep(300); } } @@ -131,7 +148,7 @@ private void checkFrontActivityAccessibility() { * 获取根节点 * @return */ - private MetaTree getRootInWindows() { + private MetaTree getRootInWindows(NodeContext context) { AccessibilityService service = accessibilityServiceRef.get(); if (service == null) { return null; @@ -140,9 +157,29 @@ private MetaTree getRootInWindows() { // 构建Windows树 List windowInfos = service.getWindows(); AccessibilityWindowInfo targetWin = null; + CharSequence targetPkgName = null; AccessibilityWindowInfo maxSizeWin = null; + CharSequence maxSizePkgName = null; int maxSize = -1; + List> windowsHasContents = new ArrayList<>(); + MetaTree inputNode = null; for (AccessibilityWindowInfo win : windowInfos) { + Rect size = new Rect(); + win.getBoundsInScreen(size); + LogUtil.i(TAG, "当前窗口区域:%s, 类型:%d", size, win.getType()); + // 在极速模式下,输入法框特殊定义 + if (SPService.getBoolean(SPService.KEY_USE_EASY_MODE, false) && win.getType() == AccessibilityWindowInfo.TYPE_INPUT_METHOD) { + LogUtil.i(TAG, "发现输入法框,注意下"); + context.put(Constant.TRIGGER_INPUT_METHOD, "true"); + + inputNode = new MetaTree(); + InputWindowTree tree = new InputWindowTree(); + tree.setNodeBound(size); + tree.setDrawingOrder(Integer.MAX_VALUE); + inputNode.setCurrentNode(tree); + continue; + } + AccessibilityNodeInfo root = win.getRoot(); // 不包含子节点,直接跳过 @@ -154,18 +191,19 @@ private MetaTree getRootInWindows() { LogUtil.d(TAG, "自己的Windows,不处理"); continue; } - Rect size = new Rect(); - win.getBoundsInScreen(size); + windowsHasContents.add(new Pair<>(root.getPackageName(), win)); int realSize = size.width() * size.height(); if (realSize > maxSize) { maxSize = realSize; maxSizeWin = win; + maxSizePkgName = root.getPackageName(); } // 首先是active window if (win.isActive()) { LogUtil.i(TAG, "Active window:::" + root.getPackageName()); targetWin = win; + targetPkgName = root.getPackageName(); break; // 然后考虑有Accessibility focus的 } else if (targetWin == null && win.isAccessibilityFocused()) { @@ -177,14 +215,25 @@ private MetaTree getRootInWindows() { if (targetWin == null) { if (maxSizeWin != null) { targetWin = maxSizeWin; + targetPkgName = maxSizePkgName; } else { return null; } } + // 多窗口情况 + List relatedWindows = new ArrayList<>(); + if (targetPkgName != null) { + for (Pair pair : windowsHasContents) { + if (targetPkgName.equals(pair.first) && pair.second.getLayer() > targetWin.getLayer()) { + relatedWindows.add(pair.second); + } + } + } + windowsHasContents.clear(); MetaTree result = new MetaTree(); - if (targetWin.getChildCount() > 0) { + if (inputNode != null || targetWin.getChildCount() > 0 || relatedWindows.size() > 0) { FakeNodeTree fake = new FakeNodeTree(); result.setCurrentNode(fake); List children = new ArrayList<>(); @@ -195,9 +244,14 @@ private MetaTree getRootInWindows() { // 添加所有子窗口 Queue windowQueue = new LinkedList<>(); windowQueue.add(targetWin); + windowQueue.addAll(relatedWindows); CharSequence packageName = null; while (!windowQueue.isEmpty()) { AccessibilityWindowInfo windowInfo = windowQueue.poll(); + if (windowInfo == null) { + continue; + } + MetaTree item = new MetaTree(); AccessibilityNodeInfo root = windowInfo.getRoot(); if (packageName == null) { @@ -240,7 +294,11 @@ private MetaTree getRootInWindows() { fake.setPackageName(StringUtil.toString(packageName)); fake.setNodeBound(maxRect); - // 整合获取到的window + if (inputNode != null) { + children.add(inputNode); + } + + // 将window结果拍平 result.setChildren(children); } else { AccessibilityNodeInfo root = targetWin.getRoot(); @@ -292,7 +350,7 @@ public MetaTree provideMetaTree(NodeContext context) { // 重试三次 while (root == null && retryCount < 3) { try { - root = loadMetaTree(); + root = loadMetaTree(context); retryCount++; } catch (Exception e) { LogUtil.e(TAG, "Load accessibility tree throw exception: " + e.getMessage(), e); @@ -306,13 +364,13 @@ public MetaTree provideMetaTree(NodeContext context) { * 加载Meta树 * @return */ - private MetaTree loadMetaTree() { - MetaTree rootNode = getRootInWindows(); + private MetaTree loadMetaTree(NodeContext context) { + MetaTree rootNode = getRootInWindows(context); int retryCount = 0; while ((rootNode == null || rootNode.getCurrentNode() == null) && retryCount < 3) { MiscUtil.sleep(500); retryCount ++; - rootNode = getRootInWindows(); + rootNode = getRootInWindows(context); } if (rootNode == null || rootNode.getCurrentNode() == null) { @@ -327,7 +385,9 @@ private MetaTree loadMetaTree() { if (rootNode.getCurrentNode() instanceof FakeNodeTree) { for (MetaTree child: rootNode.getChildren()) { - nodeQueue.add(new Pair<>(child, (AccessibilityNodeInfo) child.getCurrentNode())); + if (child.getCurrentNode() instanceof AccessibilityNodeInfo) { + nodeQueue.add(new Pair<>(child, (AccessibilityNodeInfo) child.getCurrentNode())); + } } } else { nodeQueue.add(new Pair<>(rootNode, (AccessibilityNodeInfo) rootNode.getCurrentNode())); diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/node/tree/accessibility/tree/AccessibilityNodeTree.java b/src/shared/src/main/java/com/alipay/hulu/shared/node/tree/accessibility/tree/AccessibilityNodeTree.java index 7de157c..218b1ff 100644 --- a/src/shared/src/main/java/com/alipay/hulu/shared/node/tree/accessibility/tree/AccessibilityNodeTree.java +++ b/src/shared/src/main/java/com/alipay/hulu/shared/node/tree/accessibility/tree/AccessibilityNodeTree.java @@ -20,6 +20,8 @@ import android.os.Bundle; import android.view.accessibility.AccessibilityNodeInfo; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.annotation.JSONField; import com.alipay.hulu.common.application.LauncherApplication; import com.alipay.hulu.common.injector.InjectorService; import com.alipay.hulu.common.service.SPService; @@ -125,7 +127,7 @@ private void initAccessibilityNodeInfo(AccessibilityNodeInfo info) { info.getBoundsInScreen(rect); this.nodeBound = rect; this.isScrollable = info.isScrollable(); - this.visible = info.isVisibleToUser(); + this.visible = rect.width() * rect.height() > 0;//info.isVisibleToUser(); this.isClickable = info.isClickable(); this.isFocusable = info.isFocusable(); this.isEditable = info.isEditable(); @@ -156,6 +158,7 @@ public int getScrollType() { return TYPE_NOT_SCROLLABLE; } + @JSONField(serialize = false) public AccessibilityNodeInfo getCurrentNode() { return currentNode; } @@ -186,15 +189,23 @@ public boolean equals(Object obj) { } @Override - public boolean performAction(OperationMethod method, OperationContext opContext) { + public int performAction(OperationMethod method, OperationContext opContext) { PerformActionEnum action = method.getActionEnum(); switch (action) { case INPUT: - performInput(method.getParam(OperationExecutor.INPUT_TEXT_KEY), opContext); - return true; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + try { + performInputByAccessibility(method.getParam(OperationExecutor.INPUT_TEXT_KEY), opContext); + } catch (IllegalStateException e) { + return super.performAction(method, opContext); + } + } else { + return super.performAction(method, opContext); + } + return 0; case FOCUS: currentNode.performAction(AccessibilityNodeInfo.ACTION_FOCUS, null); - return true; + return 0; } return super.performAction(method, opContext); @@ -253,68 +264,22 @@ public String getXpath() { * @param content * @param opContext */ - public void performInput(final String content, final OperationContext opContext) { - Bundle textData = new Bundle(); - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - if (!StringUtil.containsChinese(content)) { - Rect rect = getNodeBound(); - opContext.executor.executeClick(rect.centerX(), rect.centerY()); - MiscUtil.sleep(500); - - opContext.executor.executeCmd("input text \"" + StringUtil.escapeShellText(content) + "\""); - - waitInputMethodHide(); - } else { - opContext.notifyOnFinish(new Runnable() { - @Override - public void run() { - LogUtil.e(TAG, "Start Input"); - try { - CmdTools.switchToIme("com.alipay.hulu/.tools.AdbIME"); - Rect rect = getNodeBound(); - - opContext.executor.executeClick(rect.centerX(), rect.centerY()); - MiscUtil.sleep(1500); - InjectorService.g().pushMessage("ADB_INPUT_TEXT", content); - } catch (Exception e) { - LogUtil.e(TAG, "Input throw Exception:" + e.getLocalizedMessage(), e); - } - LogUtil.e(TAG, "Finish Input"); - waitInputMethodHide(); - } - }); - } - } else { - try { - textData.putString(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, content); - currentNode.performAction(AccessibilityNodeInfo.ACTION_FOCUS, null); - currentNode.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, textData); - opContext.notifyOnFinish(new Runnable() { - @Override - public void run() { - waitInputMethodHide(); - } - }); - } catch (IllegalStateException e) { - LogUtil.e(TAG, "Node recycled", e); - opContext.notifyOnFinish(new Runnable() { - @Override - public void run() { - LogUtil.e(TAG, "Start Input"); - try { - CmdTools.switchToIme("com.alipay.hulu/.tools.AdbIME"); - Rect rect = getNodeBound(); - - opContext.executor.executeClick(rect.centerX(), rect.centerY()); - MiscUtil.sleep(1500); - InjectorService.g().pushMessage("ADB_INPUT_TEXT", content); - } catch (Exception e) { - LogUtil.e(TAG, "Input throw Exception:" + e.getLocalizedMessage(), e); - } - LogUtil.e(TAG, "Finish Input"); - } - }); - } + public void performInputByAccessibility(final String content, final OperationContext opContext) throws IllegalStateException { + try { + Bundle textData = new Bundle(); + textData.putString(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, content); + currentNode.performAction(AccessibilityNodeInfo.ACTION_FOCUS, null); + currentNode.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, textData); + MiscUtil.sleep(500); + opContext.notifyOnFinish(new Runnable() { + @Override + public void run() { + waitInputMethodHide(); + } + }); + } catch (IllegalStateException e) { + LogUtil.e(TAG, "Node recycled", e); + throw e; } } @@ -350,8 +315,19 @@ public StringBuilder printTrace(StringBuilder builder) { return builder; } + @Override + public JSONObject exportToJsonObject() { + JSONObject obj = super.exportToJsonObject(); + obj.put("isWebView", isWebview); + obj.put("isClickable", isClickable); + obj.put("isEditable", isEditable); + obj.put("isFocusable", isFocusable); + obj.put("isScrollable", isScrollable); + return obj; + } + public boolean isSelfUsableForLocating() { - return visible && (!StringUtil.isEmpty(text) || !StringUtil.isEmpty(description) || !StringUtil.isEmpty(resourceId)); + return visible; } public boolean isScrollable() { diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/node/tree/capture/CaptureTree.java b/src/shared/src/main/java/com/alipay/hulu/shared/node/tree/capture/CaptureTree.java index a2ab15e..b3e6773 100644 --- a/src/shared/src/main/java/com/alipay/hulu/shared/node/tree/capture/CaptureTree.java +++ b/src/shared/src/main/java/com/alipay/hulu/shared/node/tree/capture/CaptureTree.java @@ -18,19 +18,17 @@ import android.graphics.Bitmap; import android.graphics.Rect; -import com.alipay.hulu.common.injector.InjectorService; -import com.alipay.hulu.common.tools.CmdTools; -import com.alipay.hulu.common.utils.LogUtil; +import com.alibaba.fastjson.JSONObject; import com.alipay.hulu.common.utils.MiscUtil; -import com.alipay.hulu.common.utils.StringUtil; import com.alipay.hulu.shared.node.action.OperationContext; -import com.alipay.hulu.shared.node.action.OperationExecutor; import com.alipay.hulu.shared.node.action.OperationMethod; import com.alipay.hulu.shared.node.action.PerformActionEnum; import com.alipay.hulu.shared.node.tree.AbstractNodeTree; import com.alipay.hulu.shared.node.utils.BitmapUtil; import com.alipay.hulu.shared.node.utils.RectUtil; +import java.util.ArrayList; +import java.util.List; import java.util.Map; /** @@ -83,6 +81,25 @@ public String getCapture() { return BitmapUtil.bitmapToBase64(capture); } + + @Override + public JSONObject exportToJsonObject() { + JSONObject obj = new JSONObject(6); + obj.put("type", getClass().getSimpleName()); + obj.put("nodeBound", nodeBound); + if (childrenNodes != null) { + List children = new ArrayList<>(childrenNodes.size() + 1); + for (AbstractNodeTree child : getChildrenNodes()) { + JSONObject childObj = child.exportToJsonObject(); + if (childObj != null) { + children.add(childObj); + } + } + obj.put("children", children); + } + return obj; + } + /** * 设置框选区域 * @param position @@ -137,56 +154,17 @@ public int getOriginHeigth() { } @Override - public boolean performAction(OperationMethod method, OperationContext context) { - // 输入通过位置进行 - if (method.getActionEnum() == PerformActionEnum.INPUT) { - String text = method.getParam(OperationExecutor.INPUT_TEXT_KEY); - inputText(text, context); - return true; - } - - boolean result = super.performAction(method, context); + public int performAction(OperationMethod method, OperationContext context) { + int result = super.performAction(method, context); // 等1秒 - if (result) { + if (result >= 0) { MiscUtil.sleep(1000); } return result; } - /** - * 输入文字 - * @param text - * @param opContext - */ - private void inputText(final String text, final OperationContext opContext) { - opContext.notifyOnFinish(new Runnable() { - @Override - public void run() { - LogUtil.e(TAG, "Start Input"); - if (StringUtil.containsChinese(text)) { - try { - CmdTools.switchToIme("com.alipay.hulu/.tools.AdbIME"); - Rect rect = getNodeBound(); - opContext.executor.executeClick(rect.centerX(), rect.centerY()); - MiscUtil.sleep(1500); - InjectorService.g().pushMessage("ADB_INPUT_TEXT", text); - } catch (Exception e) { - LogUtil.e(TAG, "Input throw Exception:" + e.getLocalizedMessage(), e); - } - } else { - Rect rect = getNodeBound(); - opContext.executor.executeClick(rect.centerX(), rect.centerY()); - MiscUtil.sleep(1500); - opContext.executor.executeCmdSync("input text \"" + StringUtil.escapeShellText(text) + "\""); - } - LogUtil.e(TAG, "Finish Input"); - waitInputMethodHide(); - } - }); - } - public void exportExtras(Map extras) { extras.put(KEY_ORIGIN_POS, flatRectToString(getNodeBound())); extras.put(KEY_ORIGIN_SCREEN, scaleWidth + "," + scaleHeight); diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/node/utils/ContentChangeWatcher.java b/src/shared/src/main/java/com/alipay/hulu/shared/node/utils/ContentChangeWatcher.java index 742014a..aa438db 100644 --- a/src/shared/src/main/java/com/alipay/hulu/shared/node/utils/ContentChangeWatcher.java +++ b/src/shared/src/main/java/com/alipay/hulu/shared/node/utils/ContentChangeWatcher.java @@ -52,7 +52,7 @@ public void stop() { @Subscriber(@Param(value = Constant.EVENT_ACCESSIBILITY_EVENT, sticky = false)) public void onAccessibilityEvent(UniversalEventBean eventBean) { Integer eventType = eventBean.getParam(Constant.KEY_ACCESSIBILITY_TYPE); - LogUtil.d(TAG, "【ChangeWatcher】收到辅助功能事件=%d", eventType); + LogUtil.v(TAG, "【ChangeWatcher】收到辅助功能事件=%d", eventType); if (eventType != null) { switch (eventType) { case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/node/utils/NodeContext.java b/src/shared/src/main/java/com/alipay/hulu/shared/node/utils/NodeContext.java index 6e2af3c..fb8a44f 100644 --- a/src/shared/src/main/java/com/alipay/hulu/shared/node/utils/NodeContext.java +++ b/src/shared/src/main/java/com/alipay/hulu/shared/node/utils/NodeContext.java @@ -16,6 +16,7 @@ package com.alipay.hulu.shared.node.utils; import java.util.HashMap; +import java.util.Map; /** * Created by qiaoruikai on 2018/10/8 4:30 PM. @@ -30,6 +31,13 @@ public T getField(String name, T defaultValue) { return target; } + public NodeContext() { + } + + public NodeContext(Map m) { + super(m); + } + public T getField(String name) { return getField(name, null); } diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/node/utils/OperationUtil.java b/src/shared/src/main/java/com/alipay/hulu/shared/node/utils/OperationUtil.java index 71b895d..9bee0c6 100644 --- a/src/shared/src/main/java/com/alipay/hulu/shared/node/utils/OperationUtil.java +++ b/src/shared/src/main/java/com/alipay/hulu/shared/node/utils/OperationUtil.java @@ -21,6 +21,7 @@ import android.view.WindowManager; import com.alipay.hulu.common.application.LauncherApplication; +import com.alipay.hulu.common.service.SPService; import com.alipay.hulu.common.tools.CmdTools; import com.alipay.hulu.common.utils.LogUtil; import com.alipay.hulu.common.utils.MiscUtil; @@ -245,7 +246,7 @@ public static AbstractNodeTree findAbstractNode(OperationNode node, OperationSer // 两次处理弹窗 int maxCount = 2; - while (maxCount > 0) { + while (targetNode == null && maxCount > 0) { targetNode = scrollToScreen(node, service); maxCount--; @@ -262,16 +263,12 @@ public static AbstractNodeTree findAbstractNode(OperationNode node, OperationSer if (System.currentTimeMillis() - startTime < 1500) { break; } - } else { - break; + service.invalidRoot(); } - -// MiscUtil.sleep(1500); - service.invalidRoot(); } // 上滑刷新两次 - maxCount = 2; + maxCount = SPService.getInt(SPService.KEY_MAX_SCROLL_FIND_COUNT, 0); while (targetNode == null && maxCount > 0) { targetNode = scrollToScreen(node, service); maxCount--; @@ -293,7 +290,7 @@ public static AbstractNodeTree findAbstractNode(OperationNode node, OperationSer // 下滑查找四次 - maxCount = 4; + maxCount = SPService.getInt(SPService.KEY_MAX_SCROLL_FIND_COUNT, 0) * 2; while (targetNode == null && maxCount > 0) { targetNode = scrollToScreen(node, service); maxCount--; @@ -356,13 +353,8 @@ public static AbstractNodeTree findAbstractNodeWithoutScroll(OperationNode node, if (System.currentTimeMillis() - startTime < 1500) { break; } - } else { - break; + service.invalidRoot(); } - - // 不需要Sleep,处理弹窗时已经sleep过了 -// MiscUtil.sleep(1500); - service.invalidRoot(); } return targetNode; diff --git a/src/shared/src/main/res/drawable/dialog_action_drawable_keyboard.xml b/src/shared/src/main/res/drawable/dialog_action_drawable_keyboard.xml new file mode 100644 index 0000000..ffe73d1 --- /dev/null +++ b/src/shared/src/main/res/drawable/dialog_action_drawable_keyboard.xml @@ -0,0 +1,405 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/shared/src/main/res/values-zh/strings.xml b/src/shared/src/main/res/values-zh/strings.xml index 00aa7af..4808301 100644 --- a/src/shared/src/main/res/values-zh/strings.xml +++ b/src/shared/src/main/res/values-zh/strings.xml @@ -29,6 +29,7 @@ 重复点击 发现则点击 快速点击 + 点击并输入 输入并搜索 下滑 上滑 @@ -48,6 +49,8 @@ 全局上滑 全局右滑 全局左滑 + 键盘输入 + 全局输入 回到首页 清理数据 结束进程 @@ -117,13 +120,50 @@ 全局缩小 手势路径 手势操作间隔 + 强制关闭 + 结束回放 通用模式 图像查找模式 恢复 进程状态 + 温度(BETA) 生成QR码 QR码值 请在设置里\"开发人员选项\"中开启\"GPU呈现模式分析\"选项 辅助功能启动中 生成条形码 + 瞬时电流:%.1fmA/瞬时功率:%.1fmW + 数据获取失败 + 实时电流 + 平均电流 + 实时功率 + 平均功率 + CPU: %.1f°C + 应用进程- + 全局占用 + 帧率 + + 延迟次数 + + 最长延迟时间 + 延迟占比 + 帧率:%d/延迟数:%d/最长延迟:%dms/延迟占比:%.2f%%\n%s + 帧率:%d/延迟数:%d/最长延迟:%dms/延迟占比:%.2f%% + 帧率:%d/延迟数:%d/最长延迟:%dms/延迟占比:%.2f%% + 全局占用 + 可用内存:%dMB/总内存%dMB + %s:下%.1fK/累计%.1fK\n%s:上%.1fK/累计%.1fK + total:下%.1fK/累计%.1fK\ntotal:上%.1fK/累计%.1fK + 累计全局下行流量 + 全局下行速率 + 进程下行流量- + 进程下行速率- + 累计全局上行流量 + 全局上行速率 + 进程上行流量- + 进程上行速率- + 响应耗时 + 刷新耗时 + 响应耗时: %dms/刷新耗时: %dms + 应用进程- diff --git a/src/shared/src/main/res/values/strings.xml b/src/shared/src/main/res/values/strings.xml index ff08e3b..cf4c28e 100644 --- a/src/shared/src/main/res/values/strings.xml +++ b/src/shared/src/main/res/values/strings.xml @@ -29,6 +29,7 @@ REPEAT CLICK CLICK IF EXISTS QUICK CLICK + CLICK AND INPUT INPUT AND SEARCH SCROLL DOWN SCROLL UP @@ -48,6 +49,8 @@ SCROLL UP SCROLL RIGHT SCROLL LEFT + KEYBOARD INPUT + GLOBAL INPUT RESTART CLEAR DATA KILL PROCESS @@ -117,13 +120,50 @@ Pinch In Gesture path Gesture interval + Force Stop + Exit Replay Accessibility Mode Screenshot Mode Resume Process Status + Temperature(BETA) GENERATE QR CODE QR Code Value Please turn on \"Profile HWUI rendering\" in \"Developer options\" Starting Accessibility Service GENERATE BAR CODE + Instant Current:%.1fmA/Instant power:%.1fmW + Load Data Failed + Instant Current + AVG Current + Instant Power + AVG Power + CPU: %.1f°C + App Process- + Global Usage + Frame rate + Frame + Jank Count + Times + Max Jank Time + Jank Frame Percentage + Framerate:%d/Jank Count:%d/Max Jank Time:%dms/Jank Percentage:%.2f%%\n%s + Framerate:%d/Jank Count:%d/Max Jank Time:%dms/Jank Percentage:%.2f%% + Framerate:%d/Jank Count:%d/Max Jank Time:%dms/Jank Percentage:%.2f%% + Total Usage + Available:%dMB/Total:%dMB + %s:Down%.1fK/Accum%.1fK\n%s:Up%.1fK/Accum%.1fK + Total:Down%.1fK/Accum%.1fK\nTotal:Up%.1fK/Accum%.1fK + Global Download Flow + Global Download Speed + Process Download Flow- + Process Download Speed- + Global Upload Flow + Global Upload Speed + Process Upload Flow- + Process Upload Speed- + Response Time + Refresh Time + Response Time: %dms/Refresh Time: %dms + App Process-