From 1023e1733d7486bee7b875b34edc07f39c9b710d Mon Sep 17 00:00:00 2001 From: SuperMonster003 Date: Wed, 15 Jan 2025 00:12:07 +0800 Subject: [PATCH] =?UTF-8?q?6.6.2=20-=20Alpha2=20-=20=E5=B0=9D=E8=AF=95?= =?UTF-8?q?=E6=81=A2=E5=A4=8D=20com.stardust=20=E5=89=8D=E7=BC=80=E5=8C=85?= =?UTF-8?q?=E4=BB=A5=E4=BE=BF=E6=8F=90=E5=8D=87=E4=BB=A3=E7=A0=81=E5=85=BC?= =?UTF-8?q?=E5=AE=B9=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changelog/lang_zh-Hans.json | 2 +- .../apksig/internal/asn1/Asn1BerParser.java | 1 + app/proguard-rules.pro | 14 +- app/src/main/java/com/stardust/app/AppOps.kt | 16 + .../java/com/stardust/app/DialogUtils.java | 54 ++ .../main/java/com/stardust/app/Fragment.java | 48 + .../app/FragmentPagerAdapterBuilder.java | 94 ++ .../com/stardust/app/GlobalAppContext.java | 92 ++ .../main/java/com/stardust/app/MenuUtils.java | 24 + .../app/OnActivityResultDelegate.java | 53 ++ .../app/SimpleActivityLifecycleCallbacks.java | 47 + .../main/java/com/stardust/autojs/AutoJs.java | 267 ++++++ .../java/com/stardust/autojs/BuildConfig.kt | 3 + .../main/java/com/stardust/autojs/Config.java | 41 + .../stardust/autojs/ScriptEngineService.java | 286 ++++++ .../autojs/ScriptEngineServiceBuilder.java | 43 + .../autojs/annotation/ScriptClass.java | 14 + .../autojs/annotation/ScriptInterface.java | 14 + .../autojs/annotation/ScriptVariable.java | 15 + .../autojs/codegeneration/CodeGenerator.java | 205 +++++ .../codegeneration/ReadOnlyUiObject.java | 332 +++++++ .../codegeneration/UiSelectorGenerator.java | 133 +++ .../accessibility/AccessibilityBridge.java | 152 ++++ .../AccessibilityServiceUsher.kt | 27 + .../accessibility/SimpleActionAutomator.kt | 261 ++++++ .../autojs/core/accessibility/UiSelector.kt | 372 ++++++++ .../core/activity/ActivityInfoProvider.kt | 199 +++++ .../autojs/core/boardcast/Broadcast.java | 27 + .../core/boardcast/BroadcastEmitter.java | 31 + .../autojs/core/console/ConsoleFloaty.java | 133 +++ .../autojs/core/console/ConsoleImpl.java | 298 +++++++ .../autojs/core/console/ConsoleView.java | 238 +++++ .../autojs/core/console/GlobalConsole.java | 74 ++ .../autojs/core/console/NoOpConsole.java | 72 ++ .../autojs/core/database/CursorHelper.java | 24 + .../autojs/core/database/Database.java | 90 ++ .../core/database/DatabaseResultSet.java | 61 ++ .../core/database/DatabaseVoidCallback.java | 5 + .../autojs/core/database/Databases.java | 11 + .../core/database/StatementCallback.java | 8 + .../core/database/StatementErrorCallback.java | 9 + .../autojs/core/database/Transaction.java | 32 + .../core/database/TransactionCallback.java | 5 + .../database/TransactionErrorCallback.java | 8 + .../autojs/core/eventloop/EventEmitter.java | 200 +++++ .../autojs/core/eventloop/SimpleEvent.java | 6 + .../floaty/BaseResizableFloatyWindow.java | 160 ++++ .../autojs/core/floaty/RawWindow.java | 106 +++ .../autojs/core/graphics/ScriptCanvas.java | 469 ++++++++++ .../autojs/core/graphics/ScriptCanvasView.kt | 178 ++++ .../autojs/core/http/MutableOkHttp.java | 90 ++ .../autojs/core/image/ColorDetector.java | 199 +++++ .../autojs/core/image/ColorFinder.java | 133 +++ .../stardust/autojs/core/image/Colors.java | 90 ++ .../autojs/core/image/ImageWrapper.java | 170 ++++ .../autojs/core/image/TemplateMatching.java | 250 ++++++ .../capture/ScreenCaptureRequestActivity.java | 68 ++ .../image/capture/ScreenCaptureRequester.java | 82 ++ .../core/image/capture/ScreenCapturer.java | 218 +++++ .../autojs/core/inputevent/InputDevices.java | 39 + .../core/inputevent/InputEventCodes.java | 837 ++++++++++++++++++ .../core/inputevent/InputEventObserver.java | 143 +++ .../core/inputevent/KeyEventDispatcher.java | 54 ++ .../autojs/core/inputevent/RootAutomator.java | 308 +++++++ .../core/inputevent/ShellKeyObserver.java | 88 ++ .../autojs/core/inputevent/TouchObserver.java | 89 ++ .../autojs/core/internal/Functions.java | 66 ++ .../autojs/core/looper/LooperHelper.java | 30 + .../stardust/autojs/core/looper/Loopers.java | 215 +++++ .../autojs/core/looper/MainThreadProxy.java | 224 +++++ .../stardust/autojs/core/looper/Timer.java | 134 +++ .../autojs/core/looper/TimerThread.java | 128 +++ .../com/stardust/autojs/core/opencv/Mat.java | 110 +++ .../autojs/core/opencv/MatOfPoint.java | 56 ++ .../autojs/core/opencv/OpenCVHelper.java | 67 ++ .../OnRequestPermissionsResultCallback.java | 8 + .../permission/PermissionRequestActivity.java | 32 + .../PermissionRequestProxyActivity.java | 16 + .../autojs/core/permission/Permissions.java | 51 ++ .../RequestPermissionCallbacks.java | 32 + .../stardust/autojs/core/plugin/Plugin.java | 93 ++ .../com/stardust/autojs/core/pref/Pref.kt | 20 + .../stardust/autojs/core/record/Recorder.java | 181 ++++ .../AccessibilityActionConverter.java | 144 +++ .../AccessibilityActionRecorder.java | 92 ++ .../inputevent/EventFormatException.java | 25 + .../record/inputevent/InputEventRecorder.java | 60 ++ .../InputEventToAutoFileRecorder.java | 139 +++ .../InputEventToRootAutomatorRecorder.java | 81 ++ .../InputEventToSendEventJsRecorder.java | 109 +++ .../core/record/inputevent/TouchRecorder.java | 72 ++ .../autojs/core/storage/LocalStorage.java | 75 ++ .../stardust/autojs/core/ui/BaseEvent.java | 53 ++ .../stardust/autojs/core/ui/JsViewHelper.java | 32 + .../stardust/autojs/core/ui/ViewExtras.java | 77 ++ .../core/ui/attribute/AppbarAttributes.java | 24 + .../core/ui/attribute/CardAttributes.java | 72 ++ .../core/ui/attribute/FabViewAttributes.java | 31 + .../ui/attribute/ImageViewAttributes.java | 80 ++ .../core/ui/attribute/TextViewAttributes.java | 23 + .../ui/attribute/ViewAttributeDelegate.java | 21 + .../core/ui/attribute/ViewAttributes.java | 571 ++++++++++++ .../ui/attribute/ViewAttributesFactory.java | 47 + .../core/ui/dialog/BlockedMaterialDialog.java | 187 ++++ .../autojs/core/ui/dialog/JsDialog.java | 812 +++++++++++++++++ .../core/ui/dialog/JsDialogBuilder.java | 153 ++++ .../ui/inflater/DynamicLayoutInflater.java | 361 ++++++++ .../autojs/core/ui/inflater/ImageLoader.java | 33 + .../core/ui/inflater/InflateContext.java | 32 + .../ui/inflater/LayoutInflaterDelegate.java | 136 +++ .../core/ui/inflater/ResourceParser.java | 21 + .../inflater/ShouldCallOnFinishInflate.java | 6 + .../autojs/core/ui/inflater/ViewCreator.java | 16 + .../autojs/core/ui/inflater/ViewInflater.java | 29 + .../ui/inflater/inflaters/AppBarInflater.java | 42 + .../inflater/inflaters/BaseViewInflater.java | 653 ++++++++++++++ .../inflaters/CanvasViewInflater.java | 28 + .../inflaters/DatePickerInflater.java | 98 ++ .../ui/inflater/inflaters/Exceptions.java | 36 + .../inflaters/FrameLayoutInflater.java | 45 + .../inflater/inflaters/ImageViewInflater.java | 108 +++ .../inflaters/JsGridViewInflater.java | 46 + .../inflaters/JsImageViewInflater.java | 84 ++ .../inflaters/JsListViewInflater.java | 68 ++ .../inflaters/LinearLayoutInflater.java | 64 ++ .../ui/inflater/inflaters/MapAttrbuteSet.java | 138 +++ .../inflaters/ProgressBarInflater.java | 123 +++ .../inflaters/RadioGroupInflater.java | 40 + .../inflater/inflaters/SpinnerInflater.java | 95 ++ .../inflater/inflaters/TabLayoutInflater.java | 68 ++ .../inflater/inflaters/TextViewInflater.java | 428 +++++++++ .../inflaters/TimePickerInflater.java | 32 + .../inflater/inflaters/ToolbarInflater.java | 89 ++ .../inflater/inflaters/ViewGroupInflater.java | 85 ++ .../autojs/core/ui/inflater/util/Colors.java | 28 + .../core/ui/inflater/util/Dimensions.java | 92 ++ .../core/ui/inflater/util/Drawables.java | 180 ++++ .../core/ui/inflater/util/Gravities.java | 46 + .../autojs/core/ui/inflater/util/Ids.java | 33 + .../autojs/core/ui/inflater/util/Res.java | 22 + .../autojs/core/ui/inflater/util/Strings.java | 26 + .../core/ui/inflater/util/ValueMapper.java | 61 ++ .../autojs/core/ui/nativeview/NativeView.java | 87 ++ .../core/ui/nativeview/ViewPrototype.java | 253 ++++++ .../autojs/core/ui/widget/JsButton.java | 40 + .../autojs/core/ui/widget/JsEditText.java | 42 + .../autojs/core/ui/widget/JsFrameLayout.java | 41 + .../autojs/core/ui/widget/JsGridView.java | 34 + .../autojs/core/ui/widget/JsImageView.java | 70 ++ .../autojs/core/ui/widget/JsLinearLayout.java | 41 + .../autojs/core/ui/widget/JsListView.java | 188 ++++ .../core/ui/widget/JsRelativeLayout.java | 37 + .../autojs/core/ui/widget/JsSpinner.java | 162 ++++ .../autojs/core/ui/widget/JsTabLayout.java | 21 + .../autojs/core/ui/widget/JsTextView.java | 36 + .../autojs/core/ui/widget/JsToolbar.java | 47 + .../autojs/core/ui/widget/JsViewPager.java | 67 ++ .../autojs/core/ui/widget/JsWebView.java | 52 ++ .../autojs/core/ui/xml/AttributeHandler.java | 188 ++++ .../autojs/core/ui/xml/NodeHandler.java | 91 ++ .../autojs/core/ui/xml/XmlConverter.java | 166 ++++ .../autojs/core/util/ProcessShell.java | 239 +++++ .../autojs/core/util/ScriptPromiseAdapter.kt | 49 + .../com/stardust/autojs/core/util/Shell.java | 321 +++++++ .../autojs/core/web/InjectableWebClient.java | 138 +++ .../autojs/core/web/InjectableWebView.java | 39 + .../autojs/engine/JavaScriptEngine.java | 71 ++ .../engine/LoopBasedJavaScriptEngine.java | 102 +++ .../autojs/engine/RhinoJavaScriptEngine.kt | 193 ++++ .../autojs/engine/RootAutomatorEngine.java | 161 ++++ .../stardust/autojs/engine/ScriptEngine.java | 137 +++ .../autojs/engine/ScriptEngineFactory.java | 89 ++ .../autojs/engine/ScriptEngineManager.java | 138 +++ .../autojs/engine/ScriptEngineProxy.java | 90 ++ .../engine/encryption/ScriptEncryption.kt | 14 + .../AssetAndUrlModuleSourceProvider.java | 66 ++ .../module/UrlModuleSourceProvider.java | 364 ++++++++ .../engine/preprocess/AbstractProcessor.java | 31 + .../preprocess/MultiLinePreprocessor.java | 97 ++ .../engine/preprocess/Preprocessor.java | 13 + .../autojs/execution/ExecutionConfig.kt | 99 +++ .../LoopedBasedJavaScriptExecution.java | 51 ++ .../execution/RunnableScriptExecution.java | 111 +++ .../execution/ScriptExecuteActivity.java | 263 ++++++ .../autojs/execution/ScriptExecution.java | 76 ++ .../execution/ScriptExecutionListener.java | 16 + .../execution/ScriptExecutionObserver.java | 70 ++ .../autojs/execution/ScriptExecutionTask.java | 38 + .../SimpleScriptExecutionListener.java | 23 + .../stardust/autojs/project/BuildInfo.java | 59 ++ .../stardust/autojs/project/LaunchConfig.java | 22 + .../autojs/project/ProjectConfig.java | 251 ++++++ .../autojs/project/ProjectLauncher.java | 28 + .../stardust/autojs/project/ScriptConfig.kt | 18 + .../autojs/rhino/AndroidClassLoader.java | 14 + .../autojs/rhino/AndroidContextFactory.java | 54 ++ .../stardust/autojs/rhino/AutoJsContext.kt | 27 + .../autojs/rhino/BindingNativeJavaObject.java | 30 + .../InterruptibleAndroidContextFactory.java | 55 ++ .../rhino/NativeJavaClassWithPrototype.java | 90 ++ .../rhino/NativeJavaObjectWithPrototype.java | 52 ++ .../autojs/rhino/NoSecurityController.java | 23 + .../autojs/rhino/ProxyJavaObject.java | 70 ++ .../stardust/autojs/rhino/ProxyObject.java | 57 ++ .../autojs/rhino/RhinoAndroidHelper.java | 93 ++ .../stardust/autojs/rhino/TokenStream.java | 15 + .../stardust/autojs/rhino/TopLevelScope.java | 6 + .../autojs/rhino/continuation/Continuation.kt | 59 ++ .../autojs/rhino/debug/DebugCallback.java | 17 + .../rhino/debug/DebugCallbackInternal.java | 49 + .../stardust/autojs/rhino/debug/Debugger.java | 10 + .../com/stardust/autojs/rhino/debug/Dim.java | 5 + .../autojs/runtime/ScriptBridges.java | 58 ++ .../autojs/runtime/ScriptRuntime.java | 491 ++++++++++ .../accessibility/AccessibilityConfig.java | 51 ++ .../autojs/runtime/api/AbstractConsole.java | 67 ++ .../autojs/runtime/api/AbstractShell.java | 208 +++++ .../stardust/autojs/runtime/api/AppUtils.java | 140 +++ .../stardust/autojs/runtime/api/Console.java | 47 + .../stardust/autojs/runtime/api/Device.java | 394 +++++++++ .../stardust/autojs/runtime/api/Dialogs.java | 181 ++++ .../stardust/autojs/runtime/api/Engines.java | 61 ++ .../stardust/autojs/runtime/api/Events.java | 353 ++++++++ .../stardust/autojs/runtime/api/Files.java | 205 +++++ .../stardust/autojs/runtime/api/Floaty.java | 314 +++++++ .../stardust/autojs/runtime/api/Images.java | 368 ++++++++ .../stardust/autojs/runtime/api/Media.java | 193 ++++ .../stardust/autojs/runtime/api/Plugins.java | 49 + .../stardust/autojs/runtime/api/Sensors.java | 171 ++++ .../stardust/autojs/runtime/api/Threads.java | 111 +++ .../stardust/autojs/runtime/api/Timers.java | 95 ++ .../com/stardust/autojs/runtime/api/UI.java | 119 +++ .../exception/ScriptEnvironmentException.java | 11 + .../runtime/exception/ScriptException.java | 28 + .../exception/ScriptInterruptedException.java | 28 + .../autojs/script/AutoFileSource.java | 39 + .../script/EncryptedScriptFileHeader.kt | 47 + .../autojs/script/JavaScriptFileSource.java | 75 ++ .../autojs/script/JavaScriptSource.java | 118 +++ .../stardust/autojs/script/JsBeautifier.java | 129 +++ .../stardust/autojs/script/ScriptSource.java | 22 + .../autojs/script/SequenceScriptSource.java | 54 ++ .../autojs/script/StringScriptSource.java | 37 + .../autojs/util/FloatingPermission.java | 107 +++ .../stardust/autojs/util/ProcessUtils.java | 29 + .../WrapContentLinearLayoutManager.java | 36 + .../com/stardust/automator/ActionArgument.kt | 34 + .../com/stardust/automator/BuildConfig.kt | 3 + .../automator/GlobalActionAutomator.kt | 171 ++++ .../stardust/automator/UiGlobalSelector.kt | 440 +++++++++ .../java/com/stardust/automator/UiObject.kt | 420 +++++++++ .../stardust/automator/UiObjectCollection.kt | 263 ++++++ .../automator/filter/BooleanFilter.kt | 234 +++++ .../stardust/automator/filter/BoundsFilter.kt | 39 + .../automator/filter/ClassNameFilters.kt | 45 + .../stardust/automator/filter/DescFilters.kt | 41 + .../com/stardust/automator/filter/Filter.kt | 13 + .../com/stardust/automator/filter/IdFilter.kt | 42 + .../stardust/automator/filter/IntFilter.kt | 105 +++ .../stardust/automator/filter/KeyGetter.kt | 12 + .../automator/filter/PackageNameFilter.kt | 41 + .../com/stardust/automator/filter/Selector.kt | 33 + .../automator/filter/StringContainsFilter.kt | 19 + .../automator/filter/StringEndsWithFilter.kt | 19 + .../automator/filter/StringEqualsFilter.kt | 21 + .../automator/filter/StringMatchesFilter.kt | 19 + .../filter/StringStartsWithFilter.kt | 19 + .../stardust/automator/filter/TextFilters.kt | 41 + .../java/com/stardust/automator/search/BFS.kt | 35 + .../java/com/stardust/automator/search/DFS.kt | 40 + .../automator/search/SearchAlgorithm.kt | 9 + .../stardust/automator/simple_action/Able.kt | 46 + .../automator/simple_action/ActionFactory.kt | 69 ++ .../automator/simple_action/ActionTarget.kt | 43 + .../DepthFirstSearchTargetAction.kt | 28 + .../automator/simple_action/FilterAction.kt | 142 +++ .../automator/simple_action/ScrollAction.kt | 53 ++ .../simple_action/ScrollMaxAction.kt | 82 ++ .../simple_action/SearchTargetAction.kt | 38 + .../simple_action/SearchUpTargetAction.kt | 38 + .../automator/simple_action/SimpleAction.kt | 18 + .../stardust/automator/test/TestUiObject.kt | 85 ++ .../concurrent/ConcurrentArrayList.java | 62 ++ .../java/com/stardust/concurrent/Value.java | 27 + .../com/stardust/concurrent/VolatileBox.java | 72 ++ .../stardust/concurrent/VolatileDispose.java | 75 ++ .../com/stardust/event/EventDispatcher.java | 32 + app/src/main/java/com/stardust/ext/Common.kt | 13 + .../io/ByteBufferBackedInputStream.kt | 30 + .../io/ByteBufferBackedOutputStream.kt | 19 + .../java/com/stardust/io/ConcatReader.java | 424 +++++++++ app/src/main/java/com/stardust/io/EFile.java | 29 + app/src/main/java/com/stardust/io/Zip.java | 49 + .../java/com/stardust/lang/ThreadCompat.java | 69 ++ .../stardust/net/AutoHttpURLConnection.java | 62 ++ .../com/stardust/notification/Notification.kt | 96 ++ .../NotificationListenerService.kt | 59 ++ app/src/main/java/com/stardust/pio/PFile.java | 139 +++ .../java/com/stardust/pio/PFileInterface.java | 9 + .../main/java/com/stardust/pio/PFiles.java | 525 +++++++++++ .../stardust/pio/PRandomAccessBinaryFile.java | 36 + .../com/stardust/pio/PReadableBinaryFile.java | 17 + .../com/stardust/pio/PReadableTextFile.java | 113 +++ .../com/stardust/pio/PWritableTextFile.java | 125 +++ .../stardust/pio/UncheckedIOException.java | 19 + .../util/AdvancedEncryptionStandard.kt | 42 + app/src/main/java/com/stardust/util/Array.kt | 9 + .../java/com/stardust/util/ArrayUtils.java | 46 + .../java/com/stardust/util/AssetsCache.java | 30 + .../com/stardust/util/BackPressedHandler.java | 85 ++ .../main/java/com/stardust/util/BiMap.java | 16 + .../main/java/com/stardust/util/BiMaps.java | 218 +++++ .../main/java/com/stardust/util/Callback.java | 10 + .../java/com/stardust/util/ClipboardUtil.java | 34 + .../main/java/com/stardust/util/Consumer.java | 13 + .../com/stardust/util/DeveloperUtils.java | 199 +++++ .../com/stardust/util/DrawerAutoClose.java | 28 + .../java/com/stardust/util/FileSorter.java | 86 ++ .../main/java/com/stardust/util/Func1.java | 11 + .../java/com/stardust/util/HashUtils.java | 25 + .../java/com/stardust/util/IntentExtras.java | 107 +++ .../java/com/stardust/util/IntentUtil.java | 199 +++++ .../com/stardust/util/LimitedHashMap.java | 24 + app/src/main/java/com/stardust/util/MD5.java | 32 + .../java/com/stardust/util/MapBuilder.java | 37 + .../java/com/stardust/util/MessageEvent.java | 22 + .../java/com/stardust/util/MessageIntent.java | 51 ++ .../java/com/stardust/util/MimeTypes.java | 26 + app/src/main/java/com/stardust/util/Nath.java | 16 + .../java/com/stardust/util/NetworkUtils.java | 20 + .../java/com/stardust/util/ObjectHelper.java | 18 + .../main/java/com/stardust/util/Objects.java | 40 + .../com/stardust/util/ResourceMonitor.java | 122 +++ .../java/com/stardust/util/ScreenMetrics.java | 130 +++ .../com/stardust/util/SdkVersionUtil.java | 24 + .../java/com/stardust/util/SimpleCache.java | 118 +++ .../com/stardust/util/SparseArrayEntries.java | 24 + .../java/com/stardust/util/StateObserver.java | 93 ++ .../main/java/com/stardust/util/Supplier.java | 9 + .../java/com/stardust/util/TextUtils.java | 149 ++++ .../java/com/stardust/util/UiHandler.java | 43 + .../com/stardust/util/UnderuseExecutors.java | 22 + .../main/java/com/stardust/util/ViewUtil.java | 50 ++ .../java/com/stardust/util/ViewUtils.java | 29 + .../accessibility/AccessibilityDelegate.kt | 27 + .../AccessibilityNodeInfoAllocator.kt | 183 ++++ .../AccessibilityNodeInfoHelper.kt | 65 ++ .../AccessibilityNotificationObserver.kt | 118 +++ .../accessibility/AccessibilityService.kt | 174 ++++ .../AccessibilityServiceUtils.kt | 44 + .../view/accessibility/KeyInterceptor.kt | 41 + .../accessibility/LayoutInspectService.kt | 25 + .../view/accessibility/LayoutInspector.kt | 87 ++ .../stardust/view/accessibility/NodeInfo.kt | 188 ++++ .../accessibility/NotificationListener.kt | 25 + .../view/accessibility/OnKeyListener.kt | 45 + .../autojs/autojs/rhino/AndroidClassLoader.kt | 2 +- .../org/autojs/autojs/runtime/api/AppUtils.kt | 6 +- .../org/autojs/autojs/runtime/api/Plugins.kt | 2 +- .../runtime/api/augment/automator/Auto.kt | 2 + .../autojs/runtime/api/augment/s13n/S13n.kt | 1 - .../runtime/api/augment/util/Inspect.kt | 6 +- .../exception/WrappedRuntimeException.kt | 1 + .../ResizableExpandableFloatyWindow.kt | 2 +- version.properties | 6 +- 365 files changed, 34340 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/com/stardust/app/AppOps.kt create mode 100644 app/src/main/java/com/stardust/app/DialogUtils.java create mode 100644 app/src/main/java/com/stardust/app/Fragment.java create mode 100644 app/src/main/java/com/stardust/app/FragmentPagerAdapterBuilder.java create mode 100644 app/src/main/java/com/stardust/app/GlobalAppContext.java create mode 100644 app/src/main/java/com/stardust/app/MenuUtils.java create mode 100644 app/src/main/java/com/stardust/app/OnActivityResultDelegate.java create mode 100644 app/src/main/java/com/stardust/app/SimpleActivityLifecycleCallbacks.java create mode 100644 app/src/main/java/com/stardust/autojs/AutoJs.java create mode 100644 app/src/main/java/com/stardust/autojs/BuildConfig.kt create mode 100644 app/src/main/java/com/stardust/autojs/Config.java create mode 100644 app/src/main/java/com/stardust/autojs/ScriptEngineService.java create mode 100644 app/src/main/java/com/stardust/autojs/ScriptEngineServiceBuilder.java create mode 100644 app/src/main/java/com/stardust/autojs/annotation/ScriptClass.java create mode 100644 app/src/main/java/com/stardust/autojs/annotation/ScriptInterface.java create mode 100644 app/src/main/java/com/stardust/autojs/annotation/ScriptVariable.java create mode 100644 app/src/main/java/com/stardust/autojs/codegeneration/CodeGenerator.java create mode 100644 app/src/main/java/com/stardust/autojs/codegeneration/ReadOnlyUiObject.java create mode 100644 app/src/main/java/com/stardust/autojs/codegeneration/UiSelectorGenerator.java create mode 100644 app/src/main/java/com/stardust/autojs/core/accessibility/AccessibilityBridge.java create mode 100644 app/src/main/java/com/stardust/autojs/core/accessibility/AccessibilityServiceUsher.kt create mode 100644 app/src/main/java/com/stardust/autojs/core/accessibility/SimpleActionAutomator.kt create mode 100644 app/src/main/java/com/stardust/autojs/core/accessibility/UiSelector.kt create mode 100644 app/src/main/java/com/stardust/autojs/core/activity/ActivityInfoProvider.kt create mode 100644 app/src/main/java/com/stardust/autojs/core/boardcast/Broadcast.java create mode 100644 app/src/main/java/com/stardust/autojs/core/boardcast/BroadcastEmitter.java create mode 100644 app/src/main/java/com/stardust/autojs/core/console/ConsoleFloaty.java create mode 100644 app/src/main/java/com/stardust/autojs/core/console/ConsoleImpl.java create mode 100644 app/src/main/java/com/stardust/autojs/core/console/ConsoleView.java create mode 100644 app/src/main/java/com/stardust/autojs/core/console/GlobalConsole.java create mode 100644 app/src/main/java/com/stardust/autojs/core/console/NoOpConsole.java create mode 100644 app/src/main/java/com/stardust/autojs/core/database/CursorHelper.java create mode 100644 app/src/main/java/com/stardust/autojs/core/database/Database.java create mode 100644 app/src/main/java/com/stardust/autojs/core/database/DatabaseResultSet.java create mode 100644 app/src/main/java/com/stardust/autojs/core/database/DatabaseVoidCallback.java create mode 100644 app/src/main/java/com/stardust/autojs/core/database/Databases.java create mode 100644 app/src/main/java/com/stardust/autojs/core/database/StatementCallback.java create mode 100644 app/src/main/java/com/stardust/autojs/core/database/StatementErrorCallback.java create mode 100644 app/src/main/java/com/stardust/autojs/core/database/Transaction.java create mode 100644 app/src/main/java/com/stardust/autojs/core/database/TransactionCallback.java create mode 100644 app/src/main/java/com/stardust/autojs/core/database/TransactionErrorCallback.java create mode 100644 app/src/main/java/com/stardust/autojs/core/eventloop/EventEmitter.java create mode 100644 app/src/main/java/com/stardust/autojs/core/eventloop/SimpleEvent.java create mode 100644 app/src/main/java/com/stardust/autojs/core/floaty/BaseResizableFloatyWindow.java create mode 100644 app/src/main/java/com/stardust/autojs/core/floaty/RawWindow.java create mode 100644 app/src/main/java/com/stardust/autojs/core/graphics/ScriptCanvas.java create mode 100644 app/src/main/java/com/stardust/autojs/core/graphics/ScriptCanvasView.kt create mode 100644 app/src/main/java/com/stardust/autojs/core/http/MutableOkHttp.java create mode 100644 app/src/main/java/com/stardust/autojs/core/image/ColorDetector.java create mode 100644 app/src/main/java/com/stardust/autojs/core/image/ColorFinder.java create mode 100644 app/src/main/java/com/stardust/autojs/core/image/Colors.java create mode 100644 app/src/main/java/com/stardust/autojs/core/image/ImageWrapper.java create mode 100644 app/src/main/java/com/stardust/autojs/core/image/TemplateMatching.java create mode 100644 app/src/main/java/com/stardust/autojs/core/image/capture/ScreenCaptureRequestActivity.java create mode 100644 app/src/main/java/com/stardust/autojs/core/image/capture/ScreenCaptureRequester.java create mode 100644 app/src/main/java/com/stardust/autojs/core/image/capture/ScreenCapturer.java create mode 100644 app/src/main/java/com/stardust/autojs/core/inputevent/InputDevices.java create mode 100644 app/src/main/java/com/stardust/autojs/core/inputevent/InputEventCodes.java create mode 100644 app/src/main/java/com/stardust/autojs/core/inputevent/InputEventObserver.java create mode 100644 app/src/main/java/com/stardust/autojs/core/inputevent/KeyEventDispatcher.java create mode 100644 app/src/main/java/com/stardust/autojs/core/inputevent/RootAutomator.java create mode 100644 app/src/main/java/com/stardust/autojs/core/inputevent/ShellKeyObserver.java create mode 100644 app/src/main/java/com/stardust/autojs/core/inputevent/TouchObserver.java create mode 100644 app/src/main/java/com/stardust/autojs/core/internal/Functions.java create mode 100644 app/src/main/java/com/stardust/autojs/core/looper/LooperHelper.java create mode 100644 app/src/main/java/com/stardust/autojs/core/looper/Loopers.java create mode 100644 app/src/main/java/com/stardust/autojs/core/looper/MainThreadProxy.java create mode 100644 app/src/main/java/com/stardust/autojs/core/looper/Timer.java create mode 100644 app/src/main/java/com/stardust/autojs/core/looper/TimerThread.java create mode 100644 app/src/main/java/com/stardust/autojs/core/opencv/Mat.java create mode 100644 app/src/main/java/com/stardust/autojs/core/opencv/MatOfPoint.java create mode 100644 app/src/main/java/com/stardust/autojs/core/opencv/OpenCVHelper.java create mode 100644 app/src/main/java/com/stardust/autojs/core/permission/OnRequestPermissionsResultCallback.java create mode 100644 app/src/main/java/com/stardust/autojs/core/permission/PermissionRequestActivity.java create mode 100644 app/src/main/java/com/stardust/autojs/core/permission/PermissionRequestProxyActivity.java create mode 100644 app/src/main/java/com/stardust/autojs/core/permission/Permissions.java create mode 100644 app/src/main/java/com/stardust/autojs/core/permission/RequestPermissionCallbacks.java create mode 100644 app/src/main/java/com/stardust/autojs/core/plugin/Plugin.java create mode 100644 app/src/main/java/com/stardust/autojs/core/pref/Pref.kt create mode 100644 app/src/main/java/com/stardust/autojs/core/record/Recorder.java create mode 100644 app/src/main/java/com/stardust/autojs/core/record/accessibility/AccessibilityActionConverter.java create mode 100644 app/src/main/java/com/stardust/autojs/core/record/accessibility/AccessibilityActionRecorder.java create mode 100644 app/src/main/java/com/stardust/autojs/core/record/inputevent/EventFormatException.java create mode 100644 app/src/main/java/com/stardust/autojs/core/record/inputevent/InputEventRecorder.java create mode 100644 app/src/main/java/com/stardust/autojs/core/record/inputevent/InputEventToAutoFileRecorder.java create mode 100644 app/src/main/java/com/stardust/autojs/core/record/inputevent/InputEventToRootAutomatorRecorder.java create mode 100644 app/src/main/java/com/stardust/autojs/core/record/inputevent/InputEventToSendEventJsRecorder.java create mode 100644 app/src/main/java/com/stardust/autojs/core/record/inputevent/TouchRecorder.java create mode 100644 app/src/main/java/com/stardust/autojs/core/storage/LocalStorage.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/BaseEvent.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/JsViewHelper.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/ViewExtras.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/attribute/AppbarAttributes.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/attribute/CardAttributes.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/attribute/FabViewAttributes.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/attribute/ImageViewAttributes.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/attribute/TextViewAttributes.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/attribute/ViewAttributeDelegate.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/attribute/ViewAttributes.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/attribute/ViewAttributesFactory.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/dialog/BlockedMaterialDialog.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/dialog/JsDialog.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/dialog/JsDialogBuilder.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/inflater/DynamicLayoutInflater.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/inflater/ImageLoader.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/inflater/InflateContext.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/inflater/LayoutInflaterDelegate.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/inflater/ResourceParser.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/inflater/ShouldCallOnFinishInflate.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/inflater/ViewCreator.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/inflater/ViewInflater.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/AppBarInflater.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/BaseViewInflater.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/CanvasViewInflater.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/DatePickerInflater.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/Exceptions.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/FrameLayoutInflater.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/ImageViewInflater.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/JsGridViewInflater.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/JsImageViewInflater.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/JsListViewInflater.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/LinearLayoutInflater.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/MapAttrbuteSet.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/ProgressBarInflater.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/RadioGroupInflater.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/SpinnerInflater.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/TabLayoutInflater.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/TextViewInflater.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/TimePickerInflater.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/ToolbarInflater.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/ViewGroupInflater.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/inflater/util/Colors.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/inflater/util/Dimensions.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/inflater/util/Drawables.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/inflater/util/Gravities.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/inflater/util/Ids.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/inflater/util/Res.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/inflater/util/Strings.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/inflater/util/ValueMapper.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/nativeview/NativeView.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/nativeview/ViewPrototype.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/widget/JsButton.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/widget/JsEditText.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/widget/JsFrameLayout.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/widget/JsGridView.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/widget/JsImageView.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/widget/JsLinearLayout.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/widget/JsListView.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/widget/JsRelativeLayout.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/widget/JsSpinner.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/widget/JsTabLayout.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/widget/JsTextView.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/widget/JsToolbar.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/widget/JsViewPager.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/widget/JsWebView.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/xml/AttributeHandler.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/xml/NodeHandler.java create mode 100644 app/src/main/java/com/stardust/autojs/core/ui/xml/XmlConverter.java create mode 100644 app/src/main/java/com/stardust/autojs/core/util/ProcessShell.java create mode 100644 app/src/main/java/com/stardust/autojs/core/util/ScriptPromiseAdapter.kt create mode 100644 app/src/main/java/com/stardust/autojs/core/util/Shell.java create mode 100644 app/src/main/java/com/stardust/autojs/core/web/InjectableWebClient.java create mode 100644 app/src/main/java/com/stardust/autojs/core/web/InjectableWebView.java create mode 100644 app/src/main/java/com/stardust/autojs/engine/JavaScriptEngine.java create mode 100644 app/src/main/java/com/stardust/autojs/engine/LoopBasedJavaScriptEngine.java create mode 100644 app/src/main/java/com/stardust/autojs/engine/RhinoJavaScriptEngine.kt create mode 100644 app/src/main/java/com/stardust/autojs/engine/RootAutomatorEngine.java create mode 100644 app/src/main/java/com/stardust/autojs/engine/ScriptEngine.java create mode 100644 app/src/main/java/com/stardust/autojs/engine/ScriptEngineFactory.java create mode 100644 app/src/main/java/com/stardust/autojs/engine/ScriptEngineManager.java create mode 100644 app/src/main/java/com/stardust/autojs/engine/ScriptEngineProxy.java create mode 100644 app/src/main/java/com/stardust/autojs/engine/encryption/ScriptEncryption.kt create mode 100644 app/src/main/java/com/stardust/autojs/engine/module/AssetAndUrlModuleSourceProvider.java create mode 100644 app/src/main/java/com/stardust/autojs/engine/module/UrlModuleSourceProvider.java create mode 100644 app/src/main/java/com/stardust/autojs/engine/preprocess/AbstractProcessor.java create mode 100644 app/src/main/java/com/stardust/autojs/engine/preprocess/MultiLinePreprocessor.java create mode 100644 app/src/main/java/com/stardust/autojs/engine/preprocess/Preprocessor.java create mode 100644 app/src/main/java/com/stardust/autojs/execution/ExecutionConfig.kt create mode 100644 app/src/main/java/com/stardust/autojs/execution/LoopedBasedJavaScriptExecution.java create mode 100644 app/src/main/java/com/stardust/autojs/execution/RunnableScriptExecution.java create mode 100644 app/src/main/java/com/stardust/autojs/execution/ScriptExecuteActivity.java create mode 100644 app/src/main/java/com/stardust/autojs/execution/ScriptExecution.java create mode 100644 app/src/main/java/com/stardust/autojs/execution/ScriptExecutionListener.java create mode 100644 app/src/main/java/com/stardust/autojs/execution/ScriptExecutionObserver.java create mode 100644 app/src/main/java/com/stardust/autojs/execution/ScriptExecutionTask.java create mode 100644 app/src/main/java/com/stardust/autojs/execution/SimpleScriptExecutionListener.java create mode 100644 app/src/main/java/com/stardust/autojs/project/BuildInfo.java create mode 100644 app/src/main/java/com/stardust/autojs/project/LaunchConfig.java create mode 100644 app/src/main/java/com/stardust/autojs/project/ProjectConfig.java create mode 100644 app/src/main/java/com/stardust/autojs/project/ProjectLauncher.java create mode 100644 app/src/main/java/com/stardust/autojs/project/ScriptConfig.kt create mode 100644 app/src/main/java/com/stardust/autojs/rhino/AndroidClassLoader.java create mode 100644 app/src/main/java/com/stardust/autojs/rhino/AndroidContextFactory.java create mode 100644 app/src/main/java/com/stardust/autojs/rhino/AutoJsContext.kt create mode 100644 app/src/main/java/com/stardust/autojs/rhino/BindingNativeJavaObject.java create mode 100644 app/src/main/java/com/stardust/autojs/rhino/InterruptibleAndroidContextFactory.java create mode 100644 app/src/main/java/com/stardust/autojs/rhino/NativeJavaClassWithPrototype.java create mode 100644 app/src/main/java/com/stardust/autojs/rhino/NativeJavaObjectWithPrototype.java create mode 100644 app/src/main/java/com/stardust/autojs/rhino/NoSecurityController.java create mode 100644 app/src/main/java/com/stardust/autojs/rhino/ProxyJavaObject.java create mode 100644 app/src/main/java/com/stardust/autojs/rhino/ProxyObject.java create mode 100644 app/src/main/java/com/stardust/autojs/rhino/RhinoAndroidHelper.java create mode 100644 app/src/main/java/com/stardust/autojs/rhino/TokenStream.java create mode 100644 app/src/main/java/com/stardust/autojs/rhino/TopLevelScope.java create mode 100644 app/src/main/java/com/stardust/autojs/rhino/continuation/Continuation.kt create mode 100644 app/src/main/java/com/stardust/autojs/rhino/debug/DebugCallback.java create mode 100644 app/src/main/java/com/stardust/autojs/rhino/debug/DebugCallbackInternal.java create mode 100644 app/src/main/java/com/stardust/autojs/rhino/debug/Debugger.java create mode 100644 app/src/main/java/com/stardust/autojs/rhino/debug/Dim.java create mode 100644 app/src/main/java/com/stardust/autojs/runtime/ScriptBridges.java create mode 100644 app/src/main/java/com/stardust/autojs/runtime/ScriptRuntime.java create mode 100644 app/src/main/java/com/stardust/autojs/runtime/accessibility/AccessibilityConfig.java create mode 100644 app/src/main/java/com/stardust/autojs/runtime/api/AbstractConsole.java create mode 100644 app/src/main/java/com/stardust/autojs/runtime/api/AbstractShell.java create mode 100644 app/src/main/java/com/stardust/autojs/runtime/api/AppUtils.java create mode 100644 app/src/main/java/com/stardust/autojs/runtime/api/Console.java create mode 100644 app/src/main/java/com/stardust/autojs/runtime/api/Device.java create mode 100644 app/src/main/java/com/stardust/autojs/runtime/api/Dialogs.java create mode 100644 app/src/main/java/com/stardust/autojs/runtime/api/Engines.java create mode 100644 app/src/main/java/com/stardust/autojs/runtime/api/Events.java create mode 100644 app/src/main/java/com/stardust/autojs/runtime/api/Files.java create mode 100644 app/src/main/java/com/stardust/autojs/runtime/api/Floaty.java create mode 100644 app/src/main/java/com/stardust/autojs/runtime/api/Images.java create mode 100644 app/src/main/java/com/stardust/autojs/runtime/api/Media.java create mode 100644 app/src/main/java/com/stardust/autojs/runtime/api/Plugins.java create mode 100644 app/src/main/java/com/stardust/autojs/runtime/api/Sensors.java create mode 100644 app/src/main/java/com/stardust/autojs/runtime/api/Threads.java create mode 100644 app/src/main/java/com/stardust/autojs/runtime/api/Timers.java create mode 100644 app/src/main/java/com/stardust/autojs/runtime/api/UI.java create mode 100644 app/src/main/java/com/stardust/autojs/runtime/exception/ScriptEnvironmentException.java create mode 100644 app/src/main/java/com/stardust/autojs/runtime/exception/ScriptException.java create mode 100644 app/src/main/java/com/stardust/autojs/runtime/exception/ScriptInterruptedException.java create mode 100644 app/src/main/java/com/stardust/autojs/script/AutoFileSource.java create mode 100644 app/src/main/java/com/stardust/autojs/script/EncryptedScriptFileHeader.kt create mode 100644 app/src/main/java/com/stardust/autojs/script/JavaScriptFileSource.java create mode 100644 app/src/main/java/com/stardust/autojs/script/JavaScriptSource.java create mode 100644 app/src/main/java/com/stardust/autojs/script/JsBeautifier.java create mode 100644 app/src/main/java/com/stardust/autojs/script/ScriptSource.java create mode 100644 app/src/main/java/com/stardust/autojs/script/SequenceScriptSource.java create mode 100644 app/src/main/java/com/stardust/autojs/script/StringScriptSource.java create mode 100644 app/src/main/java/com/stardust/autojs/util/FloatingPermission.java create mode 100644 app/src/main/java/com/stardust/autojs/util/ProcessUtils.java create mode 100644 app/src/main/java/com/stardust/autojs/workground/WrapContentLinearLayoutManager.java create mode 100644 app/src/main/java/com/stardust/automator/ActionArgument.kt create mode 100644 app/src/main/java/com/stardust/automator/BuildConfig.kt create mode 100644 app/src/main/java/com/stardust/automator/GlobalActionAutomator.kt create mode 100644 app/src/main/java/com/stardust/automator/UiGlobalSelector.kt create mode 100644 app/src/main/java/com/stardust/automator/UiObject.kt create mode 100644 app/src/main/java/com/stardust/automator/UiObjectCollection.kt create mode 100644 app/src/main/java/com/stardust/automator/filter/BooleanFilter.kt create mode 100644 app/src/main/java/com/stardust/automator/filter/BoundsFilter.kt create mode 100644 app/src/main/java/com/stardust/automator/filter/ClassNameFilters.kt create mode 100644 app/src/main/java/com/stardust/automator/filter/DescFilters.kt create mode 100644 app/src/main/java/com/stardust/automator/filter/Filter.kt create mode 100644 app/src/main/java/com/stardust/automator/filter/IdFilter.kt create mode 100644 app/src/main/java/com/stardust/automator/filter/IntFilter.kt create mode 100644 app/src/main/java/com/stardust/automator/filter/KeyGetter.kt create mode 100644 app/src/main/java/com/stardust/automator/filter/PackageNameFilter.kt create mode 100644 app/src/main/java/com/stardust/automator/filter/Selector.kt create mode 100644 app/src/main/java/com/stardust/automator/filter/StringContainsFilter.kt create mode 100644 app/src/main/java/com/stardust/automator/filter/StringEndsWithFilter.kt create mode 100644 app/src/main/java/com/stardust/automator/filter/StringEqualsFilter.kt create mode 100644 app/src/main/java/com/stardust/automator/filter/StringMatchesFilter.kt create mode 100644 app/src/main/java/com/stardust/automator/filter/StringStartsWithFilter.kt create mode 100644 app/src/main/java/com/stardust/automator/filter/TextFilters.kt create mode 100644 app/src/main/java/com/stardust/automator/search/BFS.kt create mode 100644 app/src/main/java/com/stardust/automator/search/DFS.kt create mode 100644 app/src/main/java/com/stardust/automator/search/SearchAlgorithm.kt create mode 100644 app/src/main/java/com/stardust/automator/simple_action/Able.kt create mode 100644 app/src/main/java/com/stardust/automator/simple_action/ActionFactory.kt create mode 100644 app/src/main/java/com/stardust/automator/simple_action/ActionTarget.kt create mode 100644 app/src/main/java/com/stardust/automator/simple_action/DepthFirstSearchTargetAction.kt create mode 100644 app/src/main/java/com/stardust/automator/simple_action/FilterAction.kt create mode 100644 app/src/main/java/com/stardust/automator/simple_action/ScrollAction.kt create mode 100644 app/src/main/java/com/stardust/automator/simple_action/ScrollMaxAction.kt create mode 100644 app/src/main/java/com/stardust/automator/simple_action/SearchTargetAction.kt create mode 100644 app/src/main/java/com/stardust/automator/simple_action/SearchUpTargetAction.kt create mode 100644 app/src/main/java/com/stardust/automator/simple_action/SimpleAction.kt create mode 100644 app/src/main/java/com/stardust/automator/test/TestUiObject.kt create mode 100644 app/src/main/java/com/stardust/concurrent/ConcurrentArrayList.java create mode 100644 app/src/main/java/com/stardust/concurrent/Value.java create mode 100644 app/src/main/java/com/stardust/concurrent/VolatileBox.java create mode 100644 app/src/main/java/com/stardust/concurrent/VolatileDispose.java create mode 100644 app/src/main/java/com/stardust/event/EventDispatcher.java create mode 100644 app/src/main/java/com/stardust/ext/Common.kt create mode 100644 app/src/main/java/com/stardust/io/ByteBufferBackedInputStream.kt create mode 100644 app/src/main/java/com/stardust/io/ByteBufferBackedOutputStream.kt create mode 100644 app/src/main/java/com/stardust/io/ConcatReader.java create mode 100644 app/src/main/java/com/stardust/io/EFile.java create mode 100644 app/src/main/java/com/stardust/io/Zip.java create mode 100644 app/src/main/java/com/stardust/lang/ThreadCompat.java create mode 100644 app/src/main/java/com/stardust/net/AutoHttpURLConnection.java create mode 100644 app/src/main/java/com/stardust/notification/Notification.kt create mode 100644 app/src/main/java/com/stardust/notification/NotificationListenerService.kt create mode 100644 app/src/main/java/com/stardust/pio/PFile.java create mode 100644 app/src/main/java/com/stardust/pio/PFileInterface.java create mode 100644 app/src/main/java/com/stardust/pio/PFiles.java create mode 100644 app/src/main/java/com/stardust/pio/PRandomAccessBinaryFile.java create mode 100644 app/src/main/java/com/stardust/pio/PReadableBinaryFile.java create mode 100644 app/src/main/java/com/stardust/pio/PReadableTextFile.java create mode 100644 app/src/main/java/com/stardust/pio/PWritableTextFile.java create mode 100644 app/src/main/java/com/stardust/pio/UncheckedIOException.java create mode 100644 app/src/main/java/com/stardust/util/AdvancedEncryptionStandard.kt create mode 100644 app/src/main/java/com/stardust/util/Array.kt create mode 100644 app/src/main/java/com/stardust/util/ArrayUtils.java create mode 100644 app/src/main/java/com/stardust/util/AssetsCache.java create mode 100644 app/src/main/java/com/stardust/util/BackPressedHandler.java create mode 100644 app/src/main/java/com/stardust/util/BiMap.java create mode 100644 app/src/main/java/com/stardust/util/BiMaps.java create mode 100644 app/src/main/java/com/stardust/util/Callback.java create mode 100644 app/src/main/java/com/stardust/util/ClipboardUtil.java create mode 100644 app/src/main/java/com/stardust/util/Consumer.java create mode 100644 app/src/main/java/com/stardust/util/DeveloperUtils.java create mode 100644 app/src/main/java/com/stardust/util/DrawerAutoClose.java create mode 100644 app/src/main/java/com/stardust/util/FileSorter.java create mode 100644 app/src/main/java/com/stardust/util/Func1.java create mode 100644 app/src/main/java/com/stardust/util/HashUtils.java create mode 100644 app/src/main/java/com/stardust/util/IntentExtras.java create mode 100644 app/src/main/java/com/stardust/util/IntentUtil.java create mode 100644 app/src/main/java/com/stardust/util/LimitedHashMap.java create mode 100644 app/src/main/java/com/stardust/util/MD5.java create mode 100644 app/src/main/java/com/stardust/util/MapBuilder.java create mode 100644 app/src/main/java/com/stardust/util/MessageEvent.java create mode 100644 app/src/main/java/com/stardust/util/MessageIntent.java create mode 100644 app/src/main/java/com/stardust/util/MimeTypes.java create mode 100644 app/src/main/java/com/stardust/util/Nath.java create mode 100644 app/src/main/java/com/stardust/util/NetworkUtils.java create mode 100644 app/src/main/java/com/stardust/util/ObjectHelper.java create mode 100644 app/src/main/java/com/stardust/util/Objects.java create mode 100644 app/src/main/java/com/stardust/util/ResourceMonitor.java create mode 100644 app/src/main/java/com/stardust/util/ScreenMetrics.java create mode 100644 app/src/main/java/com/stardust/util/SdkVersionUtil.java create mode 100644 app/src/main/java/com/stardust/util/SimpleCache.java create mode 100644 app/src/main/java/com/stardust/util/SparseArrayEntries.java create mode 100644 app/src/main/java/com/stardust/util/StateObserver.java create mode 100644 app/src/main/java/com/stardust/util/Supplier.java create mode 100644 app/src/main/java/com/stardust/util/TextUtils.java create mode 100644 app/src/main/java/com/stardust/util/UiHandler.java create mode 100644 app/src/main/java/com/stardust/util/UnderuseExecutors.java create mode 100644 app/src/main/java/com/stardust/util/ViewUtil.java create mode 100644 app/src/main/java/com/stardust/util/ViewUtils.java create mode 100644 app/src/main/java/com/stardust/view/accessibility/AccessibilityDelegate.kt create mode 100644 app/src/main/java/com/stardust/view/accessibility/AccessibilityNodeInfoAllocator.kt create mode 100644 app/src/main/java/com/stardust/view/accessibility/AccessibilityNodeInfoHelper.kt create mode 100644 app/src/main/java/com/stardust/view/accessibility/AccessibilityNotificationObserver.kt create mode 100644 app/src/main/java/com/stardust/view/accessibility/AccessibilityService.kt create mode 100644 app/src/main/java/com/stardust/view/accessibility/AccessibilityServiceUtils.kt create mode 100644 app/src/main/java/com/stardust/view/accessibility/KeyInterceptor.kt create mode 100644 app/src/main/java/com/stardust/view/accessibility/LayoutInspectService.kt create mode 100644 app/src/main/java/com/stardust/view/accessibility/LayoutInspector.kt create mode 100644 app/src/main/java/com/stardust/view/accessibility/NodeInfo.kt create mode 100644 app/src/main/java/com/stardust/view/accessibility/NotificationListener.kt create mode 100644 app/src/main/java/com/stardust/view/accessibility/OnKeyListener.kt diff --git a/.changelog/lang_zh-Hans.json b/.changelog/lang_zh-Hans.json index 99764aa6..fb20a9ac 100644 --- a/.changelog/lang_zh-Hans.json +++ b/.changelog/lang_zh-Hans.json @@ -4,7 +4,6 @@ "released_date": "2025/01/03", "fix": [ "plugins.load 方法无法正常加载插件的问题 _[`issue #290`](http://issues.autojs6.com/290)_", - "dex 文件可能因权限问题无法正常加载的问题 (试修) _[`issue #290`](http://issues.autojs6.com/290)_", "dx 库在 Android 7.x 无法正常使用的问题 _[`issue #293`](http://issues.autojs6.com/293)_", "ScriptRuntime 使用 require 引用内置模块时可能出现的同步状态异常 (试修) _[`issue #298`](http://issues.autojs6.com/298)_", "notice 模块缺失 getBuilder 等扩展方法的问题 _[`issue #301`](http://issues.autojs6.com/301)_", @@ -15,6 +14,7 @@ "打包页面支持 Pinyin 库选项", "APK 文件类型信息对话框增加文件大小与签名方案信息", "APK 文件类型信息对话框增加点击监听器支持文本复制与应用详情跳转", + "尝试恢复 com.stardust 前缀包以便提升代码兼容性 _[`issue #290`](http://issues.autojs6.com/290)_", "floaty.window/floaty.rawWindow 同时支持主线程和子线程执行" ] }, diff --git a/apksigner/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java b/apksigner/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java index 160dc4e2..8711bd69 100644 --- a/apksigner/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java +++ b/apksigner/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java @@ -37,6 +37,7 @@ import java.util.List; *

Structure is described to the parser by providing a class annotated with {@link Asn1Class}, * containing fields annotated with {@link Asn1Field}. */ +@SuppressWarnings("deprecation") public final class Asn1BerParser { private Asn1BerParser() {} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index c1bce132..77263b79 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -53,8 +53,18 @@ -keep class org.mozilla.javascript.** { *; } -keep class org.autojs.autojs.core.automator.** { *; } -keep class org.autojs.autojs.** { *; } + +-keep class com.stardust.automator.** { *; } +-keep class com.stardust.autojs.** { *; } +-dontwarn com.stardust.** + -keepattributes *Annotation*,SourceFile,LineNumberTable +-keepclassmembers class ** { + @org.autojs.autojs.annotation.ScriptInterface ; + @com.stardust.autojs.annotation.ScriptInterface ; +} + # Event bus -keep class org.greenrobot.eventbus.** { *; } @@ -65,10 +75,6 @@ -keep enum org.greenrobot.eventbus.ThreadMode { *; } --keepclassmembers class ** { - @org.autojs.autojs.annotation.ScriptInterface ; -} - # gson -keep class * extends org.json.JSONObject { diff --git a/app/src/main/java/com/stardust/app/AppOps.kt b/app/src/main/java/com/stardust/app/AppOps.kt new file mode 100644 index 00000000..7d3d070d --- /dev/null +++ b/app/src/main/java/com/stardust/app/AppOps.kt @@ -0,0 +1,16 @@ +package com.stardust.app + +import android.app.AppOpsManager +import android.content.Context +import android.content.pm.PackageManager + +fun Context.isOpPermissionGranted(permission: String): Boolean { + val appOps = getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager + @Suppress("DEPRECATION") val mode = appOps.checkOpNoThrow(permission, android.os.Process.myUid(), packageName) + + return if (mode == AppOpsManager.MODE_DEFAULT) { + checkCallingOrSelfPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) == PackageManager.PERMISSION_GRANTED + } else { + mode == AppOpsManager.MODE_ALLOWED + } +} \ No newline at end of file diff --git a/app/src/main/java/com/stardust/app/DialogUtils.java b/app/src/main/java/com/stardust/app/DialogUtils.java new file mode 100644 index 00000000..aba1bf97 --- /dev/null +++ b/app/src/main/java/com/stardust/app/DialogUtils.java @@ -0,0 +1,54 @@ +package com.stardust.app; + +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.ContextWrapper; +import android.os.Build; +import android.os.Looper; +import android.view.Window; +import android.view.WindowManager; + +/** + * Created by Stardust on 2017/8/4. + */ + +public class DialogUtils { + + public static T showDialog(final T dialog) { + Context context = dialog.getContext(); + + if (!isActivityContext(context)) { + Window window = dialog.getWindow(); + int type; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + } else { + type = WindowManager.LayoutParams.TYPE_PHONE; + } + if (window != null) + window.setType(type); + } + if (Looper.getMainLooper() == Looper.myLooper()) { + dialog.show(); + } else { + GlobalAppContext.post(new Runnable() { + @Override + public void run() { + dialog.show(); + } + }); + } + return dialog; + } + + + public static boolean isActivityContext(Context context) { + if (context instanceof Activity) + return true; + if (context instanceof ContextWrapper) { + return isActivityContext(((ContextWrapper) context).getBaseContext()); + } + return false; + } +} diff --git a/app/src/main/java/com/stardust/app/Fragment.java b/app/src/main/java/com/stardust/app/Fragment.java new file mode 100644 index 00000000..d6adbe22 --- /dev/null +++ b/app/src/main/java/com/stardust/app/Fragment.java @@ -0,0 +1,48 @@ +package com.stardust.app; + +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.stardust.util.ViewUtil; + +/** + * Created by Stardust on 2017/1/30. + */ + +public abstract class Fragment extends androidx.fragment.app.Fragment { + + private View mView; + + @NonNull + public View getView() { + return mView; + } + + public T $(int id) { + return ViewUtil.$(mView, id); + } + + public View findViewById(int id) { + return mView.findViewById(id); + } + + public View getActivityContentView() { + return getActivity().getWindow().getDecorView(); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + mView = createView(inflater, container, savedInstanceState); + return mView; + } + + @Nullable + public abstract View createView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState); + + +} diff --git a/app/src/main/java/com/stardust/app/FragmentPagerAdapterBuilder.java b/app/src/main/java/com/stardust/app/FragmentPagerAdapterBuilder.java new file mode 100644 index 00000000..68412685 --- /dev/null +++ b/app/src/main/java/com/stardust/app/FragmentPagerAdapterBuilder.java @@ -0,0 +1,94 @@ +package com.stardust.app; + +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentPagerAdapter; +import android.util.SparseArray; +import android.view.ViewGroup; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Stardust on 2017/3/24. + */ + +public class FragmentPagerAdapterBuilder { + + public interface OnFragmentInstantiateListener { + void OnInstantiate(int pos, Fragment fragment); + } + + private List mFragments = new ArrayList<>(); + private List mTitles = new ArrayList<>(); + private FragmentActivity mActivity; + + public FragmentPagerAdapterBuilder(FragmentActivity activity) { + mActivity = activity; + } + + public FragmentPagerAdapterBuilder add(Fragment fragment, String title) { + mFragments.add(fragment); + mTitles.add(title); + return this; + } + + public FragmentPagerAdapterBuilder add(Fragment fragment, int titleResId) { + return add(fragment, mActivity.getString(titleResId)); + } + + public StoredFragmentPagerAdapter build() { + return new StoredFragmentPagerAdapter(mActivity.getSupportFragmentManager()) { + @Override + public Fragment getItem(int position) { + return mFragments.get(position); + } + + @Override + public int getCount() { + return mFragments.size(); + } + + @Override + public CharSequence getPageTitle(int position) { + return mTitles.get(position); + } + }; + } + + public abstract static class StoredFragmentPagerAdapter extends FragmentPagerAdapter { + + private SparseArray mStoredFragments = new SparseArray<>(); + private OnFragmentInstantiateListener mOnFragmentInstantiateListener; + + public StoredFragmentPagerAdapter(FragmentManager fm) { + super(fm); + } + + @Override + public Object instantiateItem(ViewGroup container, int position) { + Fragment fragment = (Fragment) super.instantiateItem(container, position); + mStoredFragments.put(position, fragment); + if(mOnFragmentInstantiateListener != null){ + mOnFragmentInstantiateListener.OnInstantiate(position, fragment); + } + return fragment; + } + + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + mStoredFragments.remove(position); + super.destroyItem(container, position, object); + } + + public Fragment getStoredFragment(int position) { + return mStoredFragments.get(position); + } + + public void setOnFragmentInstantiateListener(OnFragmentInstantiateListener onFragmentInstantiateListener) { + mOnFragmentInstantiateListener = onFragmentInstantiateListener; + } + } +} diff --git a/app/src/main/java/com/stardust/app/GlobalAppContext.java b/app/src/main/java/com/stardust/app/GlobalAppContext.java new file mode 100644 index 00000000..4b5391a8 --- /dev/null +++ b/app/src/main/java/com/stardust/app/GlobalAppContext.java @@ -0,0 +1,92 @@ +package com.stardust.app; + +import android.annotation.SuppressLint; +import android.app.Application; +import android.content.Context; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import androidx.annotation.RequiresApi; +import android.widget.Toast; + +/** + * Created by Stardust on 2018/3/22. + */ + +public class GlobalAppContext { + + @SuppressLint("StaticFieldLeak") + private static Context sApplicationContext; + private static Handler sHandler; + + public static void set(Application a) { + sHandler = new Handler(Looper.getMainLooper()); + sApplicationContext = a.getApplicationContext(); + } + + public static Context get() { + if (sApplicationContext == null) + throw new IllegalStateException("Call GlobalAppContext.set() to set a application context"); + return sApplicationContext; + } + + public static String getString(int resId) { + return get().getString(resId); + } + + public static String getString(int resId, Object... formatArgs) { + return get().getString(resId, formatArgs); + } + + @RequiresApi(api = Build.VERSION_CODES.M) + public static int getColor(int id) { + return get().getColor(id); + } + + public static void toast(final String message) { + if (Looper.myLooper() == Looper.getMainLooper()) { + Toast.makeText(get(), message, Toast.LENGTH_SHORT).show(); + return; + } + sHandler.post(new Runnable() { + @Override + public void run() { + Toast.makeText(get(), message, Toast.LENGTH_SHORT).show(); + } + }); + } + + public static void toast(final int resId) { + if (Looper.myLooper() == Looper.getMainLooper()) { + Toast.makeText(get(), resId, Toast.LENGTH_SHORT).show(); + return; + } + sHandler.post(new Runnable() { + @Override + public void run() { + Toast.makeText(get(), resId, Toast.LENGTH_SHORT).show(); + } + }); + } + + public static void toast(final int resId, final Object... args) { + if (Looper.myLooper() == Looper.getMainLooper()) { + Toast.makeText(get(), getString(resId, args), Toast.LENGTH_SHORT).show(); + return; + } + sHandler.post(new Runnable() { + @Override + public void run() { + Toast.makeText(get(), getString(resId, args), Toast.LENGTH_SHORT).show(); + } + }); + } + + public static void post(Runnable r) { + sHandler.post(r); + } + + public static void postDelayed(Runnable r, long m) { + sHandler.postDelayed(r, m); + } +} diff --git a/app/src/main/java/com/stardust/app/MenuUtils.java b/app/src/main/java/com/stardust/app/MenuUtils.java new file mode 100644 index 00000000..6b590a53 --- /dev/null +++ b/app/src/main/java/com/stardust/app/MenuUtils.java @@ -0,0 +1,24 @@ +package com.stardust.app; + +import android.graphics.drawable.Drawable; +import androidx.core.graphics.drawable.DrawableCompat; +import android.view.Menu; +import android.view.MenuItem; +import android.view.SubMenu; + +public class MenuUtils { + + public static void setMenuIconColor(Menu menu, int color) { + for (int i = 0; i < menu.size(); i++) { + MenuItem item = menu.getItem(i); + Drawable icon = item.getIcon(); + if (icon != null) { + DrawableCompat.setTint(icon, color); + } + SubMenu subMenu = item.getSubMenu(); + if (subMenu != null) { + setMenuIconColor(subMenu, color); + } + } + } +} diff --git a/app/src/main/java/com/stardust/app/OnActivityResultDelegate.java b/app/src/main/java/com/stardust/app/OnActivityResultDelegate.java new file mode 100644 index 00000000..a8017895 --- /dev/null +++ b/app/src/main/java/com/stardust/app/OnActivityResultDelegate.java @@ -0,0 +1,53 @@ +package com.stardust.app; + +import android.content.Intent; +import androidx.annotation.NonNull; +import android.util.SparseArray; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Stardust on 2017/3/5. + */ + +public interface OnActivityResultDelegate { + + void onActivityResult(int requestCode, int resultCode, Intent data); + + interface DelegateHost { + @NonNull + Mediator getOnActivityResultDelegateMediator(); + } + + class Mediator implements OnActivityResultDelegate { + + private SparseArray mSpecialDelegate = new SparseArray<>(); + private List mDelegates = new ArrayList<>(); + + public void onActivityResult(int requestCode, int resultCode, Intent data) { + OnActivityResultDelegate delegate = mSpecialDelegate.get(requestCode); + if (delegate != null) { + delegate.onActivityResult(requestCode, resultCode, data); + } + for (OnActivityResultDelegate d : mDelegates) { + d.onActivityResult(requestCode, resultCode, data); + } + } + + public void addDelegate(OnActivityResultDelegate delegate) { + mDelegates.add(delegate); + } + + public void addDelegate(int requestCode, OnActivityResultDelegate delegate) { + mSpecialDelegate.put(requestCode, delegate); + } + + public void removeDelegate(OnActivityResultDelegate delegate) { + if (mDelegates.remove(delegate)) { + mSpecialDelegate.removeAt(mSpecialDelegate.indexOfValue(delegate)); + } + } + } + +} diff --git a/app/src/main/java/com/stardust/app/SimpleActivityLifecycleCallbacks.java b/app/src/main/java/com/stardust/app/SimpleActivityLifecycleCallbacks.java new file mode 100644 index 00000000..ddf1e8fb --- /dev/null +++ b/app/src/main/java/com/stardust/app/SimpleActivityLifecycleCallbacks.java @@ -0,0 +1,47 @@ +package com.stardust.app; + +import android.app.Activity; +import android.app.Application; +import android.os.Bundle; + +/** + * Created by Stardust on 2017/4/2. + */ + +public class SimpleActivityLifecycleCallbacks implements Application.ActivityLifecycleCallbacks { + + @Override + public void onActivityCreated(Activity activity, Bundle savedInstanceState) { + + } + + @Override + public void onActivityStarted(Activity activity) { + + } + + @Override + public void onActivityResumed(Activity activity) { + + } + + @Override + public void onActivityPaused(Activity activity) { + + } + + @Override + public void onActivityStopped(Activity activity) { + + } + + @Override + public void onActivitySaveInstanceState(Activity activity, Bundle outState) { + + } + + @Override + public void onActivityDestroyed(Activity activity) { + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/stardust/autojs/AutoJs.java b/app/src/main/java/com/stardust/autojs/AutoJs.java new file mode 100644 index 00000000..d65b7780 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/AutoJs.java @@ -0,0 +1,267 @@ +package com.stardust.autojs; + +import android.app.Activity; +import android.app.Application; +import android.content.Context; +import android.os.Build; +import android.os.Bundle; + +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; + +import com.stardust.app.OnActivityResultDelegate; +import com.stardust.app.SimpleActivityLifecycleCallbacks; +import com.stardust.autojs.core.accessibility.AccessibilityBridge; +import com.stardust.autojs.core.console.GlobalConsole; +import com.stardust.autojs.core.console.ConsoleImpl; +import com.stardust.autojs.core.image.capture.ScreenCaptureRequestActivity; +import com.stardust.autojs.core.image.capture.ScreenCaptureRequester; +import com.stardust.autojs.core.record.accessibility.AccessibilityActionRecorder; +import com.stardust.autojs.core.util.Shell; +import com.stardust.autojs.engine.LoopBasedJavaScriptEngine; +import com.stardust.autojs.engine.RootAutomatorEngine; +import com.stardust.autojs.engine.ScriptEngineManager; +import com.stardust.autojs.rhino.InterruptibleAndroidContextFactory; +import com.stardust.autojs.runtime.ScriptRuntime; +import com.stardust.autojs.runtime.accessibility.AccessibilityConfig; +import com.stardust.autojs.runtime.api.AppUtils; +import com.stardust.autojs.script.AutoFileSource; +import com.stardust.autojs.script.JavaScriptSource; +import com.stardust.util.ResourceMonitor; +import com.stardust.util.ScreenMetrics; +import com.stardust.util.UiHandler; +import com.stardust.autojs.core.activity.ActivityInfoProvider; +import com.stardust.view.accessibility.AccessibilityNotificationObserver; +import com.stardust.view.accessibility.AccessibilityService; +import com.stardust.view.accessibility.LayoutInspector; + +import org.mozilla.javascript.ContextFactory; +import org.mozilla.javascript.WrappedException; + +import java.io.File; + +/** + * Created by Stardust on 2017/11/29. + */ + +public abstract class AutoJs { + + private final AccessibilityActionRecorder mAccessibilityActionRecorder = new AccessibilityActionRecorder(); + private final AccessibilityNotificationObserver mNotificationObserver; + private ScriptEngineManager mScriptEngineManager; + private final LayoutInspector mLayoutInspector; + private final Context mContext; + private final Application mApplication; + private final UiHandler mUiHandler; + private final AppUtils mAppUtils; + private final ActivityInfoProvider mActivityInfoProvider; + private final ScreenCaptureRequester mScreenCaptureRequester = new ScreenCaptureRequesterImpl(); + private final ScriptEngineService mScriptEngineService; + private final GlobalConsole mGlobalConsole; + + + protected AutoJs(final Application application) { + mContext = application.getApplicationContext(); + mApplication = application; + mLayoutInspector = new LayoutInspector(mContext); + mUiHandler = new UiHandler(mContext); + mAppUtils = createAppUtils(mContext); + mGlobalConsole = createGlobalConsole(); + mNotificationObserver = new AccessibilityNotificationObserver(mContext); + mActivityInfoProvider = new ActivityInfoProvider(mContext); + mScriptEngineService = buildScriptEngineService(); + ScriptEngineService.setInstance(mScriptEngineService); + init(); + } + + protected AppUtils createAppUtils(Context context) { + return new AppUtils(mContext); + } + + protected GlobalConsole createGlobalConsole() { + return new GlobalConsole(mUiHandler); + } + + protected void init() { + addAccessibilityServiceDelegates(); + registerActivityLifecycleCallbacks(); + ResourceMonitor.setExceptionCreator(resource -> { + Exception exception; + if (org.mozilla.javascript.Context.getCurrentContext() != null) { + exception = new WrappedException(new ResourceMonitor.UnclosedResourceException(resource)); + } else { + exception = new ResourceMonitor.UnclosedResourceException(resource); + } + exception.fillInStackTrace(); + return exception; + }); + ResourceMonitor.setUnclosedResourceDetectedHandler(detectedException -> mGlobalConsole.error(detectedException)); + } + + public abstract void ensureAccessibilityServiceEnabled(); + + protected Application getApplication() { + return mApplication; + } + + public ScriptEngineManager getScriptEngineManager() { + return mScriptEngineManager; + } + + protected ScriptEngineService buildScriptEngineService() { + initScriptEngineManager(); + return new ScriptEngineServiceBuilder() + .uiHandler(mUiHandler) + .globalConsole(mGlobalConsole) + .engineManger(mScriptEngineManager) + .build(); + } + + protected void initScriptEngineManager() { + mScriptEngineManager = new ScriptEngineManager(mContext); + mScriptEngineManager.registerEngine(JavaScriptSource.ENGINE, () -> { + LoopBasedJavaScriptEngine engine = new LoopBasedJavaScriptEngine(mContext); + engine.setRuntime(createRuntime()); + return engine; + }); + initContextFactory(); + mScriptEngineManager.registerEngine(AutoFileSource.ENGINE, () -> new RootAutomatorEngine(mContext)); + } + + protected void initContextFactory() { + ContextFactory.initGlobal(new InterruptibleAndroidContextFactory(new File(mContext.getCacheDir(), "classes"))); + } + + protected ScriptRuntime createRuntime() { + return new ScriptRuntime.Builder() + .setConsole(new ConsoleImpl(mUiHandler, mGlobalConsole)) + .setScreenCaptureRequester(mScreenCaptureRequester) + .setAccessibilityBridge(new AccessibilityBridgeImpl(mUiHandler)) + .setUiHandler(mUiHandler) + .setAppUtils(mAppUtils) + .setEngineService(mScriptEngineService) + .setShellSupplier(() -> new Shell(mContext, true)).build(); + } + + protected void registerActivityLifecycleCallbacks() { + getApplication().registerActivityLifecycleCallbacks(new SimpleActivityLifecycleCallbacks() { + + @Override + public void onActivityCreated(Activity activity, Bundle savedInstanceState) { + ScreenMetrics.initIfNeeded(activity); + mAppUtils.setCurrentActivity(activity); + } + + @Override + public void onActivityPaused(Activity activity) { + mAppUtils.setCurrentActivity(null); + } + + @Override + public void onActivityResumed(Activity activity) { + mAppUtils.setCurrentActivity(activity); + } + }); + } + + + private void addAccessibilityServiceDelegates() { + AccessibilityService.Companion.addDelegate(100, mActivityInfoProvider); + AccessibilityService.Companion.addDelegate(200, mNotificationObserver); + AccessibilityService.Companion.addDelegate(300, mAccessibilityActionRecorder); + } + + public AccessibilityActionRecorder getAccessibilityActionRecorder() { + return mAccessibilityActionRecorder; + } + + public AppUtils getAppUtils() { + return mAppUtils; + } + + public UiHandler getUiHandler() { + return mUiHandler; + } + + public LayoutInspector getLayoutInspector() { + return mLayoutInspector; + } + + public GlobalConsole getGlobalConsole() { + return mGlobalConsole; + } + + public ScriptEngineService getScriptEngineService() { + return mScriptEngineService; + } + + public ActivityInfoProvider getInfoProvider() { + return mActivityInfoProvider; + } + + + public abstract void waitForAccessibilityServiceEnabled(); + + protected AccessibilityConfig createAccessibilityConfig() { + return new AccessibilityConfig(); + } + + private class AccessibilityBridgeImpl extends AccessibilityBridge { + + public AccessibilityBridgeImpl(UiHandler uiHandler) { + super(mContext, createAccessibilityConfig(), uiHandler); + } + + @Override + public void ensureServiceEnabled() { + AutoJs.this.ensureAccessibilityServiceEnabled(); + } + + @Override + public void waitForServiceEnabled() { + AutoJs.this.waitForAccessibilityServiceEnabled(); + } + + @Nullable + @Override + public AccessibilityService getService() { + return AccessibilityService.Companion.getInstance(); + } + + @Override + public ActivityInfoProvider getInfoProvider() { + return mActivityInfoProvider; + } + + @Override + public AccessibilityNotificationObserver getNotificationObserver() { + return mNotificationObserver; + } + + } + + private class ScreenCaptureRequesterImpl extends ScreenCaptureRequester.AbstractScreenCaptureRequester { + + @Override + public void setOnActivityResultCallback(Callback callback) { + super.setOnActivityResultCallback((result, data) -> { + mResult = data; + callback.onRequestResult(result, data); + }); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @Override + public void request() { + Activity activity = mAppUtils.getCurrentActivity(); + if (activity instanceof OnActivityResultDelegate.DelegateHost) { + ScreenCaptureRequester requester = new ActivityScreenCaptureRequester( + ((OnActivityResultDelegate.DelegateHost) activity).getOnActivityResultDelegateMediator(), activity); + requester.setOnActivityResultCallback(mCallback); + requester.request(); + } else { + ScreenCaptureRequestActivity.request(mContext, mCallback); + } + } + } +} diff --git a/app/src/main/java/com/stardust/autojs/BuildConfig.kt b/app/src/main/java/com/stardust/autojs/BuildConfig.kt new file mode 100644 index 00000000..3092f8ba --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/BuildConfig.kt @@ -0,0 +1,3 @@ +package com.stardust.autojs + +typealias BuildConfig = org.autojs.autojs6.BuildConfig diff --git a/app/src/main/java/com/stardust/autojs/Config.java b/app/src/main/java/com/stardust/autojs/Config.java new file mode 100644 index 00000000..ef102db4 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/Config.java @@ -0,0 +1,41 @@ +package com.stardust.autojs; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +/** + * Created by Stardust on 2017/12/8. + */ + +public class Config { + + private static Config sInstance; + private SharedPreferences mSharedPreferences; + private final Context mContext; + + public Config(Context context) { + mContext = context; + mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + } + + public static void setInstance(Config instance) { + if (sInstance != null) + throw new IllegalStateException(); + sInstance = instance; + } + + public static Config getInstance() { + return sInstance; + } + + public boolean isPrintJavaStackTraceEnabled() { + return mSharedPreferences.getBoolean("key_print_java_stack_trace", false); + } + + private String getString(int resId) { + return mContext.getString(resId); + } + + +} diff --git a/app/src/main/java/com/stardust/autojs/ScriptEngineService.java b/app/src/main/java/com/stardust/autojs/ScriptEngineService.java new file mode 100644 index 00000000..98874058 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/ScriptEngineService.java @@ -0,0 +1,286 @@ +package com.stardust.autojs; + +import android.content.Context; +import androidx.annotation.Nullable; + +import com.stardust.autojs.engine.JavaScriptEngine; +import com.stardust.autojs.engine.ScriptEngine; +import com.stardust.autojs.engine.ScriptEngineManager; +import com.stardust.autojs.execution.ExecutionConfig; +import com.stardust.autojs.execution.LoopedBasedJavaScriptExecution; +import com.stardust.autojs.execution.RunnableScriptExecution; +import com.stardust.autojs.execution.ScriptExecuteActivity; +import com.stardust.autojs.execution.ScriptExecution; +import com.stardust.autojs.execution.ScriptExecutionListener; +import com.stardust.autojs.execution.ScriptExecutionObserver; +import com.stardust.autojs.execution.ScriptExecutionTask; +import com.stardust.autojs.execution.SimpleScriptExecutionListener; +import com.stardust.autojs.runtime.ScriptRuntime; +import com.stardust.autojs.runtime.api.Console; +import com.stardust.autojs.script.JavaScriptSource; +import com.stardust.autojs.script.ScriptSource; +import com.stardust.lang.ThreadCompat; +import com.stardust.util.UiHandler; + +import org.autojs.autojs.util.ViewUtils; +import org.autojs.autojs6.R; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Set; + +import static com.stardust.autojs.runtime.exception.ScriptInterruptedException.causedByInterrupted; + +/** + * Created by Stardust on 2017/1/23. + */ + +public class ScriptEngineService { + + private static final String LOG_TAG = "ScriptEngineService"; + private static final EventBus EVENT_BUS = new EventBus(); + private static final ScriptExecutionListener GLOBAL_LISTENER = new SimpleScriptExecutionListener() { + @Override + public void onStart(ScriptExecution execution) { + if (execution.getEngine() instanceof JavaScriptEngine) { + ((JavaScriptEngine) execution.getEngine()).getRuntime() + .console.setTitle(execution.getSource().getName()); + } + EVENT_BUS.post(new ScriptExecutionEvent(ScriptExecutionEvent.ON_START, execution.getSource().toString())); + } + + @Override + public void onSuccess(ScriptExecution execution, Object result) { + onFinish(execution); + } + + private void onFinish(ScriptExecution execution) { + + } + + @Override + public void onException(ScriptExecution execution, Throwable e) { + e.printStackTrace(); + onFinish(execution); + String message = null; + if (!causedByInterrupted(e)) { + message = e.getMessage(); + if (execution.getEngine() instanceof JavaScriptEngine) { + ((JavaScriptEngine) execution.getEngine()).getRuntime() + .console.error(e); + } + } + if (execution.getEngine() instanceof JavaScriptEngine) { + JavaScriptEngine engine = (JavaScriptEngine) execution.getEngine(); + Throwable uncaughtException = engine.getUncaughtException(); + if (uncaughtException != null) { + engine.getRuntime().console.error(uncaughtException); + message = uncaughtException.getMessage(); + } + } + if (message != null) { + EVENT_BUS.post(new ScriptExecutionEvent(ScriptExecutionEvent.ON_EXCEPTION, message)); + } + } + + }; + + + private static ScriptEngineService sInstance; + private final Context mContext; + private UiHandler mUiHandler; + private final Console mGlobalConsole; + private final ScriptEngineManager mScriptEngineManager; + private final EngineLifecycleObserver mEngineLifecycleObserver = new EngineLifecycleObserver() { + + @Override + public void onEngineRemove(ScriptEngine engine) { + mScriptExecutions.remove(engine.getId()); + super.onEngineRemove(engine); + } + }; + private ScriptExecutionObserver mScriptExecutionObserver = new ScriptExecutionObserver(); + private LinkedHashMap mScriptExecutions = new LinkedHashMap<>(); + + ScriptEngineService(ScriptEngineServiceBuilder builder) { + mUiHandler = builder.mUiHandler; + mContext = mUiHandler.getContext(); + mScriptEngineManager = builder.mScriptEngineManager; + mGlobalConsole = builder.mGlobalConsole; + mScriptEngineManager.setEngineLifecycleCallback(mEngineLifecycleObserver); + mScriptExecutionObserver.registerScriptExecutionListener(GLOBAL_LISTENER); + EVENT_BUS.register(this); + mScriptEngineManager.putGlobal("context", mUiHandler.getContext()); + ScriptRuntime.setApplicationContext(builder.mUiHandler.getContext().getApplicationContext()); + } + + public Console getGlobalConsole() { + return mGlobalConsole; + } + + public void registerEngineLifecycleCallback(ScriptEngineManager.EngineLifecycleCallback engineLifecycleCallback) { + mEngineLifecycleObserver.registerCallback(engineLifecycleCallback); + } + + public void unregisterEngineLifecycleCallback(ScriptEngineManager.EngineLifecycleCallback engineLifecycleCallback) { + mEngineLifecycleObserver.unregisterCallback(engineLifecycleCallback); + } + + public boolean registerGlobalScriptExecutionListener(ScriptExecutionListener listener) { + return mScriptExecutionObserver.registerScriptExecutionListener(listener); + } + + public boolean unregisterGlobalScriptExecutionListener(ScriptExecutionListener listener) { + return mScriptExecutionObserver.removeScriptExecutionListener(listener); + } + + public ScriptExecution execute(ScriptExecutionTask task) { + ScriptExecution execution = executeInternal(task); + mScriptExecutions.put(execution.getId(), execution); + return execution; + } + + private ScriptExecution executeInternal(ScriptExecutionTask task) { + if (task.getListener() != null) { + task.setExecutionListener(new ScriptExecutionObserver.Wrapper(mScriptExecutionObserver, task.getListener())); + } else { + task.setExecutionListener(mScriptExecutionObserver); + } + ScriptSource source = task.getSource(); + if (source instanceof JavaScriptSource) { + int mode = ((JavaScriptSource) source).getExecutionMode(); + if ((mode & JavaScriptSource.EXECUTION_MODE_UI) != 0) { + return ScriptExecuteActivity.execute(mContext, mScriptEngineManager, task); + } + } + RunnableScriptExecution r; + if (source instanceof JavaScriptSource) { + r = new LoopedBasedJavaScriptExecution(mScriptEngineManager, task); + } else { + r = new RunnableScriptExecution(mScriptEngineManager, task); + } + new ThreadCompat(r).start(); + return r; + } + + public ScriptExecution execute(ScriptSource source, ScriptExecutionListener listener, ExecutionConfig config) { + return execute(new ScriptExecutionTask(source, listener, config)); + } + + public ScriptExecution execute(ScriptSource source, ExecutionConfig config) { + return execute(new ScriptExecutionTask(source, null, config)); + } + + @Subscribe + public void onScriptExecution(ScriptExecutionEvent event) { + if (event.getCode() == ScriptExecutionEvent.ON_START) { + mGlobalConsole.verbose(mContext.getString(R.string.text_start_running) + "[" + event.getMessage() + "]"); + } else if (event.getCode() == ScriptExecutionEvent.ON_EXCEPTION) { + mUiHandler.toast(mContext.getString(R.string.text_error) + ": " + event.getMessage()); + } + } + + public int stopAll() { + return mScriptEngineManager.stopAll(); + } + + + public void stopAllAndToast() { + int n = stopAll(); + if (n > 0) + // mUiHandler.toast(String.format(mContext.getString(R.string.text_already_stop_n_scripts), n)); + ViewUtils.showToast(mContext, mContext.getResources().getQuantityString(R.plurals.text_already_stop_n_scripts, n, n)); + } + + public Set getEngines() { + return mScriptEngineManager.getEngines(); + } + + public Collection getScriptExecutions() { + return mScriptExecutions.values(); + } + + @Nullable + public ScriptExecution getScriptExecution(int id) { + if (id == ScriptExecution.NO_ID) { + return null; + } + return mScriptExecutions.get(id); + } + + public static void setInstance(ScriptEngineService service) { + if (sInstance != null) { + throw new IllegalStateException(); + } + sInstance = service; + } + + public static ScriptEngineService getInstance() { + return sInstance; + } + + + private static class EngineLifecycleObserver implements ScriptEngineManager.EngineLifecycleCallback { + + private final Set mEngineLifecycleCallbacks = new LinkedHashSet<>(); + + @Override + public void onEngineCreate(ScriptEngine engine) { + synchronized (mEngineLifecycleCallbacks) { + for (ScriptEngineManager.EngineLifecycleCallback callback : mEngineLifecycleCallbacks) { + callback.onEngineCreate(engine); + } + } + } + + @Override + public void onEngineRemove(ScriptEngine engine) { + synchronized (mEngineLifecycleCallbacks) { + for (ScriptEngineManager.EngineLifecycleCallback callback : mEngineLifecycleCallbacks) { + callback.onEngineRemove(engine); + } + } + } + + void registerCallback(ScriptEngineManager.EngineLifecycleCallback callback) { + synchronized (mEngineLifecycleCallbacks) { + mEngineLifecycleCallbacks.add(callback); + } + + } + + void unregisterCallback(ScriptEngineManager.EngineLifecycleCallback callback) { + synchronized (mEngineLifecycleCallbacks) { + mEngineLifecycleCallbacks.remove(callback); + } + } + } + + + private static class ScriptExecutionEvent { + + static final int ON_START = 1001; + static final int ON_SUCCESS = 1002; + static final int ON_EXCEPTION = 1003; + + private final int mCode; + private final String mMessage; + + ScriptExecutionEvent(int code, String message) { + mCode = code; + mMessage = message; + } + + public int getCode() { + return mCode; + } + + public String getMessage() { + return mMessage; + } + } + +} diff --git a/app/src/main/java/com/stardust/autojs/ScriptEngineServiceBuilder.java b/app/src/main/java/com/stardust/autojs/ScriptEngineServiceBuilder.java new file mode 100644 index 00000000..1fa96f2a --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/ScriptEngineServiceBuilder.java @@ -0,0 +1,43 @@ +package com.stardust.autojs; + +import com.stardust.autojs.engine.ScriptEngineManager; +import com.stardust.autojs.runtime.ScriptRuntime; +import com.stardust.autojs.runtime.api.Console; +import com.stardust.util.Supplier; +import com.stardust.util.UiHandler; + +/** + * Created by Stardust on 2017/4/2. + */ + +public class ScriptEngineServiceBuilder { + + ScriptEngineManager mScriptEngineManager; + Console mGlobalConsole; + UiHandler mUiHandler; + + public ScriptEngineServiceBuilder() { + + } + + public ScriptEngineServiceBuilder uiHandler(UiHandler uiHandler) { + mUiHandler = uiHandler; + return this; + } + + public ScriptEngineServiceBuilder engineManger(ScriptEngineManager manager) { + mScriptEngineManager = manager; + return this; + } + + public ScriptEngineServiceBuilder globalConsole(Console console) { + mGlobalConsole = console; + return this; + } + + public ScriptEngineService build() { + return new ScriptEngineService(this); + } + + +} diff --git a/app/src/main/java/com/stardust/autojs/annotation/ScriptClass.java b/app/src/main/java/com/stardust/autojs/annotation/ScriptClass.java new file mode 100644 index 00000000..aaab5ba3 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/annotation/ScriptClass.java @@ -0,0 +1,14 @@ +package com.stardust.autojs.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Created by Stardust on 2017/4/3. + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.TYPE) +public @interface ScriptClass { +} diff --git a/app/src/main/java/com/stardust/autojs/annotation/ScriptInterface.java b/app/src/main/java/com/stardust/autojs/annotation/ScriptInterface.java new file mode 100644 index 00000000..80da3e29 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/annotation/ScriptInterface.java @@ -0,0 +1,14 @@ +package com.stardust.autojs.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Created by Stardust on 2017/4/2. + */ +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.METHOD}) +public @interface ScriptInterface { +} diff --git a/app/src/main/java/com/stardust/autojs/annotation/ScriptVariable.java b/app/src/main/java/com/stardust/autojs/annotation/ScriptVariable.java new file mode 100644 index 00000000..977937cc --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/annotation/ScriptVariable.java @@ -0,0 +1,15 @@ +package com.stardust.autojs.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Created by Stardust on 2017/4/2. + */ + +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.FIELD}) +public @interface ScriptVariable { +} diff --git a/app/src/main/java/com/stardust/autojs/codegeneration/CodeGenerator.java b/app/src/main/java/com/stardust/autojs/codegeneration/CodeGenerator.java new file mode 100644 index 00000000..2b00d410 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/codegeneration/CodeGenerator.java @@ -0,0 +1,205 @@ +package com.stardust.autojs.codegeneration; + +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; + +import com.stardust.automator.UiGlobalSelector; +import com.stardust.automator.UiObject; +import com.stardust.view.accessibility.NodeInfo; + +/** + * Created by Stardust on 2017/12/7. + */ + +public class CodeGenerator { + + public static final int UNTIL_FIND = 0; + public static final int FIND_ONE = 1; + public static final int WAIT_FOR = 2; + public static final int EXISTS = 3; + + private final ReadOnlyUiObject mRoot; + private final ReadOnlyUiObject mTarget; + private boolean mUsingId = true; + private boolean mUsingDesc = true; + private boolean mUsingText = true; + private int mSearchMode = FIND_ONE; + private int mAction = -1; + + public CodeGenerator(NodeInfo root, NodeInfo target) { + this(new ReadOnlyUiObject(root), new ReadOnlyUiObject(target)); + } + + public CodeGenerator(ReadOnlyUiObject root, ReadOnlyUiObject target) { + mRoot = root; + mTarget = target; + } + + + public void setUsingId(boolean usingId) { + mUsingId = usingId; + } + + public void setUsingDesc(boolean usingDesc) { + mUsingDesc = usingDesc; + } + + public void setUsingText(boolean usingText) { + mUsingText = usingText; + } + + public void setSearchMode(int searchMode) { + mSearchMode = searchMode; + } + + public void setAction(int action) { + mAction = action; + } + + public String generateCode() { + UiObject collection = getCollectionParent(mTarget); + if (collection != null) { + return generateCodeForCollectionChild(collection, mTarget); + } + + UiSelectorGenerator generator = new UiSelectorGenerator(mRoot, mTarget); + generator.setSearchMode(mSearchMode); + generator.setUsingDesc(mUsingDesc); + generator.setUsingId(mUsingId); + generator.setUsingText(mUsingText); + String selector = generateCode(generator, mRoot, mTarget, 2, 2, true); + if (selector == null) + return null; + return generateAction(selector); + } + + + protected String generateCode(UiSelectorGenerator generator, UiObject root, UiObject target, int maxParentLevel, int maxChildrenLevel, boolean withFind) { + String selector; + if (withFind) { + selector = generator.generateSelectorCode(); + } else { + UiGlobalSelector s = generator.generateSelector(); + selector = s == null ? null : s.toString(); + } + if (selector != null) { + return selector; + } + + if (maxChildrenLevel > 0) { + for (int i = 0; i < target.childCount(); i++) { + UiObject child = target.child(i); + if (child == null) + continue; + String childCode = generateCode(root, child, 0, maxChildrenLevel - 1); + if (childCode != null) { + return childCode + ".parent()"; + } + } + } + if (maxParentLevel > 0 && target.parent() != null) { + int index = target.indexInParent(); + if (index > 0) { + String parentCode = generateCode(root, target.parent(), maxParentLevel - 1, 0); + if (parentCode != null) { + return parentCode + "child(" + index + ")"; + } + } + } + return null; + } + + protected String generateCode(UiObject root, UiObject target, int maxParentLevel, int maxChildrenLevel) { + return generateCode(root, target, maxParentLevel, maxChildrenLevel, true); + } + + protected String generateCode(UiObject root, UiObject target, int maxParentLevel, int maxChildrenLevel, boolean withFind) { + UiSelectorGenerator generator = new UiSelectorGenerator(root, target); + generator.setUsingId(mUsingId); + return generateCode(generator, root, target, maxParentLevel, maxChildrenLevel, withFind); + } + + private String generateAction(String selector) { + if (selector == null) + return null; + if (mSearchMode == WAIT_FOR) { + return selector + ".waitFor()"; + } + if (mSearchMode == EXISTS) { + return "if(" + selector + ".exists()){\n \n}"; + } + String action = getAction(); + if (action.isEmpty()) { + return selector; + } else { + return selector + "." + action; + } + } + + private String getAction() { + switch (mAction) { + case AccessibilityNodeInfoCompat.ACTION_CLICK: + return "click()"; + case AccessibilityNodeInfoCompat.ACTION_LONG_CLICK: + return "longClick()"; + + case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: + return "scrollBackward()"; + + case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: + return "scrollForward()"; + + case AccessibilityNodeInfoCompat.ACTION_SET_TEXT: + return "setText(\"\")"; + } + return ""; + } + + private String generateCodeForCollectionChild(UiObject collection, UiObject target) { + UiObject parent = target.parent(); + if (parent == null) + return null; + UiObject collectionItem = null; + for (int i = 0; i < collection.childCount(); i++) { + if (inherits(collection.child(i), target)) { + collectionItem = collection.child(i); + break; + } + } + if (collectionItem == null) + return null; + String collectionCode = generateCode(mRoot, collection, 2, 0); + if (collectionCode == null) + return null; + String itemCode = generateCode(collectionItem, target, 1, 2, false); + if (itemCode == null) + return null; + return collectionCode + ".children().forEach(child => {\n" + + "var target = child.findOne(" + itemCode + ");\n" + + "target." + getAction() + ";\n" + + "});"; + } + + private boolean inherits(UiObject root, UiObject target) { + for (int i = 0; i < root.childCount(); i++) { + UiObject child = root.child(i); + if (child != null) { + if (child.equals(target) || inherits(child, target)) { + return true; + } + } + } + return false; + } + + private UiObject getCollectionParent(UiObject target) { + UiObject parent = target.parent(); + while (parent != null) { + if (parent.rowCount() > 0 || parent.columnCount() > 0) { + return parent; + } + parent = parent.parent(); + } + return null; + } + +} diff --git a/app/src/main/java/com/stardust/autojs/codegeneration/ReadOnlyUiObject.java b/app/src/main/java/com/stardust/autojs/codegeneration/ReadOnlyUiObject.java new file mode 100644 index 00000000..bd3b2ec5 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/codegeneration/ReadOnlyUiObject.java @@ -0,0 +1,332 @@ +package com.stardust.autojs.codegeneration; + +import android.graphics.Rect; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; + +import com.stardust.automator.UiObject; +import com.stardust.view.accessibility.NodeInfo; + +/** + * Created by Stardust on 2017/11/5. + */ + +public class ReadOnlyUiObject extends UiObject { + + private NodeInfo mNodeInfo; + + public ReadOnlyUiObject(NodeInfo info) { + super(null, info.getDepth(), -1); + mNodeInfo = info; + } + + public ReadOnlyUiObject(NodeInfo info, int indexInParent) { + super(null, info.getDepth(), indexInParent); + mNodeInfo = info; + } + + @Nullable + @Override + public UiObject child(int i) { + return new ReadOnlyUiObject(mNodeInfo.getChildren().get(i), i); + } + + @Nullable + @Override + public UiObject parent() { + return mNodeInfo.getParent() == null ? null : new ReadOnlyUiObject(mNodeInfo.getParent()); + } + + @Override + public int childCount() { + return mNodeInfo.getChildren().size(); + } + + @Override + public int getChildCount() { + return childCount(); + } + + @Override + public String className() { + return mNodeInfo.getClassName(); + } + + @Override + public CharSequence getClassName() { + return className(); + } + + @Override + public String packageName() { + return mNodeInfo.getPackageName(); + } + + @Override + public CharSequence getPackageName() { + return packageName(); + } + + @Override + public String id() { + return mNodeInfo.getId(); + } + + @Override + public String desc() { + return mNodeInfo.getDesc(); + } + + @Override + public String getViewIdResourceName() { + return id(); + } + + @Override + public CharSequence getContentDescription() { + return desc(); + } + + + @Override + public Rect bounds() { + return mNodeInfo.getBoundsInScreen(); + } + + @Override + public Rect boundsInParent() { + return mNodeInfo.getBoundsInParent(); + } + + @Override + public int drawingOrder() { + return mNodeInfo.getDrawingOrder(); + } + + @NonNull + @Override + public String text() { + return mNodeInfo.getText(); + } + + @Override + public CharSequence getText() { + return text(); + } + + @Override + public AccessibilityNodeInfoCompat getChild(int index) { + return child(index); + } + + @Override + public int getDrawingOrder() { + return drawingOrder(); + } + + @Override + public void getBoundsInParent(Rect outBounds) { + outBounds.set(mNodeInfo.getBoundsInParent()); + } + + @Override + public void getBoundsInScreen(Rect outBounds) { + outBounds.set(mNodeInfo.getBoundsInScreen()); + } + + + @Override + public int depth() { + return mNodeInfo.getDepth(); + } + + @Override + public boolean checkable() { + return mNodeInfo.getCheckable(); + } + + @Override + public boolean checked() { + return mNodeInfo.getChecked(); + } + + @Override + public boolean focusable() { + return mNodeInfo.getFocusable(); + } + + @Override + public boolean focused() { + return mNodeInfo.getFocused(); + } + + @Override + public boolean visibleToUser() { + return mNodeInfo.getVisibleToUser(); + } + + @Override + public boolean accessibilityFocused() { + return mNodeInfo.getAccessibilityFocused(); + } + + @Override + public boolean selected() { + return mNodeInfo.getSelected(); + } + + @Override + public boolean clickable() { + return mNodeInfo.getClickable(); + } + + @Override + public boolean longClickable() { + return mNodeInfo.getLongClickable(); + } + + @Override + public boolean enabled() { + return mNodeInfo.getEnabled(); + } + + @Override + public boolean scrollable() { + return mNodeInfo.getScrollable(); + } + + @Override + public boolean isCheckable() { + return checkable(); + } + + @Override + public boolean isChecked() { + return checked(); + } + + @Override + public boolean isFocusable() { + return focusable(); + } + + @Override + public boolean isFocused() { + return focused(); + } + + @Override + public boolean isVisibleToUser() { + return visibleToUser(); + } + + @Override + public boolean isAccessibilityFocused() { + return accessibilityFocused(); + } + + @Override + public boolean isSelected() { + return selected(); + } + + @Override + public boolean isClickable() { + return clickable(); + } + + @Override + public boolean isLongClickable() { + return longClickable(); + } + + @Override + public boolean isEnabled() { + return enabled(); + } + + @Override + public boolean isPassword() { + return password(); + } + + @Override + public boolean isScrollable() { + return scrollable(); + } + + + @Override + public boolean isContextClickable() { + return mNodeInfo.getContextClickable(); + } + + @Override + public boolean isDismissable() { + return mNodeInfo.getDismissable(); + } + + @Override + public boolean isEditable() { + return mNodeInfo.getEditable(); + } + + @Override + public int row() { + return mNodeInfo.getRow(); + } + + @Override + public int column() { + return mNodeInfo.getColumn(); + } + + @Override + public int rowSpan() { + return mNodeInfo.getRowSpan(); + } + + @Override + public int columnSpan() { + return mNodeInfo.getColumnSpan(); + } + + @Override + public int rowCount() { + return mNodeInfo.getRowCount(); + } + + @Override + public int columnCount() { + return mNodeInfo.getColumnCount(); + } + + @Override + public void recycle() { + + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + ReadOnlyUiObject that = (ReadOnlyUiObject) o; + + return mNodeInfo.equals(that.mNodeInfo); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + mNodeInfo.hashCode(); + return result; + } + + @Override + public String toString() { + return mNodeInfo.toString(); + } +} diff --git a/app/src/main/java/com/stardust/autojs/codegeneration/UiSelectorGenerator.java b/app/src/main/java/com/stardust/autojs/codegeneration/UiSelectorGenerator.java new file mode 100644 index 00000000..cd3444b0 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/codegeneration/UiSelectorGenerator.java @@ -0,0 +1,133 @@ +package com.stardust.autojs.codegeneration; + +import com.stardust.automator.UiGlobalSelector; +import com.stardust.automator.UiObject; +import com.stardust.util.Consumer; + +import androidx.appcompat.widget.AppCompatEditText; + +import static com.stardust.autojs.codegeneration.CodeGenerator.FIND_ONE; +import static com.stardust.autojs.codegeneration.CodeGenerator.UNTIL_FIND; +import static com.stardust.autojs.codegeneration.CodeGenerator.WAIT_FOR; + +/** + * Created by Stardust on 2017/12/7. + */ + +public class UiSelectorGenerator { + + private UiObject mRoot; + private UiObject mTarget; + private boolean mUsingId = true; + private boolean mUsingDesc = true; + private boolean mUsingText = true; + private int mSearchMode = FIND_ONE; + + public UiSelectorGenerator(UiObject root, UiObject target) { + mRoot = root; + mTarget = target; + } + + public UiGlobalSelector generateSelector() { + UiGlobalSelector selector = new UiGlobalSelector(); + if (mUsingId && + tryWithStringCondition(selector, mTarget.id(), selector::id)) { + return selector; + } + + if (tryWithStringCondition(selector, mTarget.className(), selector::className)) { + return selector; + } + if (mUsingText && + tryWithStringCondition(selector, mTarget.text(), selector::text)) { + return selector; + } + if (mUsingDesc && + tryWithStringCondition(selector, mTarget.desc(), selector::desc)) { + return selector; + } + if (mTarget.scrollable() && tryWithBooleanCondition(selector, mTarget.scrollable(), selector::scrollable)) { + return selector; + } + if (mTarget.clickable() && tryWithBooleanCondition(selector, mTarget.clickable(), selector::clickable)) { + return selector; + } + if (mTarget.selected() && tryWithBooleanCondition(selector, mTarget.selected(), selector::selected)) { + return selector; + } + if (mTarget.checkable() && tryWithBooleanCondition(selector, mTarget.checkable(), selector::checkable)) { + return selector; + } + if (mTarget.checked() && tryWithBooleanCondition(selector, mTarget.checked(), selector::checked)) { + return selector; + } + if (mTarget.longClickable() && tryWithBooleanCondition(selector, mTarget.longClickable(), selector::longClickable)) { + return selector; + } + if (tryWithIntCondition(selector, mTarget.depth(), selector::depth)) { + return selector; + } + return null; + } + + public String generateSelectorCode() { + UiGlobalSelector selector = generateSelector(); + if (selector == null) { + return null; + } + if (mSearchMode == FIND_ONE) { + return selector + ".findOne()"; + } else if (mSearchMode == UNTIL_FIND) { + return selector + ".untilFind()"; + } else { + return selector.toString(); + } + } + + + public void setUsingId(boolean usingId) { + mUsingId = usingId; + } + + public void setUsingDesc(boolean usingDesc) { + mUsingDesc = usingDesc; + } + + public void setUsingText(boolean usingText) { + mUsingText = usingText; + } + + public void setSearchMode(int searchMode) { + mSearchMode = searchMode; + } + + private boolean tryWithBooleanCondition(UiGlobalSelector selector, boolean value, Consumer condition) { + condition.accept(value); + return shouldStopGeneration(selector); + } + + + private boolean tryWithStringCondition(UiGlobalSelector selector, String value, Consumer condition) { + if (value == null || value.isEmpty()) { + return false; + } + condition.accept(value); + return shouldStopGeneration(selector); + } + + private boolean shouldStopGeneration(UiGlobalSelector selector) { + if (mSearchMode == UNTIL_FIND) { + return !selector.findAndReturnList(mRoot, 1).isEmpty(); + } else { + return selector.findAndReturnList(mRoot, 2).size() == 1; + + } + } + + private boolean tryWithIntCondition(UiGlobalSelector selector, int value, Consumer condition) { + condition.accept(value); + return shouldStopGeneration(selector); + } + + +} diff --git a/app/src/main/java/com/stardust/autojs/core/accessibility/AccessibilityBridge.java b/app/src/main/java/com/stardust/autojs/core/accessibility/AccessibilityBridge.java new file mode 100644 index 00000000..a42ee0a3 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/accessibility/AccessibilityBridge.java @@ -0,0 +1,152 @@ +package com.stardust.autojs.core.accessibility; + +import android.app.ActivityManager; +import android.app.AppOpsManager; +import android.content.Context; +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityWindowInfo; + +import com.stardust.app.AppOpsKt; +import com.stardust.autojs.runtime.accessibility.AccessibilityConfig; +import com.stardust.util.IntentUtil; +import com.stardust.util.UiHandler; +import com.stardust.autojs.core.activity.ActivityInfoProvider; +import com.stardust.view.accessibility.AccessibilityNotificationObserver; +import com.stardust.view.accessibility.AccessibilityService; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + + +/** + * Created by Stardust on 2017/4/2. + */ + +public abstract class AccessibilityBridge { + + public interface WindowFilter { + boolean filter(AccessibilityWindowInfo info); + } + + public static final int MODE_NORMAL = 0; + public static final int MODE_FAST = 1; + + public static final int FLAG_FIND_ON_UI_THREAD = 1; + public static final int FLAG_USE_USAGE_STATS = 2; + public static final int FLAG_USE_SHELL = 4; + + private int mMode = MODE_NORMAL; + private int mFlags = 0; + private final AccessibilityConfig mConfig; + private WindowFilter mWindowFilter; + private final UiHandler mUiHandler; + private final Context mContext; + + public AccessibilityBridge(Context context, AccessibilityConfig config, UiHandler uiHandler) { + mConfig = config; + mUiHandler = uiHandler; + mConfig.seal(); + mContext = context; + } + + public abstract void ensureServiceEnabled(); + + public abstract void waitForServiceEnabled(); + + public void post(Runnable r) { + mUiHandler.post(r); + } + + @Nullable + public abstract AccessibilityService getService(); + + public List windowRoots() { + AccessibilityService service = getService(); + if (service == null) + return Collections.emptyList(); + ArrayList roots = new ArrayList<>(); + if (mWindowFilter != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + for (AccessibilityWindowInfo window : service.getWindows()) { + if (mWindowFilter.filter(window)) { + AccessibilityNodeInfo root = window.getRoot(); + if (root != null) { + roots.add(root); + } + } + } + return roots; + } + if ((mMode & MODE_FAST) != 0) { + return Collections.singletonList(service.fastRootInActiveWindow()); + } + return Collections.singletonList(service.getRootInActiveWindow()); + } + + @Nullable + public AccessibilityNodeInfo getRootInCurrentWindow() { + AccessibilityService service = getService(); + if (service == null) + return null; + if (mWindowFilter != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + for (AccessibilityWindowInfo window : service.getWindows()) { + if (mWindowFilter.filter(window)) { + return window.getRoot(); + } + } + return null; + } + if ((mMode & MODE_FAST) != 0) { + return service.fastRootInActiveWindow(); + } + return service.getRootInActiveWindow(); + } + + public AccessibilityNodeInfo getRootInActiveWindow() { + AccessibilityService service = getService(); + if (service == null) + return null; + if ((mMode & MODE_FAST) != 0) { + return service.fastRootInActiveWindow(); + } + return service.getRootInActiveWindow(); + } + + public void setWindowFilter(WindowFilter windowFilter) { + mWindowFilter = windowFilter; + } + + public abstract ActivityInfoProvider getInfoProvider(); + + public void setMode(int mode) { + mMode = mode; + } + + public int getFlags() { + return mFlags; + } + + public void setFlags(int flags) { + mFlags = flags; + if ((mFlags & FLAG_USE_USAGE_STATS) != 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + if (!AppOpsKt.isOpPermissionGranted(mContext, AppOpsManager.OPSTR_GET_USAGE_STATS)) { + IntentUtil.requestAppUsagePermission(mContext); + throw new SecurityException("没有\"查看使用情况\"权限"); + } + } + getInfoProvider().setUseUsageStats((mFlags & FLAG_USE_USAGE_STATS) != 0); + getInfoProvider().setUseShell((mFlags & FLAG_USE_SHELL) != 0); + } + + @NonNull + public abstract AccessibilityNotificationObserver getNotificationObserver(); + + public AccessibilityConfig getConfig() { + return mConfig; + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/accessibility/AccessibilityServiceUsher.kt b/app/src/main/java/com/stardust/autojs/core/accessibility/AccessibilityServiceUsher.kt new file mode 100644 index 00000000..76a26c33 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/accessibility/AccessibilityServiceUsher.kt @@ -0,0 +1,27 @@ +package com.stardust.autojs.core.accessibility + +import android.accessibilityservice.AccessibilityServiceInfo +import android.os.Build +import com.stardust.autojs.core.pref.Pref +import com.stardust.view.accessibility.AccessibilityService + +class AccessibilityServiceUsher : AccessibilityService() { + + override fun onServiceConnected() { + val serviceInfo = serviceInfo + if (Pref.isStableModeEnabled) { + serviceInfo.flags = serviceInfo.flags and AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS.inv() + } else { + serviceInfo.flags = serviceInfo.flags or AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (Pref.isGestureObservingEnabled) { + serviceInfo.flags = serviceInfo.flags or AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE + } else { + serviceInfo.flags = serviceInfo.flags and AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE.inv() + } + } + setServiceInfo(serviceInfo) + super.onServiceConnected() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/stardust/autojs/core/accessibility/SimpleActionAutomator.kt b/app/src/main/java/com/stardust/autojs/core/accessibility/SimpleActionAutomator.kt new file mode 100644 index 00000000..4912bfda --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/accessibility/SimpleActionAutomator.kt @@ -0,0 +1,261 @@ +package com.stardust.autojs.core.accessibility + +import android.accessibilityservice.AccessibilityService +import android.accessibilityservice.GestureDescription +import android.graphics.Rect +import android.os.Build +import android.os.Handler +import android.view.accessibility.AccessibilityNodeInfo +import androidx.annotation.RequiresApi +import com.stardust.autojs.annotation.ScriptInterface +import com.stardust.autojs.runtime.ScriptRuntime +import com.stardust.autojs.runtime.accessibility.AccessibilityConfig +import com.stardust.automator.GlobalActionAutomator +import com.stardust.automator.UiObject +import com.stardust.automator.simple_action.ActionFactory +import com.stardust.automator.simple_action.ActionTarget +import com.stardust.automator.simple_action.SimpleAction +import com.stardust.util.DeveloperUtils +import com.stardust.util.ScreenMetrics + +/** + * Created by Stardust on 2017/4/2. + */ + +class SimpleActionAutomator(private val mAccessibilityBridge: AccessibilityBridge, private val mScriptRuntime: ScriptRuntime) { + + private lateinit var mGlobalActionAutomator: GlobalActionAutomator + + private var mScreenMetrics: ScreenMetrics? = null + + private val isRunningPackageSelf: Boolean + get() = DeveloperUtils.isSelfPackage(mAccessibilityBridge.infoProvider.latestPackage) + + @ScriptInterface + fun text(text: String, i: Int): ActionTarget { + return ActionTarget.TextActionTarget(text, i) + } + + @ScriptInterface + fun bounds(left: Int, top: Int, right: Int, bottom: Int): ActionTarget { + return ActionTarget.BoundsActionTarget(Rect(left, top, right, bottom)) + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @ScriptInterface + fun editable(i: Int): ActionTarget { + ScriptRuntime.requiresApi(Build.VERSION_CODES.LOLLIPOP) + return ActionTarget.EditableActionTarget(i) + } + + @ScriptInterface + fun id(id: String): ActionTarget { + return ActionTarget.IdActionTarget(id) + } + + @ScriptInterface + fun click(target: ActionTarget): Boolean { + return performAction(target.createAction(AccessibilityNodeInfo.ACTION_CLICK)) + } + + @ScriptInterface + fun longClick(target: ActionTarget): Boolean { + return performAction(target.createAction(AccessibilityNodeInfo.ACTION_LONG_CLICK)) + } + + @ScriptInterface + fun scrollUp(target: ActionTarget): Boolean { + return performAction(target.createAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD)) + } + + @ScriptInterface + fun scrollDown(target: ActionTarget): Boolean { + return performAction(target.createAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)) + } + + @ScriptInterface + fun scrollBackward(i: Int): Boolean { + return performAction(ActionFactory.createScrollAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD, i)) + } + + @ScriptInterface + fun scrollForward(i: Int): Boolean { + return performAction(ActionFactory.createScrollAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD, i)) + } + + @ScriptInterface + fun scrollMaxBackward(): Boolean { + return performAction(ActionFactory.createScrollMaxAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD)) + } + + @ScriptInterface + fun scrollMaxForward(): Boolean { + return performAction(ActionFactory.createScrollMaxAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)) + } + + @ScriptInterface + fun focus(target: ActionTarget): Boolean { + return performAction(target.createAction(AccessibilityNodeInfo.ACTION_FOCUS)) + } + + @ScriptInterface + fun select(target: ActionTarget): Boolean { + return performAction(target.createAction(AccessibilityNodeInfo.ACTION_SELECT)) + } + + @ScriptInterface + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + fun setText(target: ActionTarget, text: String): Boolean { + ScriptRuntime.requiresApi(Build.VERSION_CODES.LOLLIPOP) + return performAction(target.createAction(AccessibilityNodeInfo.ACTION_SET_TEXT, text)) + } + + @ScriptInterface + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + fun appendText(target: ActionTarget, text: String): Boolean { + ScriptRuntime.requiresApi(Build.VERSION_CODES.LOLLIPOP) + return performAction(target.createAction(UiObject.ACTION_APPEND_TEXT, text)) + } + + @ScriptInterface + fun back(): Boolean { + return performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK) + } + + @ScriptInterface + fun home(): Boolean { + return performGlobalAction(AccessibilityService.GLOBAL_ACTION_HOME) + } + + @ScriptInterface + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + fun powerDialog(): Boolean { + ScriptRuntime.requiresApi(Build.VERSION_CODES.LOLLIPOP) + return performGlobalAction(AccessibilityService.GLOBAL_ACTION_POWER_DIALOG) + } + + @ScriptInterface + fun notifications(): Boolean { + return performGlobalAction(AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS) + } + + @ScriptInterface + fun quickSettings(): Boolean { + return performGlobalAction(AccessibilityService.GLOBAL_ACTION_QUICK_SETTINGS) + } + + @ScriptInterface + fun recents(): Boolean { + return performGlobalAction(AccessibilityService.GLOBAL_ACTION_RECENTS) + } + + @ScriptInterface + @RequiresApi(api = Build.VERSION_CODES.N) + fun splitScreen(): Boolean { + return performGlobalAction(AccessibilityService.GLOBAL_ACTION_TOGGLE_SPLIT_SCREEN) + } + + @ScriptInterface + @RequiresApi(api = Build.VERSION_CODES.N) + fun gesture(start: Long, duration: Long, vararg points: IntArray): Boolean { + prepareForGesture() + return mGlobalActionAutomator.gesture(start, duration, *points) + } + + @RequiresApi(api = Build.VERSION_CODES.N) + fun gestureAsync(start: Long, duration: Long, vararg points: IntArray) { + prepareForGesture() + mGlobalActionAutomator.gestureAsync(start, duration, *points) + } + + @RequiresApi(api = Build.VERSION_CODES.N) + fun gestures(strokes: Any): Boolean { + prepareForGesture() + @Suppress("UNCHECKED_CAST") + return mGlobalActionAutomator.gestures(*strokes as Array) + } + + //如果这里用GestureDescription.StrokeDescription[]为参数,安卓7.0以下会因为找不到这个类而报错 + @RequiresApi(api = Build.VERSION_CODES.N) + fun gesturesAsync(strokes: Any) { + prepareForGesture() + @Suppress("UNCHECKED_CAST") + mGlobalActionAutomator.gesturesAsync(*strokes as Array) + } + + private fun prepareForGesture() { + ScriptRuntime.requiresApi(24) + if (!::mGlobalActionAutomator.isInitialized) { + mGlobalActionAutomator = GlobalActionAutomator(Handler(mScriptRuntime.loopers.servantLooper)) { + ensureAccessibilityServiceEnabled() + return@GlobalActionAutomator mAccessibilityBridge.service!! + } + } + mGlobalActionAutomator.setScreenMetrics(mScreenMetrics) + } + + @ScriptInterface + @RequiresApi(api = Build.VERSION_CODES.N) + fun click(x: Int, y: Int): Boolean { + prepareForGesture() + return mGlobalActionAutomator.click(x, y) + } + + @ScriptInterface + @RequiresApi(api = Build.VERSION_CODES.N) + fun press(x: Int, y: Int, delay: Int): Boolean { + prepareForGesture() + return mGlobalActionAutomator.press(x, y, delay) + } + + @ScriptInterface + @RequiresApi(api = Build.VERSION_CODES.N) + fun longClick(x: Int, y: Int): Boolean { + prepareForGesture() + return mGlobalActionAutomator.longClick(x, y) + } + + @ScriptInterface + @RequiresApi(api = Build.VERSION_CODES.N) + fun swipe(x1: Int, y1: Int, x2: Int, y2: Int, delay: Int): Boolean { + prepareForGesture() + return mGlobalActionAutomator.swipe(x1, y1, x2, y2, delay.toLong()) + } + + private fun performGlobalAction(action: Int): Boolean { + ensureAccessibilityServiceEnabled() + val service = mAccessibilityBridge.service ?: return false + return service.performGlobalAction(action) + } + + @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) + @ScriptInterface + fun paste(target: ActionTarget): Boolean { + ScriptRuntime.requiresApi(18) + return performAction(target.createAction(AccessibilityNodeInfo.ACTION_PASTE)) + } + + private fun ensureAccessibilityServiceEnabled() { + mAccessibilityBridge.ensureServiceEnabled() + } + + private fun performAction(simpleAction: SimpleAction): Boolean { + ensureAccessibilityServiceEnabled() + if (AccessibilityConfig.isUnintendedGuardEnabled() && isRunningPackageSelf) { + return false + } + val roots = mAccessibilityBridge.windowRoots().filter { it != null } + if (roots.isEmpty()) + return false + var succeed = true + for (root in roots) { + succeed = succeed and simpleAction.perform(UiObject.createRoot(root)) + } + return succeed + } + + fun setScreenMetrics(metrics: ScreenMetrics) { + mScreenMetrics = metrics + } + +} diff --git a/app/src/main/java/com/stardust/autojs/core/accessibility/UiSelector.kt b/app/src/main/java/com/stardust/autojs/core/accessibility/UiSelector.kt new file mode 100644 index 00000000..254d5957 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/accessibility/UiSelector.kt @@ -0,0 +1,372 @@ +package com.stardust.autojs.core.accessibility + +import android.os.Looper +import android.os.SystemClock +import android.util.Log +import android.view.accessibility.AccessibilityNodeInfo +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat +import com.stardust.autojs.BuildConfig +import com.stardust.autojs.annotation.ScriptInterface +import com.stardust.autojs.runtime.exception.ScriptInterruptedException +import com.stardust.automator.ActionArgument +import com.stardust.automator.UiGlobalSelector +import com.stardust.automator.UiObject +import com.stardust.automator.UiObjectCollection +import com.stardust.automator.filter.Filter +import com.stardust.concurrent.VolatileBox +import com.stardust.view.accessibility.AccessibilityNodeInfoAllocator + +/** + * Created by Stardust on 2017/3/9. + */ +class UiSelector : UiGlobalSelector { + private val mAccessibilityBridge: AccessibilityBridge + private var mAllocator: AccessibilityNodeInfoAllocator? = null + + constructor(accessibilityBridge: AccessibilityBridge) { + mAccessibilityBridge = accessibilityBridge + } + + constructor(accessibilityBridge: AccessibilityBridge, allocator: AccessibilityNodeInfoAllocator?) { + mAccessibilityBridge = accessibilityBridge + mAllocator = allocator + } + + protected fun find(max: Int): UiObjectCollection { + ensureAccessibilityServiceEnabled() + if ((mAccessibilityBridge.getFlags() and AccessibilityBridge.FLAG_FIND_ON_UI_THREAD) != 0 + && Looper.myLooper() != Looper.getMainLooper() + ) { + val result = VolatileBox() + mAccessibilityBridge.post(Runnable { result.setAndNotify(findImpl(max)) }) + return result.blockedGet() + } + return findImpl(max) + } + + @ScriptInterface + fun find(): UiObjectCollection { + return find(Int.Companion.MAX_VALUE) + } + + @ScriptInterface + protected fun findImpl(max: Int): UiObjectCollection { + val roots: MutableList = mAccessibilityBridge.windowRoots() + if (BuildConfig.DEBUG) Log.d(TAG, "find: roots = " + roots) + if (roots.isEmpty()) { + return UiObjectCollection.Companion.EMPTY + } + val result: MutableList = ArrayList() + for (root in roots) { + if (root == null) { + continue + } + if (root.getPackageName() != null && mAccessibilityBridge.getConfig().whiteListContains(root.getPackageName().toString())) { + Log.d(TAG, "package in white list, return null") + return UiObjectCollection.Companion.EMPTY + } + result.addAll(findAndReturnList(UiObject.Companion.createRoot(root, mAllocator), max - result.size)) + if (result.size >= max) { + break + } + } + return UiObjectCollection.Companion.of(result) + } + + public override fun textMatches(regex: String): UiGlobalSelector { + return super.textMatches(convertRegex(regex)) + } + + // TODO: 2018/1/30 更好的实现方式。 + private fun convertRegex(regex: String): String { + if (regex.startsWith("/") && regex.endsWith("/") && regex.length > 2) { + return regex.substring(1, regex.length - 1) + } + return regex + } + + + public override fun classNameMatches(regex: String): UiGlobalSelector { + return super.classNameMatches(convertRegex(regex)) + } + + public override fun idMatches(regex: String): UiGlobalSelector { + return super.idMatches(convertRegex(regex)) + } + + public override fun packageNameMatches(regex: String): UiGlobalSelector { + return super.packageNameMatches(convertRegex(regex)) + } + + public override fun descMatches(regex: String): UiGlobalSelector { + return super.descMatches(convertRegex(regex)) + } + + private fun ensureAccessibilityServiceEnabled() { + mAccessibilityBridge.ensureServiceEnabled() + } + + @ScriptInterface + fun untilFind(): UiObjectCollection { + ensureNonUiThread() + var uiObjectCollection = find() + while (uiObjectCollection.empty()) { + if (Thread.currentThread().isInterrupted()) { + throw ScriptInterruptedException() + } + try { + Thread.sleep(50) + } catch (e: InterruptedException) { + throw ScriptInterruptedException() + } + uiObjectCollection = find() + } + return uiObjectCollection + } + + private fun ensureNonUiThread() { + if (Looper.myLooper() == Looper.getMainLooper()) { + // TODO: 2018/11/1 配置字符串 + throw IllegalThreadStateException("不能在ui线程执行阻塞操作, 请在子线程或子脚本执行findOne()或untilFind()") + } + } + + @ScriptInterface + fun findOne(timeout: Long): UiObject? { + var uiObjectCollection = find(1) + val start = SystemClock.uptimeMillis() + while (uiObjectCollection.empty()) { + if (Thread.currentThread().isInterrupted()) { + throw ScriptInterruptedException() + } + if (timeout > 0 && SystemClock.uptimeMillis() - start > timeout) { + return null + } + try { + Thread.sleep(50) + } catch (e: InterruptedException) { + throw ScriptInterruptedException() + } + uiObjectCollection = find(1) + } + return uiObjectCollection.get(0) + } + + fun findOnce(): UiObject? { + return findOnce(0) + } + + fun findOnce(index: Int): UiObject? { + val uiObjectCollection = find(index + 1) + if (index >= uiObjectCollection.size()) { + return null + } + return uiObjectCollection.get(index) + } + + @ScriptInterface + fun findOne(): UiObject { + return untilFindOne() + } + + @ScriptInterface + fun exists(): Boolean { + val collection = find() + return collection.nonEmpty() + } + + fun untilFindOne(): UiObject { + return findOne(-1)!! + } + + @ScriptInterface + fun waitFor() { + untilFind() + } + + @ScriptInterface + public override fun id(id: String): UiSelector { + if (!id.contains(":")) { + addFilter(object : Filter { + override fun filter(node: UiObject): Boolean { + val fullId = mAccessibilityBridge.getInfoProvider().latestPackage + ":id/" + id + return fullId == node.getViewIdResourceName() + } + + override fun toString(): String { + return "id(\"" + id + "\")" + } + }) + } else { + super.id(id) + } + return this + } + + public override fun idStartsWith(prefix: String): UiGlobalSelector { + if (!prefix.contains(":")) { + addFilter(object : Filter { + override fun filter(nodeInfo: UiObject): Boolean { + val fullIdPrefix = mAccessibilityBridge.getInfoProvider().latestPackage + ":id/" + prefix + val id = nodeInfo.getViewIdResourceName() + return id != null && id.startsWith(fullIdPrefix) + } + + override fun toString(): String { + return "idStartsWith(\"" + prefix + "\")" + } + }) + } else { + super.idStartsWith(prefix) + } + return this + } + + private fun performAction(action: Int, vararg arguments: ActionArgument): Boolean { + return untilFind().performAction(action, *arguments) + } + + + @ScriptInterface + fun click(): Boolean { + return performAction(AccessibilityNodeInfoCompat.ACTION_CLICK) + } + + @ScriptInterface + fun longClick(): Boolean { + return performAction(AccessibilityNodeInfoCompat.ACTION_LONG_CLICK) + } + + @ScriptInterface + fun accessibilityFocus(): Boolean { + return performAction(AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS) + } + + @ScriptInterface + fun clearAccessibilityFocus(): Boolean { + return performAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS) + } + + @ScriptInterface + fun focus(): Boolean { + return performAction(AccessibilityNodeInfoCompat.ACTION_FOCUS) + } + + @ScriptInterface + fun clearFocus(): Boolean { + return performAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_FOCUS) + } + + @ScriptInterface + fun copy(): Boolean { + return performAction(AccessibilityNodeInfoCompat.ACTION_COPY) + } + + @ScriptInterface + fun paste(): Boolean { + return performAction(AccessibilityNodeInfoCompat.ACTION_PASTE) + } + + @ScriptInterface + fun select(): Boolean { + return performAction(AccessibilityNodeInfoCompat.ACTION_SELECT) + } + + @ScriptInterface + fun cut(): Boolean { + return performAction(AccessibilityNodeInfoCompat.ACTION_CUT) + } + + @ScriptInterface + fun collapse(): Boolean { + return performAction(AccessibilityNodeInfoCompat.ACTION_COLLAPSE) + } + + @ScriptInterface + fun expand(): Boolean { + return performAction(AccessibilityNodeInfoCompat.ACTION_EXPAND) + } + + @ScriptInterface + fun dismiss(): Boolean { + return performAction(AccessibilityNodeInfoCompat.ACTION_DISMISS) + } + + @ScriptInterface + fun show(): Boolean { + return performAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SHOW_ON_SCREEN.getId()) + } + + @ScriptInterface + fun scrollForward(): Boolean { + return performAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD) + } + + @ScriptInterface + fun scrollBackward(): Boolean { + return performAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD) + } + + @ScriptInterface + fun scrollUp(): Boolean { + return performAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_UP.getId()) + } + + @ScriptInterface + fun scrollDown(): Boolean { + return performAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_DOWN.getId()) + } + + @ScriptInterface + fun scrollLeft(): Boolean { + return performAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_LEFT.getId()) + } + + @ScriptInterface + fun scrollRight(): Boolean { + return performAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_RIGHT.getId()) + } + + @ScriptInterface + fun contextClick(): Boolean { + return performAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CONTEXT_CLICK.getId()) + } + + @ScriptInterface + fun setSelection(s: Int, e: Int): Boolean { + return performAction( + AccessibilityNodeInfoCompat.ACTION_SET_SELECTION, + ActionArgument.IntActionArgument(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_SELECTION_START_INT, s), + ActionArgument.IntActionArgument(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_SELECTION_END_INT, e) + ) + } + + @ScriptInterface + fun setText(text: String): Boolean { + return performAction( + AccessibilityNodeInfoCompat.ACTION_SET_TEXT, + ActionArgument.CharSequenceActionArgument(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text) + ) + } + + @ScriptInterface + fun setProgress(value: Float): Boolean { + return performAction( + AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SET_PROGRESS.getId(), + ActionArgument.FloatActionArgument(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_PROGRESS_VALUE, value) + ) + } + + @ScriptInterface + fun scrollTo(row: Int, column: Int): Boolean { + return performAction( + AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION.getId(), + ActionArgument.IntActionArgument(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_ROW_INT, row), + ActionArgument.IntActionArgument(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_COLUMN_INT, column) + ) + } + + companion object { + private const val TAG = "UiSelector" + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/activity/ActivityInfoProvider.kt b/app/src/main/java/com/stardust/autojs/core/activity/ActivityInfoProvider.kt new file mode 100644 index 00000000..404684ec --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/activity/ActivityInfoProvider.kt @@ -0,0 +1,199 @@ +@file:Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS", "RECEIVER_NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS", "SameParameterValue") + +package com.stardust.autojs.core.activity + +import android.accessibilityservice.AccessibilityService +import android.app.AppOpsManager +import android.app.usage.UsageStatsManager +import android.content.ComponentName +import android.content.Context +import android.content.pm.PackageManager +import android.util.Log +import android.view.accessibility.AccessibilityEvent +import android.view.accessibility.AccessibilityWindowInfo +import com.stardust.app.isOpPermissionGranted +import com.stardust.autojs.core.util.Shell +import com.stardust.view.accessibility.AccessibilityDelegate +import java.util.regex.Pattern + +/** + * Created by Stardust on 2017/3/9. + */ + +class ActivityInfoProvider(private val context: Context) : AccessibilityDelegate { + + private val mPackageManager: PackageManager = context.packageManager + + @Volatile + private var mLatestPackage: String = "" + @Volatile + private var mLatestActivity: String = "" + private var mLatestComponentFromShell: ComponentName? = null + + private var mShell: Shell? = null + private var mUseShell = false + + val latestPackage: String + get() { + val compFromShell = mLatestComponentFromShell + if (useShell && compFromShell != null) { + return compFromShell.packageName + } + if (useUsageStats) { + mLatestPackage = getLatestPackageByUsageStats() + } + return mLatestPackage + } + + val latestActivity: String + get() { + val compFromShell = mLatestComponentFromShell + if (useShell && compFromShell != null) { + return compFromShell.className + } + return mLatestActivity + } + + var useUsageStats: Boolean = false + + var useShell: Boolean + get() = mUseShell + set(value) { + if (value) { + mShell.let { + if (it == null) { + mShell = createShell(200) + } + } + } else { + mShell?.exit() + mShell = null + } + mUseShell = value + } + + override val eventTypes: Set? + get() = AccessibilityDelegate.ALL_EVENT_TYPES + + override fun onAccessibilityEvent(service: AccessibilityService, event: AccessibilityEvent): Boolean { + if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { + val window = service.getWindow(event.windowId) + if (window?.isFocused != false) { + setLatestComponent(event.packageName, event.className) + return false + } + } + return false + } + + fun getLatestPackageByUsageStatsIfGranted(): String { + if (context.isOpPermissionGranted(AppOpsManager.OPSTR_GET_USAGE_STATS)) { + return getLatestPackageByUsageStats() + } + return mLatestPackage + } + + private fun setLatestComponentFromShellOutput(output: String) { + val matcher = WINDOW_PATTERN.matcher(output) + if (!matcher.find() || matcher.groupCount() < 1) { + Log.w(LOG_TAG, "invalid format: $output") + return + } + val latestPackage = matcher.group(1) + if (latestPackage.contains(":")) { + return + } + val latestActivity = if (matcher.groupCount() >= 2) { + matcher.group(2).orEmpty() + } else { + "" + } + Log.d(LOG_TAG, "setLatestComponent: output = $output, comp = $latestPackage/$latestActivity") + mLatestComponentFromShell = ComponentName(latestPackage, latestActivity) + } + + private fun createShell(dumpInterval: Int): Shell { + val shell = Shell(true) + shell.setCallback(object : Shell.Callback { + override fun onOutput(str: String) { + + } + + override fun onNewLine(line: String) { + setLatestComponentFromShellOutput(line) + } + + override fun onInitialized() { + } + + override fun onInterrupted(e: InterruptedException) { + + } + }) + shell.exec(DUMP_WINDOW_COMMAND.format(dumpInterval)) + return shell + } + + fun getLatestPackageByUsageStats(): String { + val usageStatsManager = context.getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager + val current = System.currentTimeMillis() + val usageStats = usageStatsManager.queryUsageStats(UsageStatsManager.INTERVAL_BEST, current - 60 * 60 * 1000, current) + return if (usageStats.isEmpty()) { + mLatestPackage + } else { + usageStats.sortBy { + it.lastTimeStamp + } + usageStats.last().packageName + } + + } + + private fun setLatestComponent(latestPackage: CharSequence?, latestClass: CharSequence?) { + if (latestPackage == null) + return + val latestPackageStr = latestPackage.toString() + val latestClassStr = (latestClass ?: "").toString() + if (isPackageExists(latestPackageStr)) { + mLatestPackage = latestPackage.toString() + mLatestActivity = latestClassStr + } + Log.d(LOG_TAG, "setLatestComponent: $latestPackage/$latestClassStr $mLatestPackage/$mLatestActivity") + } + + private fun isPackageExists(packageName: String): Boolean { + return try { + mPackageManager.getPackageInfo(packageName, 0) + true + } catch (e: PackageManager.NameNotFoundException) { + false + } + } + + companion object { + private val WINDOW_PATTERN = Pattern.compile("Window\\{\\S+\\s\\S+\\s([^\\/]+)\\/?([^}]+)?\\}") + private val DUMP_WINDOW_COMMAND = """ + oldActivity="" + currentActivity=`dumpsys window windows | grep -E 'mCurrentFocus'` + while true + do + if [[ ${'$'}oldActivity != ${'$'}currentActivity && ${'$'}currentActivity != *"=null"* ]]; then + echo ${'$'}currentActivity + oldActivity=${'$'}currentActivity + fi + currentActivity=`dumpsys window windows | grep -E 'mCurrentFocus'` + done + """.trimIndent() + + private const val LOG_TAG = "ActivityInfoProvider" + } +} + +private fun AccessibilityService.getWindow(windowId: Int): AccessibilityWindowInfo? { + windows.forEach { + if (it.id == windowId) { + return it + } + } + return null +} diff --git a/app/src/main/java/com/stardust/autojs/core/boardcast/Broadcast.java b/app/src/main/java/com/stardust/autojs/core/boardcast/Broadcast.java new file mode 100644 index 00000000..ddc56110 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/boardcast/Broadcast.java @@ -0,0 +1,27 @@ +package com.stardust.autojs.core.boardcast; + +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Created by Stardust on 2018/4/1. + */ + +public class Broadcast { + + private static CopyOnWriteArrayList sEventEmitters = new CopyOnWriteArrayList<>(); + + public static void registerListener(BroadcastEmitter eventEmitter) { + sEventEmitters.add(eventEmitter); + } + + public static boolean unregisterListener(BroadcastEmitter eventEmitter) { + return sEventEmitters.remove(eventEmitter); + } + + public static void send(String eventName, Object[] args) { + for (BroadcastEmitter emitter : sEventEmitters) { + emitter.onBroadcast(eventName, args); + } + } + +} diff --git a/app/src/main/java/com/stardust/autojs/core/boardcast/BroadcastEmitter.java b/app/src/main/java/com/stardust/autojs/core/boardcast/BroadcastEmitter.java new file mode 100644 index 00000000..a5e77f9d --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/boardcast/BroadcastEmitter.java @@ -0,0 +1,31 @@ +package com.stardust.autojs.core.boardcast; + +import com.stardust.autojs.core.eventloop.EventEmitter; +import com.stardust.autojs.core.looper.Timer; +import com.stardust.autojs.runtime.ScriptBridges; + +/** + * Created by Stardust on 2018/4/1. + */ + +public class BroadcastEmitter extends EventEmitter { + + public BroadcastEmitter(ScriptBridges bridges, Timer timer) { + super(bridges, timer); + Broadcast.registerListener(this); + } + + public boolean onBroadcast(String eventName, Object... args) { + return super.emit(eventName, args); + } + + public void unregister() { + Broadcast.unregisterListener(this); + } + + @Override + public boolean emit(String eventName, Object... args) { + Broadcast.send(eventName, args); + return true; + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/console/ConsoleFloaty.java b/app/src/main/java/com/stardust/autojs/core/console/ConsoleFloaty.java new file mode 100644 index 00000000..8c9b9669 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/console/ConsoleFloaty.java @@ -0,0 +1,133 @@ +package com.stardust.autojs.core.console; + +import android.content.Context; +import android.content.ContextWrapper; +import androidx.annotation.Nullable; +import android.view.ContextThemeWrapper; +import android.view.View; +import android.view.WindowManager; +import android.widget.TextView; + +import org.autojs.autojs.ui.enhancedfloaty.FloatyService; +import org.autojs.autojs.ui.enhancedfloaty.ResizableExpandableFloaty; +import org.autojs.autojs.ui.enhancedfloaty.ResizableExpandableFloatyWindow; +import com.stardust.util.ScreenMetrics; +import com.stardust.util.ViewUtil; +import org.autojs.autojs6.R; + +/** + * Created by Stardust on 2017/4/20. + */ + +public class ConsoleFloaty extends ResizableExpandableFloaty.AbstractResizableExpandableFloaty { + + private ContextWrapper mContextWrapper; + private View mResizer, mMoveCursor; + private TextView mTitleView; + private ConsoleImpl mConsole; + private CharSequence mTitle; + private View mExpandedView; + + public ConsoleFloaty(ConsoleImpl console) { + mConsole = console; + setShouldRequestFocusWhenExpand(false); + setInitialX(100); + setInitialY(1000); + setCollapsedViewUnpressedAlpha(1.0f); + } + + @Override + public int getInitialWidth() { + return WindowManager.LayoutParams.WRAP_CONTENT; //ScreenMetrics.getDeviceScreenWidth() * 2 / 3; + } + + @Override + public int getInitialHeight() { + return WindowManager.LayoutParams.WRAP_CONTENT;//ScreenMetrics.getDeviceScreenHeight() / 3; + } + + @Override + public View inflateCollapsedView(FloatyService service, final ResizableExpandableFloatyWindow window) { + ensureContextWrapper(service); + return View.inflate(mContextWrapper, R.layout.floating_window_collapse, null); + } + + private void ensureContextWrapper(Context context) { + if (mContextWrapper == null) { + mContextWrapper = new ContextThemeWrapper(context, R.style.ConsoleTheme); + } + } + + @Override + public View inflateExpandedView(FloatyService service, ResizableExpandableFloatyWindow window) { + ensureContextWrapper(service); + View view = View.inflate(mContextWrapper, R.layout.floating_console_expand, null); + setListeners(view, window); + setUpConsole(view, window); + setInitialMeasure(view); + mExpandedView = view; + return view; + } + + public View getExpandedView() { + return mExpandedView; + } + + private void setInitialMeasure(final View view) { + view.post(() -> ViewUtil.setViewMeasure(view, ScreenMetrics.getDeviceScreenWidth() * 2 / 3, + ScreenMetrics.getDeviceScreenHeight() / 3)); + } + + private void initConsoleTitle(View view) { + mTitleView = view.findViewById(R.id.title); + if (mTitle != null) { + mTitleView.setText(mTitle); + } + } + + private void setListeners(final View view, final ResizableExpandableFloatyWindow window) { + setWindowOperationIconListeners(view, window); + } + + private void setUpConsole(View view, ResizableExpandableFloatyWindow window) { + ConsoleView consoleView = view.findViewById(R.id.console); + consoleView.setConsole(mConsole); + consoleView.setWindow(window); + initConsoleTitle(view); + } + + private void setWindowOperationIconListeners(View view, final ResizableExpandableFloatyWindow window) { + view.findViewById(R.id.close).setOnClickListener(v -> window.close()); + view.findViewById(R.id.move_or_resize).setOnClickListener(v -> { + if (mMoveCursor.getVisibility() == View.VISIBLE) { + mMoveCursor.setVisibility(View.GONE); + mResizer.setVisibility(View.GONE); + } else { + mMoveCursor.setVisibility(View.VISIBLE); + mResizer.setVisibility(View.VISIBLE); + } + }); + view.findViewById(R.id.minimize).setOnClickListener(v -> window.collapse()); + } + + @Nullable + @Override + public View getResizerView(View expandedView) { + mResizer = expandedView.findViewById(R.id.resizer); + return mResizer; + } + + @Nullable + @Override + public View getMoveCursorView(View expandedView) { + mMoveCursor = expandedView.findViewById(R.id.move_cursor); + return mMoveCursor; + } + + public void setTitle(final CharSequence title) { + mTitle = title; + if (mTitleView != null) { + mTitleView.post(() -> mTitleView.setText(title)); + } + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/console/ConsoleImpl.java b/app/src/main/java/com/stardust/autojs/core/console/ConsoleImpl.java new file mode 100644 index 00000000..7cdc0025 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/console/ConsoleImpl.java @@ -0,0 +1,298 @@ +package com.stardust.autojs.core.console; + +import android.content.Context; +import android.content.Intent; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import android.view.WindowManager; + +import org.autojs.autojs6.R; +import com.stardust.autojs.annotation.ScriptInterface; +import com.stardust.autojs.runtime.ScriptRuntime; +import com.stardust.autojs.runtime.api.AbstractConsole; +import com.stardust.autojs.runtime.api.Console; +import com.stardust.autojs.runtime.exception.ScriptInterruptedException; +import com.stardust.autojs.util.FloatingPermission; +import org.autojs.autojs.ui.enhancedfloaty.FloatyService; +import org.autojs.autojs.ui.enhancedfloaty.ResizableExpandableFloatyWindow; +import com.stardust.util.UiHandler; +import com.stardust.util.ViewUtil; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Created by Stardust on 2017/5/2. + */ + +public class ConsoleImpl extends AbstractConsole { + + public static class LogEntry implements Comparable { + + public int id; + public int level; + public CharSequence content; + public boolean newLine = false; + + public LogEntry(int id, int level, CharSequence content) { + this.id = id; + this.level = level; + this.content = content; + } + + public LogEntry(int id, int level, CharSequence content, boolean newLine) { + this.id = id; + this.level = level; + this.content = content; + this.newLine = newLine; + } + + @Override + public int compareTo(@NonNull LogEntry o) { + return 0; + } + } + + public interface LogListener { + void onNewLog(LogEntry logEntry); + + void onLogClear(); + } + + private final Object WINDOW_SHOW_LOCK = new Object(); + private final Console mGlobalConsole; + private final ArrayList mLogEntries = new ArrayList<>(); + private AtomicInteger mIdCounter = new AtomicInteger(0); + private ResizableExpandableFloatyWindow mFloatyWindow; + private ConsoleFloaty mConsoleFloaty; + private WeakReference mLogListener; + private UiHandler mUiHandler; + private BlockingQueue mInput = new ArrayBlockingQueue<>(1); + private WeakReference mConsoleView; + private volatile boolean mShown = false; + private int mX, mY; + + public ConsoleImpl(UiHandler uiHandler) { + this(uiHandler, null); + } + + public ConsoleImpl(UiHandler uiHandler, Console globalConsole) { + mUiHandler = uiHandler; + mConsoleFloaty = new ConsoleFloaty(this); + mGlobalConsole = globalConsole; + mFloatyWindow = new ResizableExpandableFloatyWindow(mConsoleFloaty) { + @Override + public void onCreate(FloatyService service, WindowManager manager) { + super.onCreate(service, manager); + expand(); + mFloatyWindow.getWindowBridge().updatePosition(mX, mY); + synchronized (WINDOW_SHOW_LOCK) { + mShown = true; + WINDOW_SHOW_LOCK.notifyAll(); + } + } + }; + } + + public void setConsoleView(ConsoleView consoleView) { + mConsoleView = new WeakReference<>(consoleView); + setLogListener(consoleView); + synchronized (this) { + this.notify(); + } + } + + + public void setLogListener(LogListener logListener) { + mLogListener = new WeakReference<>(logListener); + } + + public ArrayList getAllLogs() { + return mLogEntries; + } + + public void printAllStackTrace(Throwable t) { + println(android.util.Log.ERROR, ScriptRuntime.getStackTrace(t, true)); + } + + public String getStackTrace(Throwable t) { + return ScriptRuntime.getStackTrace(t, false); + } + + @Override + public String println(int level, CharSequence charSequence) { + LogEntry logEntry = new LogEntry(mIdCounter.getAndIncrement(), level, charSequence, true); + synchronized (mLogEntries) { + mLogEntries.add(logEntry); + } + if (mGlobalConsole != null) { + mGlobalConsole.println(level, charSequence); + } + if (mLogListener != null && mLogListener.get() != null) { + mLogListener.get().onNewLog(logEntry); + } + return null; + } + + + @Override + public void write(int level, CharSequence charSequence) { + println(level, charSequence); + } + + + @Override + public void clear() { + synchronized (mLogEntries) { + mLogEntries.clear(); + } + if (mLogListener != null && mLogListener.get() != null) { + mLogListener.get().onLogClear(); + } + } + + @Override + public void show() { + if (mShown) { + return; + } + if (!FloatingPermission.canDrawOverlays(mUiHandler.getContext())) { + FloatingPermission.manageDrawOverlays(mUiHandler.getContext()); + mUiHandler.toast(R.string.error_no_display_over_other_apps_permission); + return; + } + startFloatyService(); + mUiHandler.post(() -> { + try { + FloatyService.addWindow(mFloatyWindow); + // SecurityException: https://github.com/hyb1996-guest/AutoJsIssueReport/issues/4781 + } catch (WindowManager.BadTokenException | SecurityException e) { + e.printStackTrace(); + mUiHandler.toast(R.string.error_no_display_over_other_apps_permission); + } + }); + synchronized (WINDOW_SHOW_LOCK) { + if (mShown) { + return; + } + try { + WINDOW_SHOW_LOCK.wait(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + private void startFloatyService() { + Context context = mUiHandler.getContext(); + context.startService(new Intent(context, FloatyService.class)); + } + + @Override + public void hide() { + mUiHandler.post(() -> { + synchronized (WINDOW_SHOW_LOCK) { + if (!mShown) + return; + try { + mFloatyWindow.close(); + } catch (IllegalArgumentException ignored) { + + } + mShown = false; + } + }); + } + + + public void setSize(int w, int h) { + if (mShown) { + mUiHandler.post(() -> { + if (mShown) { + ViewUtil.setViewMeasure(mConsoleFloaty.getExpandedView(), w, h); + } + }); + } + } + + public void setPosition(int x, int y) { + mX = x; + mY = y; + if (mShown) { + mUiHandler.post(() -> { + if (mShown) + mFloatyWindow.getWindowBridge().updatePosition(x, y); + }); + } + } + + @ScriptInterface + public String rawInput() { + if (mConsoleView == null || mConsoleView.get() == null) { + if (!mShown) { + show(); + } + waitForConsoleView(); + } + mConsoleView.get().showEditText(); + try { + return mInput.take(); + } catch (InterruptedException e) { + throw new ScriptInterruptedException(); + } + } + + private void waitForConsoleView() { + synchronized (this) { + try { + this.wait(); + } catch (InterruptedException e) { + throw new ScriptInterruptedException(); + } + } + } + + @ScriptInterface + public String rawInput(Object data, Object... param) { + log(data, param); + return rawInput(); + } + + boolean submitInput(@NonNull CharSequence input) { + return mInput.offer(input.toString()); + } + + @Override + public void setTitle(CharSequence title) { + mConsoleFloaty.setTitle(title); + } + + @Override + public void error(@Nullable Object data, Object... options) { + if (data instanceof Throwable) { + data = getStackTrace((Throwable) data); + } + if (options != null && options.length > 0) { + StringBuilder sb = new StringBuilder(data == null ? "" : data.toString()); + ArrayList newOptions = new ArrayList<>(); + for (Object option : options) { + if (option instanceof Throwable) { + sb.append(getStackTrace((Throwable) option)).append(" "); + } else { + newOptions.add(option); + } + } + data = sb.toString(); + if (newOptions.isEmpty()) { + super.error(data, newOptions.toArray()); + } else { + super.error(data); + } + } else { + super.error(data, options); + } + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/console/ConsoleView.java b/app/src/main/java/com/stardust/autojs/core/console/ConsoleView.java new file mode 100644 index 00000000..33887b13 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/console/ConsoleView.java @@ -0,0 +1,238 @@ +package com.stardust.autojs.core.console; + +import android.content.Context; +import android.content.res.TypedArray; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import android.util.AttributeSet; +import android.util.Log; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.autojs.autojs.ui.enhancedfloaty.ResizableExpandableFloatyWindow; +import org.autojs.autojs6.R; +import com.stardust.util.MapBuilder; +import com.stardust.util.SparseArrayEntries; + +import java.util.ArrayList; +import java.util.Map; + +/** + * Created by Stardust on 2017/5/2. + *

+ * TODO: 优化为无锁形式 + */ +public class ConsoleView extends FrameLayout implements ConsoleImpl.LogListener { + + private static final Map ATTRS = new MapBuilder() + .put(R.styleable.ConsoleView_color_verbose, Log.VERBOSE) + .put(R.styleable.ConsoleView_color_debug, Log.DEBUG) + .put(R.styleable.ConsoleView_color_info, Log.INFO) + .put(R.styleable.ConsoleView_color_warn, Log.WARN) + .put(R.styleable.ConsoleView_color_error, Log.ERROR) + .put(R.styleable.ConsoleView_color_assert, Log.ASSERT) + .build(); + + static final SparseArray COLORS = new SparseArrayEntries() + .entry(Log.VERBOSE, 0xdfc0c0c0) + .entry(Log.DEBUG, 0xdfffffff) + .entry(Log.INFO, 0xff64dd17) + .entry(Log.WARN, 0xff2962ff) + .entry(Log.ERROR, 0xffd50000) + .entry(Log.ASSERT, 0xffff534e) + .sparseArray(); + + private static final int REFRESH_INTERVAL = 100; + private SparseArray mColors = COLORS.clone(); + private ConsoleImpl mConsole; + private RecyclerView mLogListRecyclerView; + private EditText mEditText; + private ResizableExpandableFloatyWindow mWindow; + private LinearLayout mInputContainer; + private boolean mShouldStopRefresh = false; + private ArrayList mLogEntries = new ArrayList<>(); + + public ConsoleView(Context context) { + super(context); + init(null); + } + + public ConsoleView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(attrs); + } + + public ConsoleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(attrs); + } + + public void setColors(SparseArray colors) { + mColors = colors; + } + + private void init(AttributeSet attrs) { + inflate(getContext(), R.layout.console_view, this); + if (attrs != null) { + TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.ConsoleView); + for (Map.Entry attr : ATTRS.entrySet()) { + int styleable = attr.getKey(); + int logLevel = attr.getValue(); + mColors.put(logLevel, typedArray.getColor(styleable, mColors.get(logLevel))); + } + } + mLogListRecyclerView = findViewById(R.id.log_list); + LinearLayoutManager manager = new LinearLayoutManager(getContext()); + mLogListRecyclerView.setLayoutManager(manager); + mLogListRecyclerView.setAdapter(new Adapter()); + initEditText(); + initSubmitButton(); + } + + private void initSubmitButton() { + final Button submit = findViewById(R.id.submit); + submit.setOnClickListener(v -> { + CharSequence input = mEditText.getText(); + submitInput(input); + }); + } + + private void submitInput(CharSequence input) { + if (android.text.TextUtils.isEmpty(input)) { + return; + } + if (mConsole.submitInput(input)) { + mEditText.setText(""); + } + } + + private void initEditText() { + mEditText = findViewById(R.id.input); + mEditText.setFocusableInTouchMode(true); + mInputContainer = findViewById(R.id.input_container); + OnClickListener listener = v -> { + if (mWindow != null) { + mWindow.requestWindowFocus(); + mEditText.requestFocus(); + } + }; + mEditText.setOnClickListener(listener); + mInputContainer.setOnClickListener(listener); + } + + public void setConsole(ConsoleImpl console) { + mConsole = console; + mConsole.setConsoleView(this); + } + + @Override + public void onNewLog(ConsoleImpl.LogEntry logEntry) { + + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mShouldStopRefresh = false; + postDelayed(new Runnable() { + @Override + public void run() { + refreshLog(); + if (!mShouldStopRefresh) { + postDelayed(this, REFRESH_INTERVAL); + } + } + }, REFRESH_INTERVAL); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mShouldStopRefresh = true; + } + + + @Override + public void onLogClear() { + post(() -> { + mLogEntries.clear(); + mLogListRecyclerView.getAdapter().notifyDataSetChanged(); + }); + } + + private void refreshLog() { + if (mConsole == null) + return; + int oldSize = mLogEntries.size(); + ArrayList logEntries = mConsole.getAllLogs(); + synchronized (mConsole.getAllLogs()) { + final int size = logEntries.size(); + if (size == 0) { + return; + } + if (oldSize >= size) { + return; + } + if (oldSize == 0) { + mLogEntries.addAll(logEntries); + } else { + for (int i = oldSize; i < size; i++) { + mLogEntries.add(logEntries.get(i)); + } + } + mLogListRecyclerView.getAdapter().notifyItemRangeInserted(oldSize, size - 1); + mLogListRecyclerView.scrollToPosition(size - 1); + } + } + + public void setWindow(ResizableExpandableFloatyWindow window) { + mWindow = window; + } + + public void showEditText() { + post(() -> { + mWindow.requestWindowFocus(); + //mInputContainer.setVisibility(VISIBLE); + mEditText.requestFocus(); + }); + } + + private class ViewHolder extends RecyclerView.ViewHolder { + + TextView textView; + + public ViewHolder(View itemView) { + super(itemView); + textView = (TextView) itemView; + } + } + + private class Adapter extends RecyclerView.Adapter { + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + return new ViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.console_view_item, parent, false)); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + ConsoleImpl.LogEntry logEntry = mLogEntries.get(position); + holder.textView.setText(logEntry.content); + holder.textView.setTextColor(mColors.get(logEntry.level)); + } + + @Override + public int getItemCount() { + return mLogEntries.size(); + } + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/console/GlobalConsole.java b/app/src/main/java/com/stardust/autojs/core/console/GlobalConsole.java new file mode 100644 index 00000000..77edbf6f --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/console/GlobalConsole.java @@ -0,0 +1,74 @@ +package com.stardust.autojs.core.console; + +import com.stardust.util.UiHandler; + +import org.apache.log4j.Level; +import org.apache.log4j.Logger; +import org.apache.log4j.Priority; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + + +/** + * Created by Stardust on 2017/10/22. + */ + +public class GlobalConsole extends ConsoleImpl { + private static final String LOG_tAG = "GlobalConsole"; + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault()); + private static final Logger LOGGER = Logger.getLogger(GlobalConsole.class); + + public GlobalConsole(UiHandler uiHandler) { + super(uiHandler); + } + + @Override + public String println(int level, CharSequence charSequence) { + String log = String.format(Locale.getDefault(), "%s/%s: %s", + DATE_FORMAT.format(new Date()), getLevelChar(level), charSequence.toString()); + LOGGER.log(toLog4jLevel(level), log); + android.util.Log.d(LOG_tAG, log); + super.println(level, log); + return log; + } + + private Priority toLog4jLevel(int level) { + switch (level) { + case android.util.Log.VERBOSE: + return Level.DEBUG; + case android.util.Log.DEBUG: + return Level.DEBUG; + case android.util.Log.INFO: + return Level.INFO; + case android.util.Log.WARN: + return Level.WARN; + case android.util.Log.ERROR: + return Level.ERROR; + case android.util.Log.ASSERT: + return Level.FATAL; + } + throw new IllegalArgumentException("invalid level = " + level); + } + + private String getLevelChar(int level) { + switch (level) { + case android.util.Log.VERBOSE: + return "V"; + case android.util.Log.DEBUG: + return "D"; + case android.util.Log.INFO: + return "I"; + case android.util.Log.WARN: + return "W"; + case android.util.Log.ERROR: + return "E"; + case android.util.Log.ASSERT: + return "A"; + + } + return ""; + } + +} diff --git a/app/src/main/java/com/stardust/autojs/core/console/NoOpConsole.java b/app/src/main/java/com/stardust/autojs/core/console/NoOpConsole.java new file mode 100644 index 00000000..4b23a60f --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/console/NoOpConsole.java @@ -0,0 +1,72 @@ +package com.stardust.autojs.core.console; + +import androidx.annotation.Nullable; + +import com.stardust.autojs.runtime.api.Console; + +/** + * Created by Stardust on 2017/9/21. + */ + +public class NoOpConsole implements Console { + @Override + public void verbose(@Nullable Object data, Object... options) { + + } + + @Override + public void log(@Nullable Object data, Object... options) { + + } + + @Override + public void print(int level, Object data, Object... options) { + + } + + @Override + public void info(@Nullable Object data, Object... options) { + + } + + @Override + public void warn(@Nullable Object data, Object... options) { + + } + + @Override + public void error(@Nullable Object data, Object... options) { + + } + + @Override + public void assertTrue(boolean value, @Nullable Object data, Object... options) { + + } + + @Override + public void clear() { + + } + + @Override + public void show() { + + } + + @Override + public void hide() { + + } + + @Override + public String println(int level, CharSequence charSequence) { + + return null; + } + + @Override + public void setTitle(CharSequence title) { + + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/database/CursorHelper.java b/app/src/main/java/com/stardust/autojs/core/database/CursorHelper.java new file mode 100644 index 00000000..5bcfacd4 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/database/CursorHelper.java @@ -0,0 +1,24 @@ +package com.stardust.autojs.core.database; + +import android.database.Cursor; + +public class CursorHelper { + + + public static Object getValue(Cursor cursor, int column) { + switch (cursor.getType(column)) { + case Cursor.FIELD_TYPE_STRING: + return cursor.getShort(column); + case Cursor.FIELD_TYPE_FLOAT: + return cursor.getFloat(column); + case Cursor.FIELD_TYPE_INTEGER: + return cursor.getInt(column); + case Cursor.FIELD_TYPE_NULL: + return null; + case Cursor.FIELD_TYPE_BLOB: + return cursor.getBlob(column); + default: + throw new IllegalArgumentException("unknown type"); + } + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/database/Database.java b/app/src/main/java/com/stardust/autojs/core/database/Database.java new file mode 100644 index 00000000..03780e9a --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/database/Database.java @@ -0,0 +1,90 @@ +package com.stardust.autojs.core.database; + +import android.content.Context; +import android.database.DatabaseErrorHandler; +import android.database.SQLException; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteTransactionListener; + +public class Database { + + + private DatabasesOpenHelper mHelper; + private SQLiteDatabase mWritableDatabase; + private SQLiteDatabase mReadableDatabase; + + public void executeSql(String sql){ + mWritableDatabase.execSQL(sql); + } + + public void transaction(TransactionCallback callback, TransactionErrorCallback errorCallback, DatabaseVoidCallback successCallback){ + transactionInternal(mWritableDatabase, callback, errorCallback, successCallback); + } + + public void readTransaction(TransactionCallback callback, TransactionErrorCallback errorCallback, DatabaseVoidCallback successCallback){ + transactionInternal(mReadableDatabase, callback, errorCallback, successCallback); + } + + public void changeVersion(int oldVersion, int newVersion, TransactionCallback callback, TransactionErrorCallback errorCallback, DatabaseVoidCallback successCallback){ + transactionInternal(mWritableDatabase, new TransactionCallback() { + @Override + public void handleEvent(Transaction transaction) { + //TODO + if(transaction.getDatabase().getVersion() == oldVersion){ + transaction.getDatabase().setVersion(newVersion); + callback.handleEvent(transaction); + } + } + }, errorCallback, successCallback); + } + + private void transactionInternal(SQLiteDatabase database, TransactionCallback callback, TransactionErrorCallback errorCallback, DatabaseVoidCallback successCallback){ + database.beginTransactionWithListener(new SQLiteTransactionListener() { + @Override + public void onBegin() { + Transaction transaction = new Transaction(database); + try { + callback.handleEvent(transaction); + transaction.succeed(); + }catch (SQLException e){ + errorCallback.handleEvent(e); + }finally { + transaction.end(); + } + } + + @Override + public void onCommit() { + successCallback.handleEvent(); + } + + @Override + public void onRollback() { + } + }); + } + + + private static class DatabasesOpenHelper extends SQLiteOpenHelper { + + public DatabasesOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) { + super(context, name, factory, version); + } + + public DatabasesOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version, DatabaseErrorHandler errorHandler) { + super(context, name, factory, version, errorHandler); + } + + @Override + public void onCreate(SQLiteDatabase db) { + + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + + } + } + +} diff --git a/app/src/main/java/com/stardust/autojs/core/database/DatabaseResultSet.java b/app/src/main/java/com/stardust/autojs/core/database/DatabaseResultSet.java new file mode 100644 index 00000000..0f79c574 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/database/DatabaseResultSet.java @@ -0,0 +1,61 @@ +package com.stardust.autojs.core.database; + +import android.database.Cursor; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +public class DatabaseResultSet { + + + public static class RowList { + + public final int length; + private final ArrayList> mData; + + + public RowList(ArrayList> data) { + mData = data; + length = mData.size(); + } + + public Object item(int i) { + return mData.get(i); + } + } + + public final long insertId; + public final long rowsAffected; + public final RowList rows; + + public static DatabaseResultSet fromCursor(Cursor cursor) { + ArrayList> rows = new ArrayList<>(); + if (!cursor.moveToFirst()) { + return null; + } + int columnCount = cursor.getColumnCount(); + long insertId = cursor.getLong(0); + rows.add(readRowAsMap(cursor, columnCount)); + while (cursor.moveToNext()) { + rows.add(readRowAsMap(cursor, columnCount)); + } + cursor.close(); + return new DatabaseResultSet(insertId, new RowList(rows)); + } + + private static Map readRowAsMap(Cursor cursor, int columnCount) { + Map map = new HashMap<>(); + for (int i = 0; i < columnCount; i++) { + map.put(cursor.getColumnName(i), CursorHelper.getValue(cursor, columnCount)); + } + return map; + } + + public DatabaseResultSet(long insertId, RowList rowList) { + this.insertId = insertId; + this.rowsAffected = rowList.length; + this.rows = rowList; + } + +} diff --git a/app/src/main/java/com/stardust/autojs/core/database/DatabaseVoidCallback.java b/app/src/main/java/com/stardust/autojs/core/database/DatabaseVoidCallback.java new file mode 100644 index 00000000..7d945ccc --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/database/DatabaseVoidCallback.java @@ -0,0 +1,5 @@ +package com.stardust.autojs.core.database; + +public interface DatabaseVoidCallback { + void handleEvent(); +} diff --git a/app/src/main/java/com/stardust/autojs/core/database/Databases.java b/app/src/main/java/com/stardust/autojs/core/database/Databases.java new file mode 100644 index 00000000..5e8ed8e5 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/database/Databases.java @@ -0,0 +1,11 @@ +package com.stardust.autojs.core.database; + +public class Databases { + + public static Database openDatabase(String name, int version, String desc, long size){ + return new Database(); + } + + + +} diff --git a/app/src/main/java/com/stardust/autojs/core/database/StatementCallback.java b/app/src/main/java/com/stardust/autojs/core/database/StatementCallback.java new file mode 100644 index 00000000..f8e721b0 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/database/StatementCallback.java @@ -0,0 +1,8 @@ +package com.stardust.autojs.core.database; + +import java.sql.ResultSet; + +public interface StatementCallback { + + void handleEvent(Transaction transaction, DatabaseResultSet resultSet); +} diff --git a/app/src/main/java/com/stardust/autojs/core/database/StatementErrorCallback.java b/app/src/main/java/com/stardust/autojs/core/database/StatementErrorCallback.java new file mode 100644 index 00000000..504f7fc8 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/database/StatementErrorCallback.java @@ -0,0 +1,9 @@ +package com.stardust.autojs.core.database; + +import android.database.SQLException; + +public interface StatementErrorCallback { + + boolean handleEvent(Transaction transaction, SQLException error); + +} diff --git a/app/src/main/java/com/stardust/autojs/core/database/Transaction.java b/app/src/main/java/com/stardust/autojs/core/database/Transaction.java new file mode 100644 index 00000000..9f155403 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/database/Transaction.java @@ -0,0 +1,32 @@ +package com.stardust.autojs.core.database; + +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import java.sql.ResultSet; + +public class Transaction { + + private final SQLiteDatabase mDatabase; + + public SQLiteDatabase getDatabase() { + return mDatabase; + } + + public Transaction(SQLiteDatabase database) { + mDatabase = database; + } + + public void executeSql(String sqlStatement, String[] arguments, StatementCallback callback, StatementErrorCallback errorCallback){ + Cursor cursor = mDatabase.rawQuery(sqlStatement, arguments); + callback.handleEvent(this, DatabaseResultSet.fromCursor(cursor)); + } + + void succeed() { + mDatabase.setTransactionSuccessful(); + } + + void end() { + mDatabase.endTransaction(); + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/database/TransactionCallback.java b/app/src/main/java/com/stardust/autojs/core/database/TransactionCallback.java new file mode 100644 index 00000000..ab87a56d --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/database/TransactionCallback.java @@ -0,0 +1,5 @@ +package com.stardust.autojs.core.database; + +public interface TransactionCallback { + void handleEvent(Transaction transaction); +} diff --git a/app/src/main/java/com/stardust/autojs/core/database/TransactionErrorCallback.java b/app/src/main/java/com/stardust/autojs/core/database/TransactionErrorCallback.java new file mode 100644 index 00000000..249e052a --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/database/TransactionErrorCallback.java @@ -0,0 +1,8 @@ +package com.stardust.autojs.core.database; + +import android.database.SQLException; + +public interface TransactionErrorCallback { + + void handleEvent(SQLException e); +} diff --git a/app/src/main/java/com/stardust/autojs/core/eventloop/EventEmitter.java b/app/src/main/java/com/stardust/autojs/core/eventloop/EventEmitter.java new file mode 100644 index 00000000..e9841608 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/eventloop/EventEmitter.java @@ -0,0 +1,200 @@ +package com.stardust.autojs.core.eventloop; + +import androidx.annotation.NonNull; + + +import com.stardust.autojs.core.looper.Timer; +import com.stardust.autojs.runtime.ScriptBridges; +import com.stardust.autojs.runtime.exception.ScriptException; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.TooManyListenersException; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Created by Stardust on 2017/7/19. + */ + +public class EventEmitter { + + private static class ListenerWrapper { + Object listener; + boolean isOnce; + + public ListenerWrapper(Object listener, boolean isOnce) { + this.listener = listener; + this.isOnce = isOnce; + } + } + + + private class Listeners { + private CopyOnWriteArrayList mListenerWrappers = new CopyOnWriteArrayList<>(); + + void add(Object listener, boolean once) { + ensureListenersNotAtLimit(); + mListenerWrappers.add(new ListenerWrapper(listener, once)); + } + + private void ensureListenersNotAtLimit() { + if (mMaxListeners != 0 && mListenerWrappers.size() >= mMaxListeners) { + throw new ScriptException(new TooManyListenersException("max = " + mMaxListeners)); + } + } + + boolean empty() { + return mListenerWrappers.isEmpty(); + } + + void emit(Object[] args) { + Iterator listenerIterator = mListenerWrappers.iterator(); + while (listenerIterator.hasNext()) { + ListenerWrapper listenerWrapper = listenerIterator.next(); + if (mTimer != null) { + mTimer.setImmediate(listenerWrapper.listener, args); + } else { + mBridges.callFunction(listenerWrapper.listener, EventEmitter.this, args); + } + if (listenerWrapper.isOnce) { + mListenerWrappers.remove(listenerWrapper); + } + } + } + + int count() { + return mListenerWrappers.size(); + } + + Object[] toArray() { + Iterator listenerIterator = mListenerWrappers.iterator(); + ArrayList listeners = new ArrayList<>(mListenerWrappers.size()); + while (listenerIterator.hasNext()) { + listeners.add(listenerIterator.next().listener); + } + return listeners.toArray(new Object[listeners.size()]); + } + + void prepend(Object listener, boolean once) { + ensureListenersNotAtLimit(); + mListenerWrappers.add(0, new ListenerWrapper(listener, once)); + } + + void remove(Object listener) { + Iterator listenerIterator = mListenerWrappers.iterator(); + while (listenerIterator.hasNext()) { + ListenerWrapper l = listenerIterator.next(); + if (l.listener == listener) { + listenerIterator.remove(); + break; + } + } + } + } + + private Map mListenersMap = new HashMap<>(); + public static int defaultMaxListeners = 10; + private int mMaxListeners = defaultMaxListeners; + protected ScriptBridges mBridges; + private Timer mTimer; + + public EventEmitter(ScriptBridges bridges) { + mBridges = bridges; + } + + public EventEmitter(ScriptBridges bridges, Timer timer) { + mTimer = timer; + mBridges = bridges; + } + + public EventEmitter once(String eventName, Object listener) { + getListeners(eventName).add(listener, true); + return this; + } + @NonNull + private Listeners getListeners(String eventName) { + Listeners listeners = mListenersMap.get(eventName); + if (listeners == null) { + listeners = new Listeners(); + mListenersMap.put(eventName, listeners); + } + return listeners; + } + + + public EventEmitter on(String eventName, Object listener) { + getListeners(eventName).add(listener, false); + return this; + } + + public EventEmitter addListener(String eventName, Object listener) { + return on(eventName, listener); + } + + public boolean emit(String eventName, Object... args) { + Listeners listeners = mListenersMap.get(eventName); + if (listeners == null || listeners.empty()) + return false; + listeners.emit(args); + return true; + } + + public String[] eventNames() { + return mListenersMap.keySet().toArray(new String[0]); + } + + public int listenerCount(String eventName) { + Listeners listeners = mListenersMap.get(eventName); + if (listeners == null) + return 0; + return listeners.count(); + } + + public Object[] listeners(String eventName) { + return getListeners(eventName).toArray(); + } + + public EventEmitter prependListener(String eventName, Object listener) { + getListeners(eventName).prepend(listener, false); + return this; + } + + public EventEmitter prependOnceListener(String eventName, Object listener) { + getListeners(eventName).prepend(listener, true); + return this; + } + + public EventEmitter removeAllListeners() { + mListenersMap.clear(); + return this; + } + + public EventEmitter removeAllListeners(String eventName) { + mListenersMap.remove(eventName); + return this; + } + + public EventEmitter removeListener(String eventName, Object listener) { + Listeners listeners = mListenersMap.get(eventName); + if (listeners != null) + listeners.remove(listener); + return this; + } + + public EventEmitter setMaxListeners(int n) { + mMaxListeners = n; + return this; + } + + public int getMaxListeners() { + return mMaxListeners; + } + + public static int defaultMaxListeners() { + return defaultMaxListeners; + } + + +} diff --git a/app/src/main/java/com/stardust/autojs/core/eventloop/SimpleEvent.java b/app/src/main/java/com/stardust/autojs/core/eventloop/SimpleEvent.java new file mode 100644 index 00000000..5fa98a52 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/eventloop/SimpleEvent.java @@ -0,0 +1,6 @@ +package com.stardust.autojs.core.eventloop; + +public class SimpleEvent { + + public boolean consumed = false; +} diff --git a/app/src/main/java/com/stardust/autojs/core/floaty/BaseResizableFloatyWindow.java b/app/src/main/java/com/stardust/autojs/core/floaty/BaseResizableFloatyWindow.java new file mode 100644 index 00000000..94cfa450 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/floaty/BaseResizableFloatyWindow.java @@ -0,0 +1,160 @@ +package com.stardust.autojs.core.floaty; + +import android.content.Context; + +import android.widget.ImageView; +import androidx.annotation.Nullable; + +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.FrameLayout; + +import org.autojs.autojs6.R; +import com.stardust.autojs.core.ui.inflater.inflaters.Exceptions; +import com.stardust.autojs.runtime.exception.ScriptInterruptedException; +import com.stardust.concurrent.VolatileDispose; +import org.autojs.autojs.ui.enhancedfloaty.FloatyService; +import org.autojs.autojs.ui.enhancedfloaty.ResizableFloaty; +import org.autojs.autojs.ui.enhancedfloaty.ResizableFloatyWindow; +import org.autojs.autojs.ui.enhancedfloaty.WindowBridge; + +/** + * Created by Stardust on 2017/12/5. + */ + +public class BaseResizableFloatyWindow extends ResizableFloatyWindow { + + public interface ViewSupplier { + + View inflate(Context context, ViewGroup parent); + + } + + private VolatileDispose mInflateException = new VolatileDispose<>(); + private View mCloseButton; + private int mOffset; + + + public BaseResizableFloatyWindow(Context context, ViewSupplier viewSupplier) { + this(new MyFloaty(context, viewSupplier)); + mOffset = context.getResources().getDimensionPixelSize(R.dimen.floaty_window_offset); + } + + private BaseResizableFloatyWindow(MyFloaty floaty) { + super(floaty); + } + + public RuntimeException waitForCreation() { + return mInflateException.blockedGetOrThrow(ScriptInterruptedException.class); + } + + @Override + protected WindowBridge onCreateWindowBridge(WindowManager.LayoutParams params) { + return new WindowBridge.DefaultImpl(params, getWindowManager(), getWindowView()) { + @Override + public int getX() { + return super.getX() + mOffset; + } + + @Override + public int getY() { + return super.getY() + mOffset; + } + + @Override + public void updatePosition(int x, int y) { + super.updatePosition(x - mOffset, y - mOffset); + } + }; + } + + @Override + public void onCreate(FloatyService service, WindowManager manager) { + try { + super.onCreate(service, manager); + } catch (RuntimeException e) { + mInflateException.setAndNotify(e); + return; + } + mInflateException.setAndNotify(Exceptions.NO_EXCEPTION); + } + + public void setOnCloseButtonClickListener(View.OnClickListener listener) { + mCloseButton.setOnClickListener(listener); + } + + public void setAdjustEnabled(boolean enabled) { + if (!enabled) { + getMoveCursor().setVisibility(View.GONE); + getResizer().setVisibility(View.GONE); + mCloseButton.setVisibility(View.GONE); + } else { + getMoveCursor().setVisibility(View.VISIBLE); + getResizer().setVisibility(View.VISIBLE); + mCloseButton.setVisibility(View.VISIBLE); + } + } + + public boolean isAdjustEnabled() { + return getMoveCursor().getVisibility() == View.VISIBLE; + } + + @Override + protected void onViewCreated(View view) { + super.onViewCreated(view); + mCloseButton = view.findViewById(R.id.close); + } + + public void disableWindowFocus() { + WindowManager.LayoutParams windowLayoutParams = getWindowLayoutParams(); + windowLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + updateWindowLayoutParams(windowLayoutParams); + } + + public void requestWindowFocus() { + WindowManager.LayoutParams windowLayoutParams = getWindowLayoutParams(); + windowLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + updateWindowLayoutParams(windowLayoutParams); + getWindowView().requestLayout(); + } + + private static class MyFloaty implements ResizableFloaty { + + + private ViewSupplier mContentViewSupplier; + private View mRootView; + private Context mContext; + + + public MyFloaty(Context context, ViewSupplier supplier) { + mContentViewSupplier = supplier; + mContext = context; + } + + @Override + public View inflateView(FloatyService floatyService, ResizableFloatyWindow resizableFloatyWindow) { + mRootView = View.inflate(mContext, R.layout.floaty_window, null); + FrameLayout container = mRootView.findViewById(R.id.container); + View contentView = mContentViewSupplier.inflate(mContext, container); + return mRootView; + } + + @Nullable + @Override + public ImageView getResizerView(View view) { + return view.findViewById(R.id.resizer); + } + + @Nullable + @Override + public ImageView getMoveCursorView(View view) { + return view.findViewById(R.id.move_cursor); + } + + @Override + public ImageView getCloseButtonView(View view) { + return view.findViewById(R.id.close); + } + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/floaty/RawWindow.java b/app/src/main/java/com/stardust/autojs/core/floaty/RawWindow.java new file mode 100644 index 00000000..1169f5d8 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/floaty/RawWindow.java @@ -0,0 +1,106 @@ +package com.stardust.autojs.core.floaty; + +import android.graphics.PixelFormat; +import android.os.Build; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; + +import org.autojs.autojs6.R; +import com.stardust.autojs.core.ui.inflater.inflaters.Exceptions; +import com.stardust.autojs.runtime.exception.ScriptInterruptedException; +import com.stardust.concurrent.VolatileBox; +import com.stardust.concurrent.VolatileDispose; +import org.autojs.autojs.ui.enhancedfloaty.FloatyService; +import org.autojs.autojs.ui.enhancedfloaty.FloatyWindow; +import org.autojs.autojs.ui.enhancedfloaty.WindowBridge; +import org.autojs.autojs.ui.enhancedfloaty.util.WindowTypeCompat; + +public class RawWindow extends FloatyWindow { + + + + public interface RawFloaty { + + View inflateWindowView(FloatyService service, ViewGroup parent); + } + + private VolatileDispose mInflateException = new VolatileDispose<>(); + private RawFloaty mRawFloaty; + private View mContentView; + + public RawWindow(RawFloaty rawFloaty) { + mRawFloaty = rawFloaty; + } + + @Override + public void onCreate(FloatyService floatyService, WindowManager windowManager) { + try { + super.onCreate(floatyService, windowManager); + } catch (RuntimeException e) { + mInflateException.setAndNotify(e); + return; + } + mInflateException.setAndNotify(Exceptions.NO_EXCEPTION); + } + + @Override + protected View onCreateView(FloatyService floatyService) { + ViewGroup windowView = (ViewGroup) View.inflate(floatyService, R.layout.raw_window, null); + mContentView = mRawFloaty.inflateWindowView(floatyService, windowView); + return windowView; + } + + public RuntimeException waitForCreation() { + return mInflateException.blockedGetOrThrow(ScriptInterruptedException.class); + } + + public View getContentView() { + return mContentView; + } + + @Override + protected WindowManager.LayoutParams onCreateWindowLayoutParams() { + int flags = + WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS + | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL + | WindowManager.LayoutParams.FLAG_FULLSCREEN + | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + flags |= WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS; + } + WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams( + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.WRAP_CONTENT, + WindowTypeCompat.getWindowType(), + flags, + PixelFormat.TRANSLUCENT); + layoutParams.gravity = Gravity.TOP | Gravity.START; + return layoutParams; + } + + public void disableWindowFocus() { + WindowManager.LayoutParams windowLayoutParams = getWindowLayoutParams(); + windowLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + updateWindowLayoutParams(windowLayoutParams); + } + + public void requestWindowFocus() { + WindowManager.LayoutParams windowLayoutParams = getWindowLayoutParams(); + windowLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + updateWindowLayoutParams(windowLayoutParams); + getWindowView().requestLayout(); + } + + public void setTouchable(boolean touchable) { + WindowManager.LayoutParams windowLayoutParams = getWindowLayoutParams(); + if (touchable) { + windowLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; + } else { + windowLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; + } + updateWindowLayoutParams(windowLayoutParams); + } + +} diff --git a/app/src/main/java/com/stardust/autojs/core/graphics/ScriptCanvas.java b/app/src/main/java/com/stardust/autojs/core/graphics/ScriptCanvas.java new file mode 100644 index 00000000..a71e6aa1 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/graphics/ScriptCanvas.java @@ -0,0 +1,469 @@ +package com.stardust.autojs.core.graphics; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.DrawFilter; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Picture; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Region; +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; + +import com.stardust.autojs.core.image.ImageWrapper; +import com.stardust.autojs.runtime.ScriptRuntime; + +/** + * Created by Stardust on 2018/3/22. + */ + +public class ScriptCanvas { + + private Canvas mCanvas; + private Bitmap mBitmap; + + public ScriptCanvas(int width, int height) { + this(Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)); + } + + public ScriptCanvas(@NonNull Bitmap bitmap) { + mCanvas = new Canvas(bitmap); + mBitmap = bitmap; + } + + public ScriptCanvas(@NonNull ImageWrapper image) { + this(image.getBitmap().copy(image.getBitmap().getConfig(), true)); + } + + public ScriptCanvas() { + + } + + public Canvas getAndroidCanvas() { + return mCanvas; + } + + void setCanvas(Canvas canvas) { + mCanvas = canvas; + } + + public ImageWrapper toImage() { + return ImageWrapper.ofBitmap(mBitmap.copy(mBitmap.getConfig(), true)); + } + + public boolean isHardwareAccelerated() { + return mCanvas.isHardwareAccelerated(); + } + + public void setBitmap(@Nullable Bitmap bitmap) { + mCanvas.setBitmap(bitmap); + } + + public boolean isOpaque() { + return mCanvas.isOpaque(); + } + + public int getWidth() { + return mCanvas.getWidth(); + } + + public int getHeight() { + return mCanvas.getHeight(); + } + + public int getDensity() { + return mCanvas.getDensity(); + } + + public void setDensity(int density) { + mCanvas.setDensity(density); + } + + public int getMaximumBitmapWidth() { + return mCanvas.getMaximumBitmapWidth(); + } + + public int getMaximumBitmapHeight() { + return mCanvas.getMaximumBitmapHeight(); + } + + public int save() { + return mCanvas.save(); + } + + public int saveLayer(@Nullable RectF bounds, @Nullable Paint paint, int saveFlags) { + return mCanvas.saveLayer(bounds, paint, saveFlags); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public int saveLayer(@Nullable RectF bounds, @Nullable Paint paint) { + ScriptRuntime.requiresApi(Build.VERSION_CODES.LOLLIPOP); + return mCanvas.saveLayer(bounds, paint); + } + + public int saveLayer(float left, float top, float right, float bottom, @Nullable Paint paint, int saveFlags) { + return mCanvas.saveLayer(left, top, right, bottom, paint, saveFlags); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public int saveLayer(float left, float top, float right, float bottom, @Nullable Paint paint) { + ScriptRuntime.requiresApi(Build.VERSION_CODES.LOLLIPOP); + return mCanvas.saveLayer(left, top, right, bottom, paint); + } + + public int saveLayerAlpha(@Nullable RectF bounds, int alpha, int saveFlags) { + return mCanvas.saveLayerAlpha(bounds, alpha, saveFlags); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public int saveLayerAlpha(@Nullable RectF bounds, int alpha) { + ScriptRuntime.requiresApi(Build.VERSION_CODES.LOLLIPOP); + return mCanvas.saveLayerAlpha(bounds, alpha); + } + + public int saveLayerAlpha(float left, float top, float right, float bottom, int alpha, int saveFlags) { + return mCanvas.saveLayerAlpha(left, top, right, bottom, alpha, saveFlags); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public int saveLayerAlpha(float left, float top, float right, float bottom, int alpha) { + ScriptRuntime.requiresApi(Build.VERSION_CODES.LOLLIPOP); + return mCanvas.saveLayerAlpha(left, top, right, bottom, alpha); + } + + public void restore() { + mCanvas.restore(); + } + + public int getSaveCount() { + return mCanvas.getSaveCount(); + } + + public void restoreToCount(int saveCount) { + mCanvas.restoreToCount(saveCount); + } + + public void translate(float dx, float dy) { + mCanvas.translate(dx, dy); + } + + public void scale(float sx, float sy) { + mCanvas.scale(sx, sy); + } + + public void scale(float sx, float sy, float px, float py) { + mCanvas.scale(sx, sy, px, py); + } + + public void rotate(float degrees) { + mCanvas.rotate(degrees); + } + + public void rotate(float degrees, float px, float py) { + mCanvas.rotate(degrees, px, py); + } + + public void skew(float sx, float sy) { + mCanvas.skew(sx, sy); + } + + public void concat(@Nullable Matrix matrix) { + mCanvas.concat(matrix); + } + + public void setMatrix(@Nullable Matrix matrix) { + mCanvas.setMatrix(matrix); + } + + @Deprecated + public void getMatrix(@NonNull Matrix ctm) { + mCanvas.getMatrix(ctm); + } + + @Deprecated + public Matrix getMatrix() { + return mCanvas.getMatrix(); + } + + public boolean clipRect(@NonNull RectF rect, @NonNull Region.Op op) { + return mCanvas.clipRect(rect, op); + } + + public boolean clipRect(@NonNull Rect rect, @NonNull Region.Op op) { + return mCanvas.clipRect(rect, op); + } + + public boolean clipRect(@NonNull RectF rect) { + return mCanvas.clipRect(rect); + } + + public boolean clipRect(@NonNull Rect rect) { + return mCanvas.clipRect(rect); + } + + public boolean clipRect(float left, float top, float right, float bottom, @NonNull Region.Op op) { + return mCanvas.clipRect(left, top, right, bottom, op); + } + + public boolean clipRect(float left, float top, float right, float bottom) { + return mCanvas.clipRect(left, top, right, bottom); + } + + public boolean clipRect(int left, int top, int right, int bottom) { + return mCanvas.clipRect(left, top, right, bottom); + } + + public boolean clipPath(@NonNull Path path, @NonNull Region.Op op) { + return mCanvas.clipPath(path, op); + } + + public boolean clipPath(@NonNull Path path) { + return mCanvas.clipPath(path); + } + + public DrawFilter getDrawFilter() { + return mCanvas.getDrawFilter(); + } + + public void setDrawFilter(@Nullable DrawFilter filter) { + mCanvas.setDrawFilter(filter); + } + + public boolean quickReject(@NonNull RectF rect, @NonNull Canvas.EdgeType type) { + return mCanvas.quickReject(rect, type); + } + + public boolean quickReject(@NonNull Path path, @NonNull Canvas.EdgeType type) { + return mCanvas.quickReject(path, type); + } + + public boolean quickReject(float left, float top, float right, float bottom, @NonNull Canvas.EdgeType type) { + return mCanvas.quickReject(left, top, right, bottom, type); + } + + public boolean getClipBounds(@Nullable Rect bounds) { + return mCanvas.getClipBounds(bounds); + } + + public Rect getClipBounds() { + return mCanvas.getClipBounds(); + } + + public void drawRGB(int r, int g, int b) { + mCanvas.drawRGB(r, g, b); + } + + public void drawARGB(int a, int r, int g, int b) { + mCanvas.drawARGB(a, r, g, b); + } + + public void drawColor(int color) { + mCanvas.drawColor(color); + } + + public void drawColor(int color, @NonNull PorterDuff.Mode mode) { + mCanvas.drawColor(color, mode); + } + + public void drawPaint(@NonNull Paint paint) { + mCanvas.drawPaint(paint); + } + + public void drawPoints(float[] pts, int offset, int count, @NonNull Paint paint) { + mCanvas.drawPoints(pts, offset, count, paint); + } + + public void drawPoints(@NonNull float[] pts, @NonNull Paint paint) { + mCanvas.drawPoints(pts, paint); + } + + public void drawPoint(float x, float y, @NonNull Paint paint) { + mCanvas.drawPoint(x, y, paint); + } + + public void drawLine(float startX, float startY, float stopX, float stopY, @NonNull Paint paint) { + mCanvas.drawLine(startX, startY, stopX, stopY, paint); + } + + public void drawLines(@NonNull float[] pts, int offset, int count, @NonNull Paint paint) { + mCanvas.drawLines(pts, offset, count, paint); + } + + public void drawLines(@NonNull float[] pts, @NonNull Paint paint) { + mCanvas.drawLines(pts, paint); + } + + public void drawRect(@NonNull RectF rect, @NonNull Paint paint) { + mCanvas.drawRect(rect, paint); + } + + public void drawRect(@NonNull Rect r, @NonNull Paint paint) { + mCanvas.drawRect(r, paint); + } + + public void drawRect(float left, float top, float right, float bottom, @NonNull Paint paint) { + mCanvas.drawRect(left, top, right, bottom, paint); + } + + public void drawOval(@NonNull RectF oval, @NonNull Paint paint) { + mCanvas.drawOval(oval, paint); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public void drawOval(float left, float top, float right, float bottom, @NonNull Paint paint) { + mCanvas.drawOval(left, top, right, bottom, paint); + } + + public void drawCircle(float cx, float cy, float radius, @NonNull Paint paint) { + mCanvas.drawCircle(cx, cy, radius, paint); + } + + public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter, @NonNull Paint paint) { + mCanvas.drawArc(oval, startAngle, sweepAngle, useCenter, paint); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public void drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, @NonNull Paint paint) { + ScriptRuntime.requiresApi(Build.VERSION_CODES.LOLLIPOP); + mCanvas.drawArc(left, top, right, bottom, startAngle, sweepAngle, useCenter, paint); + } + + + public void drawRoundRect(@NonNull RectF rect, float rx, float ry, @NonNull Paint paint) { + mCanvas.drawRoundRect(rect, rx, ry, paint); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public void drawRoundRect(float left, float top, float right, float bottom, float rx, float ry, @NonNull Paint paint) { + ScriptRuntime.requiresApi(Build.VERSION_CODES.LOLLIPOP); + mCanvas.drawRoundRect(left, top, right, bottom, rx, ry, paint); + } + + public void drawPath(@NonNull Path path, @NonNull Paint paint) { + mCanvas.drawPath(path, paint); + } + + public void drawBitmap(@NonNull Bitmap bitmap, float left, float top, @Nullable Paint paint) { + mCanvas.drawBitmap(bitmap, left, top, paint); + } + + public void drawBitmap(@NonNull Bitmap bitmap, @Nullable Rect src, @NonNull RectF dst, @Nullable Paint paint) { + mCanvas.drawBitmap(bitmap, src, dst, paint); + } + + public void drawBitmap(@NonNull Bitmap bitmap, @Nullable Rect src, @NonNull Rect dst, @Nullable Paint paint) { + mCanvas.drawBitmap(bitmap, src, dst, paint); + } + + @Deprecated + public void drawBitmap(@NonNull int[] colors, int offset, int stride, float x, float y, int width, int height, boolean hasAlpha, @Nullable Paint paint) { + mCanvas.drawBitmap(colors, offset, stride, x, y, width, height, hasAlpha, paint); + } + + @Deprecated + public void drawBitmap(@NonNull int[] colors, int offset, int stride, int x, int y, int width, int height, boolean hasAlpha, @Nullable Paint paint) { + mCanvas.drawBitmap(colors, offset, stride, x, y, width, height, hasAlpha, paint); + } + + public void drawBitmap(@NonNull Bitmap bitmap, @NonNull Matrix matrix, @Nullable Paint paint) { + mCanvas.drawBitmap(bitmap, matrix, paint); + } + + public void drawBitmapMesh(@NonNull Bitmap bitmap, int meshWidth, int meshHeight, @NonNull float[] verts, int vertOffset, @Nullable int[] colors, int colorOffset, @Nullable Paint paint) { + mCanvas.drawBitmapMesh(bitmap, meshWidth, meshHeight, verts, vertOffset, colors, colorOffset, paint); + } + + public void drawVertices(@NonNull Canvas.VertexMode mode, int vertexCount, @NonNull float[] verts, int vertOffset, @Nullable float[] texs, int texOffset, @Nullable int[] colors, int colorOffset, @Nullable short[] indices, int indexOffset, int indexCount, @NonNull Paint paint) { + mCanvas.drawVertices(mode, vertexCount, verts, vertOffset, texs, texOffset, colors, colorOffset, indices, indexOffset, indexCount, paint); + } + + public void drawText(@NonNull char[] text, int index, int count, float x, float y, @NonNull Paint paint) { + mCanvas.drawText(text, index, count, x, y, paint); + } + + public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint) { + mCanvas.drawText(text, x, y, paint); + } + + public void drawText(@NonNull String text, int start, int end, float x, float y, @NonNull Paint paint) { + mCanvas.drawText(text, start, end, x, y, paint); + } + + public void drawText(@NonNull CharSequence text, int start, int end, float x, float y, @NonNull Paint paint) { + mCanvas.drawText(text, start, end, x, y, paint); + } + + @RequiresApi(api = Build.VERSION_CODES.M) + public void drawTextRun(@NonNull char[] text, int index, int count, int contextIndex, int contextCount, float x, float y, boolean isRtl, @NonNull Paint paint) { + ScriptRuntime.requiresApi(Build.VERSION_CODES.M); + mCanvas.drawTextRun(text, index, count, contextIndex, contextCount, x, y, isRtl, paint); + } + + @RequiresApi(api = Build.VERSION_CODES.M) + public void drawTextRun(@NonNull CharSequence text, int start, int end, int contextStart, int contextEnd, float x, float y, boolean isRtl, @NonNull Paint paint) { + ScriptRuntime.requiresApi(Build.VERSION_CODES.M); + mCanvas.drawTextRun(text, start, end, contextStart, contextEnd, x, y, isRtl, paint); + } + + @Deprecated + public void drawPosText(@NonNull char[] text, int index, int count, @NonNull float[] pos, @NonNull Paint paint) { + mCanvas.drawPosText(text, index, count, pos, paint); + } + + @Deprecated + public void drawPosText(@NonNull String text, @NonNull float[] pos, @NonNull Paint paint) { + mCanvas.drawPosText(text, pos, paint); + } + + public void drawTextOnPath(@NonNull char[] text, int index, int count, @NonNull Path path, float hOffset, float vOffset, @NonNull Paint paint) { + mCanvas.drawTextOnPath(text, index, count, path, hOffset, vOffset, paint); + } + + public void drawTextOnPath(@NonNull String text, @NonNull Path path, float hOffset, float vOffset, @NonNull Paint paint) { + mCanvas.drawTextOnPath(text, path, hOffset, vOffset, paint); + } + + public void drawPicture(@NonNull Picture picture) { + mCanvas.drawPicture(picture); + } + + public void drawPicture(@NonNull Picture picture, @NonNull RectF dst) { + mCanvas.drawPicture(picture, dst); + } + + public void drawPicture(@NonNull Picture picture, @NonNull Rect dst) { + mCanvas.drawPicture(picture, dst); + } + + public void drawImage(@NonNull ImageWrapper image, float left, float top, @Nullable Paint paint) { + mCanvas.drawBitmap(image.getBitmap(), left, top, paint); + } + + public void drawImage(@NonNull ImageWrapper image, float left, float top, float width, float height, @Nullable Paint paint) { + mCanvas.drawBitmap(image.getBitmap(), null, new RectF(left, top, left + width, top + height), paint); + } + + public void drawImage(@NonNull ImageWrapper image, int sx, int sy, int swidth, int sheight, float left, float top, float width, float height, @Nullable Paint paint) { + mCanvas.drawBitmap(image.getBitmap(), new Rect(sx, sy, sx + swidth, sy + sheight), + new RectF(left, top, left + width, top + height), paint); + } + + public void drawImage(@NonNull ImageWrapper image, @Nullable Rect src, @NonNull RectF dst, @Nullable Paint paint) { + mCanvas.drawBitmap(image.getBitmap(), src, dst, paint); + } + + public void drawImage(@NonNull ImageWrapper image, @Nullable Rect src, @NonNull Rect dst, @Nullable Paint paint) { + mCanvas.drawBitmap(image.getBitmap(), src, dst, paint); + } + + public void drawImage(@NonNull ImageWrapper image, @NonNull Matrix matrix, @Nullable Paint paint) { + mCanvas.drawBitmap(image.getBitmap(), matrix, paint); + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/graphics/ScriptCanvasView.kt b/app/src/main/java/com/stardust/autojs/core/graphics/ScriptCanvasView.kt new file mode 100644 index 00000000..145fcd17 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/graphics/ScriptCanvasView.kt @@ -0,0 +1,178 @@ +package com.stardust.autojs.core.graphics + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Canvas +import android.graphics.SurfaceTexture +import android.os.SystemClock +import android.util.Log +import android.view.TextureView +import android.view.View +import com.stardust.autojs.core.eventloop.EventEmitter +import com.stardust.autojs.runtime.ScriptRuntime +import com.stardust.autojs.runtime.exception.ScriptInterruptedException +import com.stardust.ext.ifNull +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +/** + * Created by Stardust on 2018/3/16. + */ + +@SuppressLint("ViewConstructor") +class ScriptCanvasView(context: Context, private val mScriptRuntime: ScriptRuntime) : TextureView(context), TextureView.SurfaceTextureListener { + @Volatile + private var mDrawing = true + private val mEventEmitter: EventEmitter = EventEmitter(mScriptRuntime.bridges) + private var mDrawingThreadPool: ExecutorService? = null + + @Volatile + private var mTimePerDraw = (1000 / 30).toLong() + + val maxListeners: Int + get() = mEventEmitter.maxListeners + + init { + surfaceTextureListener = this + } + + fun setMaxFps(maxFps: Int) { + mTimePerDraw = if (maxFps <= 0) { + 0 + } else { + (100 / maxFps).toLong() + } + } + + @Synchronized + private fun performDraw() { + ::mDrawingThreadPool.ifNull { + Executors.newCachedThreadPool() + }.run { + execute { + var canvas: Canvas? = null + var time = SystemClock.uptimeMillis() + val scriptCanvas = ScriptCanvas() + try { + while (mDrawing) { + canvas = lockCanvas() + scriptCanvas.setCanvas(canvas) + emit("draw", scriptCanvas, this@ScriptCanvasView) + canvas?.let { unlockCanvasAndPost(it) } + canvas = null + val dt = mTimePerDraw - (SystemClock.uptimeMillis() - time) + if (dt > 0) { + sleep(dt) + } + time = SystemClock.uptimeMillis() + } + } catch (e: Exception) { + mScriptRuntime.exit(e) + mDrawing = false + } finally { + if (canvas != null) { + unlockCanvasAndPost(canvas) + } + } + } + } + } + + private fun sleep(dt: Long) { + try { + Thread.sleep(dt) + } catch (e: InterruptedException) { + throw ScriptInterruptedException(e) + } + + } + + override fun onWindowVisibilityChanged(visibility: Int) { + Log.d(LOG_TAG, "onWindowVisibilityChanged: " + this + ": visibility=" + visibility + ", mDrawingThreadPool=" + mDrawingThreadPool) + val oldDrawing = mDrawing + mDrawing = visibility == View.VISIBLE + if (!oldDrawing && mDrawing) { + performDraw() + } + super.onWindowVisibilityChanged(visibility) + } + + fun once(eventName: String, listener: Any): EventEmitter { + return mEventEmitter.once(eventName, listener) + } + + fun on(eventName: String, listener: Any): EventEmitter { + return mEventEmitter.on(eventName, listener) + } + + fun addListener(eventName: String, listener: Any): EventEmitter { + return mEventEmitter.addListener(eventName, listener) + } + + fun emit(eventName: String, vararg args: Any): Boolean { + return mEventEmitter.emit(eventName, *args) + } + + fun eventNames(): Array { + return mEventEmitter.eventNames() + } + + fun listenerCount(eventName: String): Int { + return mEventEmitter.listenerCount(eventName) + } + + fun listeners(eventName: String): Array { + return mEventEmitter.listeners(eventName) + } + + fun prependListener(eventName: String, listener: Any): EventEmitter { + return mEventEmitter.prependListener(eventName, listener) + } + + fun prependOnceListener(eventName: String, listener: Any): EventEmitter { + return mEventEmitter.prependOnceListener(eventName, listener) + } + + fun removeAllListeners(): EventEmitter { + return mEventEmitter.removeAllListeners() + } + + fun removeAllListeners(eventName: String): EventEmitter { + return mEventEmitter.removeAllListeners(eventName) + } + + fun removeListener(eventName: String, listener: Any): EventEmitter { + return mEventEmitter.removeListener(eventName, listener) + } + + fun setMaxListeners(n: Int): EventEmitter { + return mEventEmitter.setMaxListeners(n) + } + + override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) { + performDraw() + Log.d(LOG_TAG, "onSurfaceTextureAvailable: ${this}, width = $width, height = $height") + } + + override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {} + + override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean { + mDrawing = false + mDrawingThreadPool?.shutdown() + Log.d(LOG_TAG, "onSurfaceTextureDestroyed: ${this}") + return true + } + + override fun onSurfaceTextureUpdated(surface: SurfaceTexture) { + + } + + companion object { + + private const val LOG_TAG = "ScriptCanvasView" + + fun defaultMaxListeners(): Int { + return EventEmitter.defaultMaxListeners() + } + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/http/MutableOkHttp.java b/app/src/main/java/com/stardust/autojs/core/http/MutableOkHttp.java new file mode 100644 index 00000000..1d4f607e --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/http/MutableOkHttp.java @@ -0,0 +1,90 @@ +package com.stardust.autojs.core.http; + +import java.net.SocketTimeoutException; +import java.util.Collections; +import java.util.concurrent.TimeUnit; + +import okhttp3.Interceptor; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +/** + * Created by Stardust on 2018/4/11. + */ + +public class MutableOkHttp extends OkHttpClient { + + private OkHttpClient mOkHttpClient; + private int mMaxRetries = 3; + private long mTimeout = 30 * 1000; + private Interceptor mRetryInterceptor = chain -> { + Request request = chain.request(); + Response response = null; + int tryCount = 0; + do { + boolean succeed; + try { + response = chain.proceed(request); + succeed = response.isSuccessful(); + } catch (SocketTimeoutException e) { + succeed = false; + if (tryCount >= getMaxRetries()) { + throw e; + } + } + if (succeed || tryCount >= getMaxRetries()) { + return response; + } + tryCount++; + } while (true); + }; + + public MutableOkHttp() { + mOkHttpClient = newClient(new OkHttpClient.Builder()); + } + + public OkHttpClient client() { + return mOkHttpClient; + } + + protected OkHttpClient newClient(Builder builder) { + builder.readTimeout(getTimeout(), TimeUnit.MILLISECONDS) + .writeTimeout(getTimeout(), TimeUnit.MILLISECONDS) + .connectTimeout(getTimeout(), TimeUnit.MILLISECONDS); + for (Interceptor interceptor : getInterceptors()) { + builder.addInterceptor(interceptor); + } + return builder.build(); + } + + public Iterable getInterceptors() { + return Collections.singletonList(mRetryInterceptor); + } + + public int getMaxRetries() { + return mMaxRetries; + } + + public void setMaxRetries(int maxRetries) { + mMaxRetries = maxRetries; + } + + public long getTimeout() { + return mTimeout; + } + + + public void setTimeout(long timeout) { + mTimeout = timeout; + muteClient(); + } + + public synchronized void muteClient(Builder builder) { + mOkHttpClient = newClient(builder); + } + + protected synchronized void muteClient() { + mOkHttpClient = newClient(mOkHttpClient.newBuilder()); + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/image/ColorDetector.java b/app/src/main/java/com/stardust/autojs/core/image/ColorDetector.java new file mode 100644 index 00000000..76a1fef4 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/image/ColorDetector.java @@ -0,0 +1,199 @@ +package com.stardust.autojs.core.image; + +import android.graphics.Color; + +/** + * Created by Stardust on 2017/5/20. + */ + +public interface ColorDetector { + + boolean detectsColor(int red, int green, int blue); + + abstract class AbstractColorDetector implements ColorDetector { + + protected final int mColor; + protected final int mR, mG, mB; + + public AbstractColorDetector(int color) { + mColor = color; + mR = Color.red(color); + mG = Color.green(color); + mB = Color.blue(color); + } + } + + class EqualityDetector extends AbstractColorDetector { + + + public EqualityDetector(int color) { + super(color); + } + + @Override + public boolean detectsColor(int red, int green, int blue) { + return mR == red && mG == green && mB == blue; + } + } + + class DifferenceDetector extends AbstractColorDetector { + + private final int mThreshold; + + public DifferenceDetector(int color, int threshold) { + super(color); + mThreshold = threshold * 3; + } + + + @Override + public boolean detectsColor(int R, int G, int B) { + return Math.abs(R - mR) + Math.abs(G - mG) + Math.abs(B - mB) <= mThreshold; + } + } + + class RDistanceDetector extends AbstractColorDetector { + + private final int mThreshold; + + public RDistanceDetector(int color, int threshold) { + super(color); + mThreshold = threshold; + } + + @Override + public boolean detectsColor(int R, int G, int B) { + return Math.abs(mR - R) <= mThreshold; + } + } + + class RGBDistanceDetector extends AbstractColorDetector { + + private final int mThreshold; + + public RGBDistanceDetector(int color, int threshold) { + super(color); + mThreshold = threshold * threshold * 3; + } + + @Override + public boolean detectsColor(int R, int G, int B) { + int dR = R - mR; + int dG = G - mG; + int dB = B - mB; + int d = dR * dR + dG * dG + dB * dB; + return d <= mThreshold; + } + } + + class WeightedRGBDistanceDetector extends AbstractColorDetector { + + private final int mThreshold; + private final int mR, mG, mB; + + public WeightedRGBDistanceDetector(int color, int threshold) { + super(color); + mR = (color & 0xff0000) >> 16; + mG = (color & 0x00ff00) >> 8; + mB = color & 0xff; + mThreshold = threshold * threshold * 8; + } + + @Override + public boolean detectsColor(int R, int G, int B) { + int dR = R - mR; + int dG = G - mG; + int dB = B - mB; + double meanR = (mR + R) / 2; + double weightR = 2 + meanR / 256; + double weightG = 4.0; + double weightB = 2 + (255 - meanR) / 256; + return weightR * dR * dR + weightG * dG * dG + weightB * dB * dB <= mThreshold; + } + } + + class HDistanceDetector extends AbstractColorDetector { + + private final int mH; + private final int mThreshold; + + public HDistanceDetector(int color, int threshold) { + super(color); + mH = getH(mR, mG, mB); + mThreshold = threshold; + } + + @Override + public boolean detectsColor(int R, int G, int B) { + return Math.abs(mH - getH(R, G, B)) <= mThreshold; + } + + private static int getH(int R, int G, int B) { + int max, min, H; + if (R > G) { + min = Math.min(G, B); + max = Math.max(R, B); + } else { + min = Math.min(R, B); + max = Math.max(G, B); + } + if (R == max) { + H = (G - B) / (max - min) * 60; + } else if (G == max) { + H = 120 + (B - R) / (max - min) * 60; + } else { + H = 240 + (R - G) / (max - min) * 60; + } + if (H < 0) H = H + 360; + return H; + } + } + + class HSDistanceDetector extends AbstractColorDetector { + + private final int mH, mS; + private final int mThreshold; + + public HSDistanceDetector(int color, int threshold) { + super(color); + long HS = getHS(mR, mG, mB); + mH = (int) (HS & 0xffffffffL); + mS = (int) ((HS >> 32) & 0xffffffffL); + mThreshold = threshold * 3729600 / 255; + } + + public HSDistanceDetector(int color, float similarity) { + this(color, (int) (1.0f - similarity) * 255); + } + + @Override + public boolean detectsColor(int R, int G, int B) { + long hs = getHS(R, G, B); + int dH = (int) (hs & 0xffffffffL) - mH; + int dS = (int) ((hs >> 32) & 0xffffffffL) - mS; + return dH * dH + dS * dS <= mThreshold; + } + + private static long getHS(int R, int G, int B) { + int max, min, H; + if (R > G) { + min = Math.min(G, B); + max = Math.max(R, B); + } else { + min = Math.min(R, B); + max = Math.max(G, B); + } + if (R == max) { + H = (G - B) / (max - min) * 60; + } else if (G == max) { + H = 120 + (B - R) / (max - min) * 60; + } else { + H = 240 + (R - G) / (max - min) * 60; + } + if (H < 0) H = H + 360; + int S = (max - min) * 100 / max; + return H & ((long) S << 32); + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/stardust/autojs/core/image/ColorFinder.java b/app/src/main/java/com/stardust/autojs/core/image/ColorFinder.java new file mode 100644 index 00000000..d5dba865 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/image/ColorFinder.java @@ -0,0 +1,133 @@ +package com.stardust.autojs.core.image; + +import android.graphics.Color; +import android.os.Build; +import androidx.annotation.RequiresApi; + +import com.stardust.autojs.core.opencv.MatOfPoint; +import com.stardust.autojs.core.opencv.OpenCVHelper; +import com.stardust.util.ScreenMetrics; + +import org.opencv.core.Core; + +import com.stardust.autojs.core.opencv.Mat; + +import org.opencv.core.Point; +import org.opencv.core.Rect; +import org.opencv.core.Scalar; + +/** + * Created by Stardust on 2017/5/18. + */ + +@RequiresApi(api = Build.VERSION_CODES.KITKAT) +public class ColorFinder { + + private ScreenMetrics mScreenMetrics; + + public ColorFinder(ScreenMetrics screenMetrics) { + mScreenMetrics = screenMetrics; + } + + public Point findColorEquals(ImageWrapper imageWrapper, int color) { + return findColorEquals(imageWrapper, color, null); + } + + public Point findColorEquals(ImageWrapper imageWrapper, int color, Rect region) { + return findColor(imageWrapper, color, 0, region); + } + + public Point findColor(ImageWrapper imageWrapper, int color, int threshold) { + return findColor(imageWrapper, color, threshold, null); + } + + public Point findColor(ImageWrapper image, int color, int threshold, Rect rect) { + MatOfPoint matOfPoint = findColorInner(image, color, threshold, rect); + if (matOfPoint == null) { + return null; + } + Point point = matOfPoint.toArray()[0]; + if (rect != null) { + point.x = mScreenMetrics.scaleX((int) (point.x + rect.x)); + point.y = mScreenMetrics.scaleX((int) (point.y + rect.y)); + } + OpenCVHelper.release(matOfPoint); + return point; + } + + public Point[] findAllPointsForColor(ImageWrapper image, int color, int threshold, Rect rect) { + + MatOfPoint matOfPoint = findColorInner(image, color, threshold, rect); + if (matOfPoint == null) { + return new Point[0]; + } + Point[] points = matOfPoint.toArray(); + OpenCVHelper.release(matOfPoint); + if (rect != null) { + for (int i = 0; i < points.length; i++) { + points[i].x = mScreenMetrics.scaleX((int) (points[i].x + rect.x)); + points[i].y = mScreenMetrics.scaleX((int) (points[i].y + rect.y)); + } + } + return points; + } + + private MatOfPoint findColorInner(ImageWrapper image, int color, int threshold, Rect rect) { + Mat bi = new Mat(); + Scalar lowerBound = new Scalar(Color.red(color) - threshold, Color.green(color) - threshold, + Color.blue(color) - threshold, 255); + Scalar upperBound = new Scalar(Color.red(color) + threshold, Color.green(color) + threshold, + Color.blue(color) + threshold, 255); + if (rect != null) { + Mat m = new Mat(image.getMat(), rect); + Core.inRange(m, lowerBound, upperBound, bi); + OpenCVHelper.release(m); + } else { + Core.inRange(image.getMat(), lowerBound, upperBound, bi); + } + Mat nonZeroPos = new Mat(); + Core.findNonZero(bi, nonZeroPos); + MatOfPoint result; + if (nonZeroPos.rows() == 0 || nonZeroPos.cols() == 0) { + result = null; + } else { + result = OpenCVHelper.newMatOfPoint(nonZeroPos); + } + OpenCVHelper.release(bi); + OpenCVHelper.release(nonZeroPos); + return result; + } + + public Point findMultiColors(ImageWrapper image, int firstColor, int threshold, Rect rect, int[] points) { + Point[] firstPoints = findAllPointsForColor(image, firstColor, threshold, rect); + for (Point firstPoint : firstPoints) { + if (firstPoint == null) + continue; + if (checksPath(image, firstPoint, threshold, rect, points)) { + return firstPoint; + } + } + return null; + } + + private boolean checksPath(ImageWrapper image, Point startingPoint, int threshold, Rect rect, int[] points) { + for (int i = 0; i < points.length; i += 3) { + int x = points[i]; + int y = points[i + 1]; + int color = points[i + 2]; + ColorDetector colorDetector = new ColorDetector.DifferenceDetector(color, threshold); + x += startingPoint.x; + y += startingPoint.y; + if (x >= image.getWidth() || y >= image.getHeight() + || x < 0 || y < 0) { + return false; + } + int c = image.pixel(x, y); + if (!colorDetector.detectsColor(Color.red(c), Color.green(c), Color.blue(c))) { + return false; + } + + } + return true; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/stardust/autojs/core/image/Colors.java b/app/src/main/java/com/stardust/autojs/core/image/Colors.java new file mode 100644 index 00000000..911476f3 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/image/Colors.java @@ -0,0 +1,90 @@ +package com.stardust.autojs.core.image; + +import android.graphics.Color; +import android.os.Build; +import androidx.annotation.RequiresApi; + +/** + * Created by Stardust on 2017/12/31. + */ + +public class Colors { + + public static final int BLACK = 0xFF000000; + public static final int DKGRAY = 0xFF444444; + public static final int GRAY = 0xFF888888; + public static final int LTGRAY = 0xFFCCCCCC; + public static final int WHITE = 0xFFFFFFFF; + public static final int RED = 0xFFFF0000; + public static final int GREEN = 0xFF00FF00; + public static final int BLUE = 0xFF0000FF; + public static final int YELLOW = 0xFFFFFF00; + public static final int CYAN = 0xFF00FFFF; + public static final int MAGENTA = 0xFFFF00FF; + public static final int TRANSPARENT = 0; + + public int rgb(int red, int green, int blue) { + return Color.rgb(red, green, blue); + } + + public int argb(int alpha, int red, int green, int blue) { + return Color.argb(alpha, red, green, blue); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + public float luminance(int color) { + double red = Color.red(color) / 255.0; + red = red < 0.03928 ? red / 12.92 : Math.pow((red + 0.055) / 1.055, 2.4); + double green = Color.green(color) / 255.0; + green = green < 0.03928 ? green / 12.92 : Math.pow((green + 0.055) / 1.055, 2.4); + double blue = Color.blue(color) / 255.0; + blue = blue < 0.03928 ? blue / 12.92 : Math.pow((blue + 0.055) / 1.055, 2.4); + return (float) ((0.2126 * red) + (0.7152 * green) + (0.0722 * blue)); + } + + public int parseColor(String colorString) { + return Color.parseColor(colorString); + } + + public void RGBToHSV(int red, int green, int blue, float[] hsv) { + Color.RGBToHSV(red, green, blue, hsv); + } + + public void colorToHSV(int color, float[] hsv) { + Color.colorToHSV(color, hsv); + } + + public int HSVToColor(float[] hsv) { + return Color.HSVToColor(hsv); + } + + public int HSVToColor(int alpha, float[] hsv) { + return Color.HSVToColor(alpha, hsv); + } + + public String toString(int color) { + StringBuilder c = new StringBuilder(Integer.toHexString(color)); + while (c.length() < 6) { + c.insert(0, "0"); + } + return "#" + c; + } + + public boolean equals(int c1, int c2) { + return (c1 & 0xffffff) == (c2 & 0xffffff); + } + + public boolean equals(int c1, String c2) { + return equals(c1, parseColor(c2)); + } + + public boolean equals(String c1, int c2) { + return equals(parseColor(c1), c2); + } + + public boolean equals(String c1, String c2) { + return equals(parseColor(c1), parseColor(c2)); + } + + +} diff --git a/app/src/main/java/com/stardust/autojs/core/image/ImageWrapper.java b/app/src/main/java/com/stardust/autojs/core/image/ImageWrapper.java new file mode 100644 index 00000000..a260785e --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/image/ImageWrapper.java @@ -0,0 +1,170 @@ +package com.stardust.autojs.core.image; + +import android.graphics.Bitmap; +import android.graphics.Color; +import android.media.Image; +import android.os.Build; + +import com.stardust.autojs.core.opencv.Mat; +import com.stardust.autojs.core.opencv.OpenCVHelper; +import com.stardust.pio.UncheckedIOException; + +import org.opencv.android.Utils; +import org.opencv.imgcodecs.Imgcodecs; + +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.nio.ByteBuffer; + +import androidx.annotation.RequiresApi; + +/** + * Created by Stardust on 2017/11/25. + */ +public class ImageWrapper { + + private Mat mMat; + private int mWidth; + private int mHeight; + private Bitmap mBitmap; + + protected ImageWrapper(Mat mat) { + mMat = mat; + mWidth = mat.cols(); + mHeight = mat.rows(); + } + + protected ImageWrapper(Bitmap bitmap) { + mBitmap = bitmap; + mWidth = bitmap.getWidth(); + mHeight = bitmap.getHeight(); + } + + protected ImageWrapper(Bitmap bitmap, Mat mat) { + mBitmap = bitmap; + mMat = mat; + mWidth = bitmap.getWidth(); + mHeight = bitmap.getHeight(); + } + + public ImageWrapper(int width, int height) { + this(Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)); + } + + + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + public static ImageWrapper ofImage(Image image) { + if (image == null) { + return null; + } + return new ImageWrapper(toBitmap(image)); + } + + public static ImageWrapper ofMat(Mat mat) { + if (mat == null) { + return null; + } + return new ImageWrapper(mat); + } + + + public static ImageWrapper ofBitmap(Bitmap bitmap) { + if (bitmap == null) { + return null; + } + return new ImageWrapper(bitmap); + } + + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + public static Bitmap toBitmap(Image image) { + Image.Plane plane = image.getPlanes()[0]; + ByteBuffer buffer = plane.getBuffer(); + buffer.position(0); + int pixelStride = plane.getPixelStride(); + int rowPadding = plane.getRowStride() - pixelStride * image.getWidth(); + Bitmap bitmap = Bitmap.createBitmap(image.getWidth() + rowPadding / pixelStride, image.getHeight(), Bitmap.Config.ARGB_8888); + bitmap.copyPixelsFromBuffer(buffer); + if (rowPadding == 0) { + return bitmap; + } + return Bitmap.createBitmap(bitmap, 0, 0, image.getWidth(), image.getHeight()); + } + + public int getWidth() { + ensureNotRecycled(); + return mWidth; + } + + public int getHeight() { + ensureNotRecycled(); + return mHeight; + } + + public Mat getMat() { + ensureNotRecycled(); + if (mMat == null && mBitmap != null) { + mMat = new Mat(); + Utils.bitmapToMat(mBitmap, mMat); + } + return mMat; + } + + public void saveTo(String path) { + ensureNotRecycled(); + if (mBitmap != null) { + try { + mBitmap.compress(Bitmap.CompressFormat.PNG, 100, new FileOutputStream(path)); + } catch (FileNotFoundException e) { + throw new UncheckedIOException(e); + } + } else { + Imgcodecs.imwrite(path, mMat); + } + } + + public int pixel(int x, int y) { + ensureNotRecycled(); + if (mBitmap != null) { + return mBitmap.getPixel(x, y); + } + double[] channels = mMat.get(x, y); + return Color.argb((int) channels[3], (int) channels[0], (int) channels[1], (int) channels[2]); + } + + public Bitmap getBitmap() { + ensureNotRecycled(); + if (mBitmap == null && mMat != null) { + mBitmap = Bitmap.createBitmap(mMat.width(), mMat.height(), Bitmap.Config.ARGB_8888); + Utils.matToBitmap(mMat, mBitmap); + } + return mBitmap; + } + + public void recycle() { + if (mBitmap != null) { + mBitmap.recycle(); + mBitmap = null; + } + if (mMat != null) { + OpenCVHelper.release(mMat); + mMat = null; + } + + } + + public void ensureNotRecycled() { + if (mBitmap == null && mMat == null) + throw new IllegalStateException("image has been recycled"); + } + + public ImageWrapper clone() { + ensureNotRecycled(); + if (mBitmap == null) { + return ImageWrapper.ofMat(mMat.clone()); + } + if (mMat == null) { + return ImageWrapper.ofBitmap(mBitmap.copy(mBitmap.getConfig(), true)); + } + return new ImageWrapper(mBitmap.copy(mBitmap.getConfig(), true), mMat.clone()); + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/image/TemplateMatching.java b/app/src/main/java/com/stardust/autojs/core/image/TemplateMatching.java new file mode 100644 index 00000000..9eb1ae6c --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/image/TemplateMatching.java @@ -0,0 +1,250 @@ +package com.stardust.autojs.core.image; + +import android.util.TimingLogger; + +import com.stardust.autojs.core.opencv.OpenCVHelper; +import com.stardust.util.Nath; + +import org.opencv.core.Core; +import org.opencv.core.CvType; + +import com.stardust.autojs.core.opencv.Mat; + +import org.opencv.core.Point; +import org.opencv.core.Rect; +import org.opencv.core.Scalar; +import org.opencv.core.Size; +import org.opencv.imgproc.Imgproc; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + + +/** + * Created by Stardust on 2017/11/25. + */ + +public class TemplateMatching { + + public static class Match { + public final Point point; + public final double similarity; + + public Match(Point point, double similarity) { + this.point = point; + this.similarity = similarity; + } + + @Override + public String toString() { + return "Match{" + + "point=" + point + + ", similarity=" + similarity + + '}'; + } + } + + private static final String LOG_TAG = "TemplateMatching"; + + public static final int MAX_LEVEL_AUTO = -1; + public static final int MATCHING_METHOD_DEFAULT = Imgproc.TM_CCOEFF_NORMED; + + public static Point fastTemplateMatching(Mat img, Mat template, int matchMethod, float weakThreshold, float strictThreshold, int maxLevel) { + List result = fastTemplateMatching(img, template, matchMethod, weakThreshold, strictThreshold, maxLevel, 1); + if (result.isEmpty()) { + return null; + } + return result.get(0).point; + } + + /** + * 采用图像金字塔算法快速找图 + * + * @param img 图片 + * @param template 模板图片 + * @param matchMethod 匹配算法 + * @param weakThreshold 弱阈值。该值用于在每一轮模板匹配中检验是否继续匹配。如果相似度小于该值,则不再继续匹配。 + * @param strictThreshold 强阈值。该值用于检验最终匹配结果,以及在每一轮匹配中如果相似度大于该值则直接返回匹配结果。 + * @param maxLevel 图像金字塔的层数 + * @return + */ + public static List fastTemplateMatching(Mat img, Mat template, int matchMethod, float weakThreshold, float strictThreshold, int maxLevel, int limit) { + TimingLogger logger = new TimingLogger(LOG_TAG, "fast_tm"); + if (maxLevel == MAX_LEVEL_AUTO) { + //自动选取金字塔层数 + maxLevel = selectPyramidLevel(img, template); + logger.addSplit("selectPyramidLevel:" + maxLevel); + } + //保存每一轮匹配到模板图片在原图片的位置 + List finalMatchResult = new ArrayList<>(); + List previousMatchResult = Collections.emptyList(); + boolean isFirstMatching = true; + for (int level = maxLevel; level >= 0; level--) { + // 放缩图片 + List currentMatchResult = new ArrayList<>(); + Mat src = getPyramidDownAtLevel(img, level); + Mat currentTemplate = getPyramidDownAtLevel(template, level); + // 如果在上一轮中没有匹配到图片,则考虑是否退出匹配 + if (previousMatchResult.isEmpty()) { + // 如果不是第一次匹配,并且不满足shouldContinueMatching的条件,则直接退出匹配 + if (!isFirstMatching && !shouldContinueMatching(level, maxLevel)) { + break; + } + Mat matchResult = matchTemplate(src, currentTemplate, matchMethod); + getBestMatched(matchResult, currentTemplate, matchMethod, weakThreshold, currentMatchResult, limit, null); + OpenCVHelper.release(matchResult); + } else { + for (Match match : previousMatchResult) { + // 根据上一轮的匹配点,计算本次匹配的区域 + Rect r = getROI(match.point, src, currentTemplate); + Mat m = new Mat(src, r); + Mat matchResult = matchTemplate(m, currentTemplate, matchMethod); + getBestMatched(matchResult, currentTemplate, matchMethod, weakThreshold, currentMatchResult, limit, r); + OpenCVHelper.release(m); + OpenCVHelper.release(matchResult); + } + } + + if (src != img) + OpenCVHelper.release(src); + if (currentTemplate != template) + OpenCVHelper.release(currentTemplate); + + logger.addSplit("level:" + level + ", result:" + previousMatchResult); + + // 把满足强阈值的点找出来,加到最终结果列表 + if (!currentMatchResult.isEmpty()) { + Iterator iterator = currentMatchResult.iterator(); + while (iterator.hasNext()) { + Match match = iterator.next(); + if (match.similarity >= strictThreshold) { + pyrUp(match.point, level); + finalMatchResult.add(match); + iterator.remove(); + } + } + // 如果所有结果都满足强阈值,则退出循环,返回最终结果 + if (currentMatchResult.isEmpty()) { + break; + } + } + isFirstMatching = false; + previousMatchResult = currentMatchResult; + } + logger.addSplit("result:" + finalMatchResult); + logger.dumpToLog(); + return finalMatchResult; + } + + + private static Mat getPyramidDownAtLevel(Mat m, int level) { + if (level == 0) { + return m; + } + int cols = m.cols(); + int rows = m.rows(); + for (int i = 0; i < level; i++) { + cols = (cols + 1) / 2; + rows = (rows + 1) / 2; + } + Mat r = new Mat(rows, cols, m.type()); + Imgproc.resize(m, r, new Size(cols, rows)); + return r; + } + + private static void pyrUp(Point p, int level) { + for (int i = 0; i < level; i++) { + p.x *= 2; + p.y *= 2; + } + } + + private static boolean shouldContinueMatching(int level, int maxLevel) { + if (level == maxLevel && level != 0) { + return true; + } + if (maxLevel <= 2) { + return false; + } + return level == maxLevel - 1; + } + + private static Rect getROI(Point p, Mat src, Mat currentTemplate) { + int x = (int) (p.x * 2 - currentTemplate.cols() / 4); + x = Math.max(0, x); + int y = (int) (p.y * 2 - currentTemplate.rows() / 4); + y = Math.max(0, y); + int w = (int) (currentTemplate.cols() * 1.5); + int h = (int) (currentTemplate.rows() * 1.5); + if (x + w >= src.cols()) { + w = src.cols() - x - 1; + } + if (y + h >= src.rows()) { + h = src.rows() - y - 1; + } + return new Rect(x, y, w, h); + } + + private static int selectPyramidLevel(Mat img, Mat template) { + int minDim = Nath.min(img.rows(), img.cols(), template.rows(), template.cols()); + //这里选取16为图像缩小后的最小宽高,从而用log(2, minDim / 16)得到最多可以经过几次缩小。 + int maxLevel = (int) (Math.log(minDim / 16) / Math.log(2)); + if (maxLevel < 0) { + return 0; + } + //上限为6 + return Math.min(6, maxLevel); + } + + + private static Mat matchTemplate(Mat img, Mat temp, int match_method) { + int result_cols = img.cols() - temp.cols() + 1; + int result_rows = img.rows() - temp.rows() + 1; + Mat result = new Mat(result_rows, result_cols, CvType.CV_32FC1); + Imgproc.matchTemplate(img, temp, result, match_method); + return result; + } + + private static void getBestMatched(Mat tmResult, Mat template, int matchMethod, float weakThreshold, List outResult, int limit, Rect rect) { + for (int i = 0; i < limit; i++) { + Match bestMatched = getBestMatched(tmResult, matchMethod, weakThreshold, rect); + if (bestMatched == null) { + break; + } + outResult.add(bestMatched); + Point start = new Point(Math.max(0, bestMatched.point.x - template.width() + 1), + Math.max(0, bestMatched.point.y - template.height() + 1)); + Point end = new Point(Math.min(tmResult.width(), bestMatched.point.x + template.width()), + Math.min(tmResult.height(), bestMatched.point.y + template.height())); + Imgproc.rectangle(tmResult, start, end, new Scalar(0, 255, 0), -1); + } + } + + private static Match getBestMatched(Mat tmResult, int matchMethod, float weakThreshold, Rect rect) { + TimingLogger logger = new TimingLogger(LOG_TAG, "best_matched_point"); + Core.MinMaxLocResult mmr = Core.minMaxLoc(tmResult); + logger.addSplit("minMaxLoc"); + double value; + Point pos; + if (matchMethod == Imgproc.TM_SQDIFF || matchMethod == Imgproc.TM_SQDIFF_NORMED) { + pos = mmr.minLoc; + value = -mmr.minVal; + } else { + pos = mmr.maxLoc; + value = mmr.maxVal; + } + if (value < weakThreshold) { + return null; + } + if (rect != null) { + pos.x += rect.x; + pos.y += rect.y; + } + logger.addSplit("value:" + value); + return new Match(pos, value); + } + + +} diff --git a/app/src/main/java/com/stardust/autojs/core/image/capture/ScreenCaptureRequestActivity.java b/app/src/main/java/com/stardust/autojs/core/image/capture/ScreenCaptureRequestActivity.java new file mode 100644 index 00000000..453e7dbd --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/image/capture/ScreenCaptureRequestActivity.java @@ -0,0 +1,68 @@ +package com.stardust.autojs.core.image.capture; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; + +import com.stardust.app.OnActivityResultDelegate; +import com.stardust.util.IntentExtras; + +/** + * Created by Stardust on 2017/5/22. + */ + +@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) +public class ScreenCaptureRequestActivity extends Activity { + + private OnActivityResultDelegate.Mediator mOnActivityResultDelegateMediator = new OnActivityResultDelegate.Mediator(); + private ScreenCaptureRequester mScreenCaptureRequester; + private ScreenCaptureRequester.Callback mCallback; + + public static void request(Context context, ScreenCaptureRequester.Callback callback) { + Intent intent = new Intent(context, ScreenCaptureRequestActivity.class) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + IntentExtras.newExtras() + .put("callback", callback) + .putInIntent(intent); + context.startActivity(intent); + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + IntentExtras extras = IntentExtras.fromIntentAndRelease(getIntent()); + if (extras == null) { + finish(); + return; + } + mCallback = extras.get("callback"); + if (mCallback == null) { + finish(); + return; + } + mScreenCaptureRequester = new ScreenCaptureRequester.ActivityScreenCaptureRequester(mOnActivityResultDelegateMediator, this); + mScreenCaptureRequester.setOnActivityResultCallback(mCallback); + mScreenCaptureRequester.request(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + mCallback = null; + if (mScreenCaptureRequester == null) + return; + mScreenCaptureRequester.cancel(); + mScreenCaptureRequester = null; + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + mOnActivityResultDelegateMediator.onActivityResult(requestCode, resultCode, data); + finish(); + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/stardust/autojs/core/image/capture/ScreenCaptureRequester.java b/app/src/main/java/com/stardust/autojs/core/image/capture/ScreenCaptureRequester.java new file mode 100644 index 00000000..0e276a43 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/image/capture/ScreenCaptureRequester.java @@ -0,0 +1,82 @@ +package com.stardust.autojs.core.image.capture; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.media.projection.MediaProjectionManager; +import android.os.Build; +import androidx.annotation.RequiresApi; + +import com.stardust.app.OnActivityResultDelegate; + +/** + * Created by Stardust on 2017/5/17. + */ + +public interface ScreenCaptureRequester { + + void cancel(); + + interface Callback { + + void onRequestResult(int result, Intent data); + + } + + void request(); + + void setOnActivityResultCallback(Callback callback); + + abstract class AbstractScreenCaptureRequester implements ScreenCaptureRequester { + + protected Callback mCallback; + protected Intent mResult; + + @Override + public void setOnActivityResultCallback(Callback callback) { + mCallback = callback; + } + + public void onResult(int resultCode, Intent data) { + mResult = data; + if (mCallback != null) + mCallback.onRequestResult(resultCode, data); + } + + @Override + public void cancel() { + if (mResult != null) + return; + if (mCallback != null) + mCallback.onRequestResult(Activity.RESULT_CANCELED, null); + } + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + class ActivityScreenCaptureRequester extends AbstractScreenCaptureRequester implements ScreenCaptureRequester, OnActivityResultDelegate { + + private static final int REQUEST_CODE_MEDIA_PROJECTION = 17777; + private OnActivityResultDelegate.Mediator mMediator; + private Activity mActivity; + + public ActivityScreenCaptureRequester(Mediator mediator, Activity activity) { + mMediator = mediator; + mActivity = activity; + mMediator.addDelegate(REQUEST_CODE_MEDIA_PROJECTION, this); + } + + + @Override + public void request() { + mActivity.startActivityForResult(((MediaProjectionManager) mActivity.getSystemService(Context.MEDIA_PROJECTION_SERVICE)).createScreenCaptureIntent(), REQUEST_CODE_MEDIA_PROJECTION); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + mResult = data; + mMediator.removeDelegate(this); + onResult(resultCode, data); + } + } + +} diff --git a/app/src/main/java/com/stardust/autojs/core/image/capture/ScreenCapturer.java b/app/src/main/java/com/stardust/autojs/core/image/capture/ScreenCapturer.java new file mode 100644 index 00000000..2a87fd17 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/image/capture/ScreenCapturer.java @@ -0,0 +1,218 @@ +package com.stardust.autojs.core.image.capture; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.graphics.PixelFormat; +import android.hardware.display.DisplayManager; +import android.hardware.display.VirtualDisplay; +import android.media.Image; +import android.media.ImageReader; +import android.media.projection.MediaProjection; +import android.media.projection.MediaProjectionManager; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import android.util.Log; +import android.view.OrientationEventListener; + +import com.stardust.autojs.runtime.exception.ScriptException; +import com.stardust.autojs.runtime.exception.ScriptInterruptedException; +import com.stardust.lang.ThreadCompat; +import com.stardust.util.ScreenMetrics; + +import java.util.concurrent.atomic.AtomicReference; + +/** + * Created by Stardust on 2017/5/17. + */ +@RequiresApi(Build.VERSION_CODES.LOLLIPOP) +public class ScreenCapturer { + + public static final int ORIENTATION_AUTO = Configuration.ORIENTATION_UNDEFINED; + public static final int ORIENTATION_LANDSCAPE = Configuration.ORIENTATION_LANDSCAPE ; + public static final int ORIENTATION_PORTRAIT = Configuration.ORIENTATION_PORTRAIT ; + + + private static final String LOG_TAG = "ScreenCapturer"; + private final MediaProjectionManager mProjectionManager; + private ImageReader mImageReader; + private MediaProjection mMediaProjection; + private VirtualDisplay mVirtualDisplay; + private volatile Looper mImageAcquireLooper; + private volatile Image mUnderUsingImage; + private volatile AtomicReference mCachedImage = new AtomicReference<>(); + private volatile Exception mException; + private final int mScreenDensity; + private Handler mHandler; + private Intent mData; + private Context mContext; + private int mOrientation = -1; + private int mDetectedOrientation; + private OrientationEventListener mOrientationEventListener; + + public ScreenCapturer(Context context, Intent data, int orientation, int screenDensity, Handler handler) { + mContext = context; + mData = data; + mScreenDensity = screenDensity; + mHandler = handler; + mProjectionManager = (MediaProjectionManager) context.getSystemService(Context.MEDIA_PROJECTION_SERVICE); + mMediaProjection = mProjectionManager.getMediaProjection(Activity.RESULT_OK, (Intent) mData.clone()); + mHandler = handler; + setOrientation(orientation); + observeOrientation(); + } + + private void observeOrientation() { + mOrientationEventListener = new OrientationEventListener(mContext) { + @Override + public void onOrientationChanged(int o) { + int orientation = mContext.getResources().getConfiguration().orientation; + if (mOrientation == ORIENTATION_AUTO && mDetectedOrientation != orientation) { + mDetectedOrientation = orientation; + try { + refreshVirtualDisplay(orientation); + }catch (Exception e){ + e.printStackTrace(); + mException = e; + } + } + } + + }; + if (mOrientationEventListener.canDetectOrientation()) { + mOrientationEventListener.enable(); + } + } + + public void setOrientation(int orientation) { + if (mOrientation == orientation) + return; + mOrientation = orientation; + mDetectedOrientation = mContext.getResources().getConfiguration().orientation; + refreshVirtualDisplay(mOrientation == ORIENTATION_AUTO ? mDetectedOrientation : mOrientation); + } + + + private void refreshVirtualDisplay(int orientation) { + if (mImageAcquireLooper != null) { + mImageAcquireLooper.quit(); + } + if (mImageReader != null) { + mImageReader.close(); + } + if (mVirtualDisplay != null) { + mVirtualDisplay.release(); + } + if (mMediaProjection != null) { + mMediaProjection.stop(); + } + mMediaProjection = mProjectionManager.getMediaProjection(Activity.RESULT_OK, (Intent) mData.clone()); + int screenHeight = ScreenMetrics.getOrientationAwareScreenHeight(orientation); + int screenWidth = ScreenMetrics.getOrientationAwareScreenWidth(orientation); + initVirtualDisplay(screenWidth, screenHeight, mScreenDensity); + startAcquireImageLoop(); + } + + private void initVirtualDisplay(int width, int height, int screenDensity) { + mImageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 3); + mVirtualDisplay = mMediaProjection.createVirtualDisplay(LOG_TAG, + width, height, screenDensity, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, + mImageReader.getSurface(), null, null); + } + + private void startAcquireImageLoop() { + if (mHandler != null) { + setImageListener(mHandler); + return; + } + new Thread(() -> { + Log.d(LOG_TAG, "AcquireImageLoop: start"); + Looper.prepare(); + mImageAcquireLooper = Looper.myLooper(); + setImageListener(new Handler()); + Looper.loop(); + Log.d(LOG_TAG, "AcquireImageLoop: stop"); + }).start(); + } + + private void setImageListener(Handler handler) { + mImageReader.setOnImageAvailableListener(reader -> { + try { + Image oldCacheImage = mCachedImage.getAndSet(null); + if (oldCacheImage != null) { + oldCacheImage.close(); + } + mCachedImage.set(reader.acquireLatestImage()); + } catch (Exception e) { + mException = e; + } + + }, handler); + } + + @Nullable + public Image capture() { + Exception e = mException; + if (e != null) { + mException = null; + throw new ScriptException(e); + } + Thread thread = ThreadCompat.currentThread(); + while (!thread.isInterrupted()) { + Image cachedImage = mCachedImage.getAndSet(null); + if (cachedImage != null) { + if (mUnderUsingImage != null) { + mUnderUsingImage.close(); + } + mUnderUsingImage = cachedImage; + return cachedImage; + } + } + throw new ScriptInterruptedException(); + } + + public int getScreenDensity() { + return mScreenDensity; + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public void release() { + if (mImageAcquireLooper != null) { + mImageAcquireLooper.quit(); + } + if (mMediaProjection != null) { + mMediaProjection.stop(); + mMediaProjection = null; + } + if (mVirtualDisplay != null) { + mVirtualDisplay.release(); + } + if (mImageReader != null) { + mImageReader.close(); + } + if (mUnderUsingImage != null) { + mUnderUsingImage.close(); + } + Image cachedImage = mCachedImage.getAndSet(null); + if (cachedImage != null) { + cachedImage.close(); + } + if (mOrientationEventListener != null) { + mOrientationEventListener.disable(); + } + } + + @Override + protected void finalize() throws Throwable { + try { + release(); + } finally { + super.finalize(); + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/stardust/autojs/core/inputevent/InputDevices.java b/app/src/main/java/com/stardust/autojs/core/inputevent/InputDevices.java new file mode 100644 index 00000000..2d48a0ed --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/inputevent/InputDevices.java @@ -0,0 +1,39 @@ +package com.stardust.autojs.core.inputevent; + +import androidx.annotation.Nullable; +import android.util.Log; +import android.view.InputDevice; + + +/** + * Created by Stardust on 2017/8/1. + */ + +public class InputDevices { + + private static final String LOG_TAG = "InputDevices"; + + @Nullable + public static String getTouchDeviceName() { + InputDevice device = getTouchDevice(); + return device == null ? null : device.getName(); + } + + @Nullable + public static InputDevice getTouchDevice() { + for (int id : InputDevice.getDeviceIds()) { + InputDevice device = InputDevice.getDevice(id); + Log.d(LOG_TAG, "device: " + device); + if (supportSource(device, InputDevice.SOURCE_TOUCHSCREEN) || supportSource(device, InputDevice.SOURCE_TOUCHPAD)) { + return device; + } + } + return null; + } + + private static boolean supportSource(InputDevice device, int source) { + return (device.getSources() & source) == source; + } + + +} diff --git a/app/src/main/java/com/stardust/autojs/core/inputevent/InputEventCodes.java b/app/src/main/java/com/stardust/autojs/core/inputevent/InputEventCodes.java new file mode 100644 index 00000000..22daf439 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/inputevent/InputEventCodes.java @@ -0,0 +1,837 @@ +package com.stardust.autojs.core.inputevent; + +/** + * Created by Stardust on 2017/8/5. + *

+ * from linux/input-event-codes.h + */ + + +public class InputEventCodes { + + + public static final int UP = 0x00; + public static final int DOWN = 0x01; + + +/* + * Device properties and quirks + */ + + public static final int INPUT_PROP_POINTER = 0x00; /* needs a pointer */ + public static final int INPUT_PROP_DIRECT = 0x01; /* direct input devices */ + public static final int INPUT_PROP_BUTTONPAD = 0x02; /* has button(s) under pad */ + public static final int INPUT_PROP_SEMI_MT = 0x03; /* touch rectangle only */ + public static final int INPUT_PROP_TOPBUTTONPAD = 0x04; /* softbuttons at top of pad */ + public static final int INPUT_PROP_POINTING_STICK = 0x05; /* is a pointing stick */ + public static final int INPUT_PROP_ACCELEROMETER = 0x06; /* has accelerometer */ + + public static final int INPUT_PROP_MAX = 0x1f; + public static final int INPUT_PROP_CNT = (INPUT_PROP_MAX + 1); + +/* + * Event types + */ + + public static final int EV_SYN = 0x00; + public static final int EV_KEY = 0x01; + public static final int EV_REL = 0x02; + public static final int EV_ABS = 0x03; + public static final int EV_MSC = 0x04; + public static final int EV_SW = 0x05; + public static final int EV_LED = 0x11; + public static final int EV_SND = 0x12; + public static final int EV_REP = 0x14; + public static final int EV_FF = 0x15; + public static final int EV_PWR = 0x16; + public static final int EV_FF_STATUS = 0x17; + public static final int EV_MAX = 0x1f; + public static final int EV_CNT = (EV_MAX + 1); + +/* + * Synchronization events. + */ + + public static final int SYN_REPORT = 0; + public static final int SYN_CONFIG = 1; + public static final int SYN_MT_REPORT = 2; + public static final int SYN_DROPPED = 3; + public static final int SYN_MAX = 0xf; + public static final int SYN_CNT = (SYN_MAX + 1); + +/* + * Keys and buttons + * + * Most of the keys/buttons are modeled after USB HUT 1.12 + * (see http://www.usb.org/developers/hidpage). + * Abbreviations in the comments: + * AC - Application Control + * AL - Application Launch Button + * SC - System Control + */ + + public static final int KEY_RESERVED = 0; + public static final int KEY_ESC = 1; + public static final int KEY_1 = 2; + public static final int KEY_2 = 3; + public static final int KEY_3 = 4; + public static final int KEY_4 = 5; + public static final int KEY_5 = 6; + public static final int KEY_6 = 7; + public static final int KEY_7 = 8; + public static final int KEY_8 = 9; + public static final int KEY_9 = 10; + public static final int KEY_0 = 11; + public static final int KEY_MINUS = 12; + public static final int KEY_EQUAL = 13; + public static final int KEY_BACKSPACE = 14; + public static final int KEY_TAB = 15; + public static final int KEY_Q = 16; + public static final int KEY_W = 17; + public static final int KEY_E = 18; + public static final int KEY_R = 19; + public static final int KEY_T = 20; + public static final int KEY_Y = 21; + public static final int KEY_U = 22; + public static final int KEY_I = 23; + public static final int KEY_O = 24; + public static final int KEY_P = 25; + public static final int KEY_LEFTBRACE = 26; + public static final int KEY_RIGHTBRACE = 27; + public static final int KEY_ENTER = 28; + public static final int KEY_LEFTCTRL = 29; + public static final int KEY_A = 30; + public static final int KEY_S = 31; + public static final int KEY_D = 32; + public static final int KEY_F = 33; + public static final int KEY_G = 34; + public static final int KEY_H = 35; + public static final int KEY_J = 36; + public static final int KEY_K = 37; + public static final int KEY_L = 38; + public static final int KEY_SEMICOLON = 39; + public static final int KEY_APOSTROPHE = 40; + public static final int KEY_GRAVE = 41; + public static final int KEY_LEFTSHIFT = 42; + public static final int KEY_BACKSLASH = 43; + public static final int KEY_Z = 44; + public static final int KEY_X = 45; + public static final int KEY_C = 46; + public static final int KEY_V = 47; + public static final int KEY_B = 48; + public static final int KEY_N = 49; + public static final int KEY_M = 50; + public static final int KEY_COMMA = 51; + public static final int KEY_DOT = 52; + public static final int KEY_SLASH = 53; + public static final int KEY_RIGHTSHIFT = 54; + public static final int KEY_KPASTERISK = 55; + public static final int KEY_LEFTALT = 56; + public static final int KEY_SPACE = 57; + public static final int KEY_CAPSLOCK = 58; + public static final int KEY_F1 = 59; + public static final int KEY_F2 = 60; + public static final int KEY_F3 = 61; + public static final int KEY_F4 = 62; + public static final int KEY_F5 = 63; + public static final int KEY_F6 = 64; + public static final int KEY_F7 = 65; + public static final int KEY_F8 = 66; + public static final int KEY_F9 = 67; + public static final int KEY_F10 = 68; + public static final int KEY_NUMLOCK = 69; + public static final int KEY_SCROLLLOCK = 70; + public static final int KEY_KP7 = 71; + public static final int KEY_KP8 = 72; + public static final int KEY_KP9 = 73; + public static final int KEY_KPMINUS = 74; + public static final int KEY_KP4 = 75; + public static final int KEY_KP5 = 76; + public static final int KEY_KP6 = 77; + public static final int KEY_KPPLUS = 78; + public static final int KEY_KP1 = 79; + public static final int KEY_KP2 = 80; + public static final int KEY_KP3 = 81; + public static final int KEY_KP0 = 82; + public static final int KEY_KPDOT = 83; + + public static final int KEY_ZENKAKUHANKAKU = 85; + public static final int KEY_102ND = 86; + public static final int KEY_F11 = 87; + public static final int KEY_F12 = 88; + public static final int KEY_RO = 89; + public static final int KEY_KATAKANA = 90; + public static final int KEY_HIRAGANA = 91; + public static final int KEY_HENKAN = 92; + public static final int KEY_KATAKANAHIRAGANA = 93; + public static final int KEY_MUHENKAN = 94; + public static final int KEY_KPJPCOMMA = 95; + public static final int KEY_KPENTER = 96; + public static final int KEY_RIGHTCTRL = 97; + public static final int KEY_KPSLASH = 98; + public static final int KEY_SYSRQ = 99; + public static final int KEY_RIGHTALT = 100; + public static final int KEY_LINEFEED = 101; + public static final int KEY_HOME = 102; + public static final int KEY_UP = 103; + public static final int KEY_PAGEUP = 104; + public static final int KEY_LEFT = 105; + public static final int KEY_RIGHT = 106; + public static final int KEY_END = 107; + public static final int KEY_DOWN = 108; + public static final int KEY_PAGEDOWN = 109; + public static final int KEY_INSERT = 110; + public static final int KEY_DELETE = 111; + public static final int KEY_MACRO = 112; + public static final int KEY_MUTE = 113; + public static final int KEY_VOLUMEDOWN = 114; + public static final int KEY_VOLUMEUP = 115; + public static final int KEY_POWER = 116; /* SC System Power Down */ + public static final int KEY_KPEQUAL = 117; + public static final int KEY_KPPLUSMINUS = 118; + public static final int KEY_PAUSE = 119; + public static final int KEY_SCALE = 120; /* AL Compiz Scale (Expose) */ + + public static final int KEY_KPCOMMA = 121; + public static final int KEY_HANGEUL = 122; + public static final int KEY_HANGUEL = KEY_HANGEUL; + public static final int KEY_HANJA = 123; + public static final int KEY_YEN = 124; + public static final int KEY_LEFTMETA = 125; + public static final int KEY_RIGHTMETA = 126; + public static final int KEY_COMPOSE = 127; + + public static final int KEY_STOP = 128; /* AC Stop */ + public static final int KEY_AGAIN = 129; + public static final int KEY_PROPS = 130; /* AC Properties */ + public static final int KEY_UNDO = 131; /* AC Undo */ + public static final int KEY_FRONT = 132; + public static final int KEY_COPY = 133; /* AC Copy */ + public static final int KEY_OPEN = 134; /* AC Open */ + public static final int KEY_PASTE = 135; /* AC Paste */ + public static final int KEY_FIND = 136; /* AC Search */ + public static final int KEY_CUT = 137; /* AC Cut */ + public static final int KEY_HELP = 138; /* AL Integrated Help Center */ + public static final int KEY_MENU = 139; /* Menu (show menu) */ + public static final int KEY_CALC = 140; /* AL Calculator */ + public static final int KEY_SETUP = 141; + public static final int KEY_SLEEP = 142; /* SC System Sleep */ + public static final int KEY_WAKEUP = 143; /* System Wake Up */ + public static final int KEY_FILE = 144; /* AL Local Machine Browser */ + public static final int KEY_SENDFILE = 145; + public static final int KEY_DELETEFILE = 146; + public static final int KEY_XFER = 147; + public static final int KEY_PROG1 = 148; + public static final int KEY_PROG2 = 149; + public static final int KEY_WWW = 150; /* AL Internet Browser */ + public static final int KEY_MSDOS = 151; + public static final int KEY_COFFEE = 152; /* AL Terminal Lock/Screensaver */ + public static final int KEY_SCREENLOCK = KEY_COFFEE; + public static final int KEY_ROTATE_DISPLAY = 153; /* Display orientation for e.g. tablets */ + public static final int KEY_DIRECTION = KEY_ROTATE_DISPLAY; + public static final int KEY_CYCLEWINDOWS = 154; + public static final int KEY_MAIL = 155; + public static final int KEY_BOOKMARKS = 156; /* AC Bookmarks */ + public static final int KEY_COMPUTER = 157; + public static final int KEY_BACK = 158; /* AC Back */ + public static final int KEY_FORWARD = 159; /* AC Forward */ + public static final int KEY_CLOSECD = 160; + public static final int KEY_EJECTCD = 161; + public static final int KEY_EJECTCLOSECD = 162; + public static final int KEY_NEXTSONG = 163; + public static final int KEY_PLAYPAUSE = 164; + public static final int KEY_PREVIOUSSONG = 165; + public static final int KEY_STOPCD = 166; + public static final int KEY_RECORD = 167; + public static final int KEY_REWIND = 168; + public static final int KEY_PHONE = 169; /* Media Select Telephone */ + public static final int KEY_ISO = 170; + public static final int KEY_CONFIG = 171; /* AL Consumer Control Configuration */ + public static final int KEY_HOMEPAGE = 172; /* AC Home */ + public static final int KEY_REFRESH = 173; /* AC Refresh */ + public static final int KEY_EXIT = 174; /* AC Exit */ + public static final int KEY_MOVE = 175; + public static final int KEY_EDIT = 176; + public static final int KEY_SCROLLUP = 177; + public static final int KEY_SCROLLDOWN = 178; + public static final int KEY_KPLEFTPAREN = 179; + public static final int KEY_KPRIGHTPAREN = 180; + public static final int KEY_NEW = 181; /* AC New */ + public static final int KEY_REDO = 182; /* AC Redo/Repeat */ + + public static final int KEY_F13 = 183; + public static final int KEY_F14 = 184; + public static final int KEY_F15 = 185; + public static final int KEY_F16 = 186; + public static final int KEY_F17 = 187; + public static final int KEY_F18 = 188; + public static final int KEY_F19 = 189; + public static final int KEY_F20 = 190; + public static final int KEY_F21 = 191; + public static final int KEY_F22 = 192; + public static final int KEY_F23 = 193; + public static final int KEY_F24 = 194; + + public static final int KEY_PLAYCD = 200; + public static final int KEY_PAUSECD = 201; + public static final int KEY_PROG3 = 202; + public static final int KEY_PROG4 = 203; + public static final int KEY_DASHBOARD = 204; /* AL Dashboard */ + public static final int KEY_SUSPEND = 205; + public static final int KEY_CLOSE = 206; /* AC Close */ + public static final int KEY_PLAY = 207; + public static final int KEY_FASTFORWARD = 208; + public static final int KEY_BASSBOOST = 209; + public static final int KEY_PRINT = 210; /* AC Print */ + public static final int KEY_HP = 211; + public static final int KEY_CAMERA = 212; + public static final int KEY_SOUND = 213; + public static final int KEY_QUESTION = 214; + public static final int KEY_EMAIL = 215; + public static final int KEY_CHAT = 216; + public static final int KEY_SEARCH = 217; + public static final int KEY_CONNECT = 218; + public static final int KEY_FINANCE = 219; /* AL Checkbook/Finance */ + public static final int KEY_SPORT = 220; + public static final int KEY_SHOP = 221; + public static final int KEY_ALTERASE = 222; + public static final int KEY_CANCEL = 223; /* AC Cancel */ + public static final int KEY_BRIGHTNESSDOWN = 224; + public static final int KEY_BRIGHTNESSUP = 225; + public static final int KEY_MEDIA = 226; + + public static final int KEY_SWITCHVIDEOMODE = 227; /* Cycle between available video + outputs (Monitor/LCD/TV-out/etc) */ + public static final int KEY_KBDILLUMTOGGLE = 228; + public static final int KEY_KBDILLUMDOWN = 229; + public static final int KEY_KBDILLUMUP = 230; + + public static final int KEY_SEND = 231; /* AC Send */ + public static final int KEY_REPLY = 232; /* AC Reply */ + public static final int KEY_FORWARDMAIL = 233; /* AC Forward Msg */ + public static final int KEY_SAVE = 234; /* AC Save */ + public static final int KEY_DOCUMENTS = 235; + + public static final int KEY_BATTERY = 236; + + public static final int KEY_BLUETOOTH = 237; + public static final int KEY_WLAN = 238; + public static final int KEY_UWB = 239; + + public static final int KEY_UNKNOWN = 240; + + public static final int KEY_VIDEO_NEXT = 241; /* drive next video source */ + public static final int KEY_VIDEO_PREV = 242; /* drive previous video source */ + public static final int KEY_BRIGHTNESS_CYCLE = 243; /* brightness up, after max is min */ + public static final int KEY_BRIGHTNESS_AUTO = 244; /* Set Auto Brightness: manual + brightness control is off, + rely on ambient */ + public static final int KEY_BRIGHTNESS_ZERO = KEY_BRIGHTNESS_AUTO; + public static final int KEY_DISPLAY_OFF = 245; /* display device to off state */ + + public static final int KEY_WWAN = 246; /* Wireless WAN (LTE, UMTS, GSM, etc.) */ + public static final int KEY_WIMAX = KEY_WWAN; + public static final int KEY_RFKILL = 247; /* Key that controls all radios */ + + public static final int KEY_MICMUTE = 248; /* Mute / unmute the microphone */ + +/* Code 255 is reserved for special needs of AT keyboard driver */ + + public static final int BTN_MISC = 0x100; + public static final int BTN_0 = 0x100; + public static final int BTN_1 = 0x101; + public static final int BTN_2 = 0x102; + public static final int BTN_3 = 0x103; + public static final int BTN_4 = 0x104; + public static final int BTN_5 = 0x105; + public static final int BTN_6 = 0x106; + public static final int BTN_7 = 0x107; + public static final int BTN_8 = 0x108; + public static final int BTN_9 = 0x109; + + public static final int BTN_MOUSE = 0x110; + public static final int BTN_LEFT = 0x110; + public static final int BTN_RIGHT = 0x111; + public static final int BTN_MIDDLE = 0x112; + public static final int BTN_SIDE = 0x113; + public static final int BTN_EXTRA = 0x114; + public static final int BTN_FORWARD = 0x115; + public static final int BTN_BACK = 0x116; + public static final int BTN_TASK = 0x117; + + public static final int BTN_JOYSTICK = 0x120; + public static final int BTN_TRIGGER = 0x120; + public static final int BTN_THUMB = 0x121; + public static final int BTN_THUMB2 = 0x122; + public static final int BTN_TOP = 0x123; + public static final int BTN_TOP2 = 0x124; + public static final int BTN_PINKIE = 0x125; + public static final int BTN_BASE = 0x126; + public static final int BTN_BASE2 = 0x127; + public static final int BTN_BASE3 = 0x128; + public static final int BTN_BASE4 = 0x129; + public static final int BTN_BASE5 = 0x12a; + public static final int BTN_BASE6 = 0x12b; + public static final int BTN_DEAD = 0x12f; + + public static final int BTN_GAMEPAD = 0x130; + public static final int BTN_SOUTH = 0x130; + public static final int BTN_A = BTN_SOUTH; + public static final int BTN_EAST = 0x131; + public static final int BTN_B = BTN_EAST; + public static final int BTN_C = 0x132; + public static final int BTN_NORTH = 0x133; + public static final int BTN_X = BTN_NORTH; + public static final int BTN_WEST = 0x134; + public static final int BTN_Y = BTN_WEST; + public static final int BTN_Z = 0x135; + public static final int BTN_TL = 0x136; + public static final int BTN_TR = 0x137; + public static final int BTN_TL2 = 0x138; + public static final int BTN_TR2 = 0x139; + public static final int BTN_SELECT = 0x13a; + public static final int BTN_START = 0x13b; + public static final int BTN_MODE = 0x13c; + public static final int BTN_THUMBL = 0x13d; + public static final int BTN_THUMBR = 0x13e; + + public static final int BTN_DIGI = 0x140; + public static final int BTN_TOOL_PEN = 0x140; + public static final int BTN_TOOL_RUBBER = 0x141; + public static final int BTN_TOOL_BRUSH = 0x142; + public static final int BTN_TOOL_PENCIL = 0x143; + public static final int BTN_TOOL_AIRBRUSH = 0x144; + public static final int BTN_TOOL_FINGER = 0x145; + public static final int BTN_TOOL_MOUSE = 0x146; + public static final int BTN_TOOL_LENS = 0x147; + public static final int BTN_TOOL_QUINTTAP = 0x148; /* Five fingers on trackpad */ + public static final int BTN_TOUCH = 0x14a; + public static final int BTN_STYLUS = 0x14b; + public static final int BTN_STYLUS2 = 0x14c; + public static final int BTN_TOOL_DOUBLETAP = 0x14d; + public static final int BTN_TOOL_TRIPLETAP = 0x14e; + public static final int BTN_TOOL_QUADTAP = 0x14f; /* Four fingers on trackpad */ + + public static final int BTN_WHEEL = 0x150; + public static final int BTN_GEAR_DOWN = 0x150; + public static final int BTN_GEAR_UP = 0x151; + + public static final int KEY_OK = 0x160; + public static final int KEY_SELECT = 0x161; + public static final int KEY_GOTO = 0x162; + public static final int KEY_CLEAR = 0x163; + public static final int KEY_POWER2 = 0x164; + public static final int KEY_OPTION = 0x165; + public static final int KEY_INFO = 0x166; /* AL OEM Features/Tips/Tutorial */ + public static final int KEY_TIME = 0x167; + public static final int KEY_VENDOR = 0x168; + public static final int KEY_ARCHIVE = 0x169; + public static final int KEY_PROGRAM = 0x16a; /* Media Select Program Guide */ + public static final int KEY_CHANNEL = 0x16b; + public static final int KEY_FAVORITES = 0x16c; + public static final int KEY_EPG = 0x16d; + public static final int KEY_PVR = 0x16e; /* Media Select Home */ + public static final int KEY_MHP = 0x16f; + public static final int KEY_LANGUAGE = 0x170; + public static final int KEY_TITLE = 0x171; + public static final int KEY_SUBTITLE = 0x172; + public static final int KEY_ANGLE = 0x173; + public static final int KEY_ZOOM = 0x174; + public static final int KEY_MODE = 0x175; + public static final int KEY_KEYBOARD = 0x176; + public static final int KEY_SCREEN = 0x177; + public static final int KEY_PC = 0x178; /* Media Select Computer */ + public static final int KEY_TV = 0x179; /* Media Select TV */ + public static final int KEY_TV2 = 0x17a; /* Media Select Cable */ + public static final int KEY_VCR = 0x17b; /* Media Select VCR */ + public static final int KEY_VCR2 = 0x17c; /* VCR Plus */ + public static final int KEY_SAT = 0x17d; /* Media Select Satellite */ + public static final int KEY_SAT2 = 0x17e; + public static final int KEY_CD = 0x17f; /* Media Select CD */ + public static final int KEY_TAPE = 0x180; /* Media Select Tape */ + public static final int KEY_RADIO = 0x181; + public static final int KEY_TUNER = 0x182; /* Media Select Tuner */ + public static final int KEY_PLAYER = 0x183; + public static final int KEY_TEXT = 0x184; + public static final int KEY_DVD = 0x185; /* Media Select DVD */ + public static final int KEY_AUX = 0x186; + public static final int KEY_MP3 = 0x187; + public static final int KEY_AUDIO = 0x188; /* AL Audio Browser */ + public static final int KEY_VIDEO = 0x189; /* AL Movie Browser */ + public static final int KEY_DIRECTORY = 0x18a; + public static final int KEY_LIST = 0x18b; + public static final int KEY_MEMO = 0x18c; /* Media Select Messages */ + public static final int KEY_CALENDAR = 0x18d; + public static final int KEY_RED = 0x18e; + public static final int KEY_GREEN = 0x18f; + public static final int KEY_YELLOW = 0x190; + public static final int KEY_BLUE = 0x191; + public static final int KEY_CHANNELUP = 0x192; /* Channel Increment */ + public static final int KEY_CHANNELDOWN = 0x193; /* Channel Decrement */ + public static final int KEY_FIRST = 0x194; + public static final int KEY_LAST = 0x195; /* Recall Last */ + public static final int KEY_AB = 0x196; + public static final int KEY_NEXT = 0x197; + public static final int KEY_RESTART = 0x198; + public static final int KEY_SLOW = 0x199; + public static final int KEY_SHUFFLE = 0x19a; + public static final int KEY_BREAK = 0x19b; + public static final int KEY_PREVIOUS = 0x19c; + public static final int KEY_DIGITS = 0x19d; + public static final int KEY_TEEN = 0x19e; + public static final int KEY_TWEN = 0x19f; + public static final int KEY_VIDEOPHONE = 0x1a0; /* Media Select Video Phone */ + public static final int KEY_GAMES = 0x1a1; /* Media Select Games */ + public static final int KEY_ZOOMIN = 0x1a2; /* AC Zoom In */ + public static final int KEY_ZOOMOUT = 0x1a3; /* AC Zoom Out */ + public static final int KEY_ZOOMRESET = 0x1a4; /* AC Zoom */ + public static final int KEY_WORDPROCESSOR = 0x1a5; /* AL Word Processor */ + public static final int KEY_EDITOR = 0x1a6; /* AL Text Editor */ + public static final int KEY_SPREADSHEET = 0x1a7; /* AL Spreadsheet */ + public static final int KEY_GRAPHICSEDITOR = 0x1a8; /* AL Graphics Editor */ + public static final int KEY_PRESENTATION = 0x1a9; /* AL Presentation App */ + public static final int KEY_DATABASE = 0x1aa; /* AL Database App */ + public static final int KEY_NEWS = 0x1ab; /* AL Newsreader */ + public static final int KEY_VOICEMAIL = 0x1ac; /* AL Voicemail */ + public static final int KEY_ADDRESSBOOK = 0x1ad; /* AL Contacts/Address Book */ + public static final int KEY_MESSENGER = 0x1ae; /* AL Instant Messaging */ + public static final int KEY_DISPLAYTOGGLE = 0x1af; /* Turn display (LCD) on and off */ + public static final int KEY_BRIGHTNESS_TOGGLE = KEY_DISPLAYTOGGLE; + public static final int KEY_SPELLCHECK = 0x1b0; /* AL Spell Check */ + public static final int KEY_LOGOFF = 0x1b1; /* AL Logoff */ + + public static final int KEY_DOLLAR = 0x1b2; + public static final int KEY_EURO = 0x1b3; + + public static final int KEY_FRAMEBACK = 0x1b4; /* Consumer - transport controls */ + public static final int KEY_FRAMEFORWARD = 0x1b5; + public static final int KEY_CONTEXT_MENU = 0x1b6; /* GenDesc - system context menu */ + public static final int KEY_MEDIA_REPEAT = 0x1b7; /* Consumer - transport control */ + public static final int KEY_10CHANNELSUP = 0x1b8; /* 10 channels up (10+) */ + public static final int KEY_10CHANNELSDOWN = 0x1b9; /* 10 channels down (10-) */ + public static final int KEY_IMAGES = 0x1ba; /* AL Image Browser */ + + public static final int KEY_DEL_EOL = 0x1c0; + public static final int KEY_DEL_EOS = 0x1c1; + public static final int KEY_INS_LINE = 0x1c2; + public static final int KEY_DEL_LINE = 0x1c3; + + public static final int KEY_FN = 0x1d0; + public static final int KEY_FN_ESC = 0x1d1; + public static final int KEY_FN_F1 = 0x1d2; + public static final int KEY_FN_F2 = 0x1d3; + public static final int KEY_FN_F3 = 0x1d4; + public static final int KEY_FN_F4 = 0x1d5; + public static final int KEY_FN_F5 = 0x1d6; + public static final int KEY_FN_F6 = 0x1d7; + public static final int KEY_FN_F7 = 0x1d8; + public static final int KEY_FN_F8 = 0x1d9; + public static final int KEY_FN_F9 = 0x1da; + public static final int KEY_FN_F10 = 0x1db; + public static final int KEY_FN_F11 = 0x1dc; + public static final int KEY_FN_F12 = 0x1dd; + public static final int KEY_FN_1 = 0x1de; + public static final int KEY_FN_2 = 0x1df; + public static final int KEY_FN_D = 0x1e0; + public static final int KEY_FN_E = 0x1e1; + public static final int KEY_FN_F = 0x1e2; + public static final int KEY_FN_S = 0x1e3; + public static final int KEY_FN_B = 0x1e4; + + public static final int KEY_BRL_DOT1 = 0x1f1; + public static final int KEY_BRL_DOT2 = 0x1f2; + public static final int KEY_BRL_DOT3 = 0x1f3; + public static final int KEY_BRL_DOT4 = 0x1f4; + public static final int KEY_BRL_DOT5 = 0x1f5; + public static final int KEY_BRL_DOT6 = 0x1f6; + public static final int KEY_BRL_DOT7 = 0x1f7; + public static final int KEY_BRL_DOT8 = 0x1f8; + public static final int KEY_BRL_DOT9 = 0x1f9; + public static final int KEY_BRL_DOT10 = 0x1fa; + + public static final int KEY_NUMERIC_0 = 0x200; /* used by phones, remote controls, */ + public static final int KEY_NUMERIC_1 = 0x201; /* and other keypads */ + public static final int KEY_NUMERIC_2 = 0x202; + public static final int KEY_NUMERIC_3 = 0x203; + public static final int KEY_NUMERIC_4 = 0x204; + public static final int KEY_NUMERIC_5 = 0x205; + public static final int KEY_NUMERIC_6 = 0x206; + public static final int KEY_NUMERIC_7 = 0x207; + public static final int KEY_NUMERIC_8 = 0x208; + public static final int KEY_NUMERIC_9 = 0x209; + public static final int KEY_NUMERIC_STAR = 0x20a; + public static final int KEY_NUMERIC_POUND = 0x20b; + public static final int KEY_NUMERIC_A = 0x20c; /* Phone key A - HUT Telephony 0xb9 */ + public static final int KEY_NUMERIC_B = 0x20d; + public static final int KEY_NUMERIC_C = 0x20e; + public static final int KEY_NUMERIC_D = 0x20f; + + public static final int KEY_CAMERA_FOCUS = 0x210; + public static final int KEY_WPS_BUTTON = 0x211; /* WiFi Protected Setup key */ + + public static final int KEY_TOUCHPAD_TOGGLE = 0x212; /* Request switch touchpad on or off */ + public static final int KEY_TOUCHPAD_ON = 0x213; + public static final int KEY_TOUCHPAD_OFF = 0x214; + + public static final int KEY_CAMERA_ZOOMIN = 0x215; + public static final int KEY_CAMERA_ZOOMOUT = 0x216; + public static final int KEY_CAMERA_UP = 0x217; + public static final int KEY_CAMERA_DOWN = 0x218; + public static final int KEY_CAMERA_LEFT = 0x219; + public static final int KEY_CAMERA_RIGHT = 0x21a; + + public static final int KEY_ATTENDANT_ON = 0x21b; + public static final int KEY_ATTENDANT_OFF = 0x21c; + public static final int KEY_ATTENDANT_TOGGLE = 0x21d; /* Attendant call on or off */ + public static final int KEY_LIGHTS_TOGGLE = 0x21e; /* Reading light on or off */ + + public static final int BTN_DPAD_UP = 0x220; + public static final int BTN_DPAD_DOWN = 0x221; + public static final int BTN_DPAD_LEFT = 0x222; + public static final int BTN_DPAD_RIGHT = 0x223; + + public static final int KEY_ALS_TOGGLE = 0x230; /* Ambient light sensor */ + + public static final int KEY_BUTTONCONFIG = 0x240; /* AL Button Configuration */ + public static final int KEY_TASKMANAGER = 0x241; /* AL Task/Project Manager */ + public static final int KEY_JOURNAL = 0x242; /* AL Log/Journal/Timecard */ + public static final int KEY_CONTROLPANEL = 0x243; /* AL Control Panel */ + public static final int KEY_APPSELECT = 0x244; /* AL Select Task/Application */ + public static final int KEY_SCREENSAVER = 0x245; /* AL Screen Saver */ + public static final int KEY_VOICECOMMAND = 0x246; /* Listening Voice Command */ + public static final int KEY_ASSISTANT = 0x247; /* AL Context-aware desktop assistant */ + + public static final int KEY_BRIGHTNESS_MIN = 0x250; /* Set Brightness to Minimum */ + public static final int KEY_BRIGHTNESS_MAX = 0x251; /* Set Brightness to Maximum */ + + public static final int KEY_KBDINPUTASSIST_PREV = 0x260; + public static final int KEY_KBDINPUTASSIST_NEXT = 0x261; + public static final int KEY_KBDINPUTASSIST_PREVGROUP = 0x262; + public static final int KEY_KBDINPUTASSIST_NEXTGROUP = 0x263; + public static final int KEY_KBDINPUTASSIST_ACCEPT = 0x264; + public static final int KEY_KBDINPUTASSIST_CANCEL = 0x265; + + /* Diagonal movement keys */ + public static final int KEY_RIGHT_UP = 0x266; + public static final int KEY_RIGHT_DOWN = 0x267; + public static final int KEY_LEFT_UP = 0x268; + public static final int KEY_LEFT_DOWN = 0x269; + + public static final int KEY_ROOT_MENU = 0x26a; /* Show Device's Root Menu */ + /* Show Top Menu of the Media (e.g. DVD) */ + public static final int KEY_MEDIA_TOP_MENU = 0x26b; + public static final int KEY_NUMERIC_11 = 0x26c; + public static final int KEY_NUMERIC_12 = 0x26d; + /* + * Toggle Audio Description: refers to an audio service that helps blind and + * visually impaired consumers understand the action in a program. Note: in + * some countries this is referred to as "Video Description". + */ + public static final int KEY_AUDIO_DESC = 0x26e; + public static final int KEY_3D_MODE = 0x26f; + public static final int KEY_NEXT_FAVORITE = 0x270; + public static final int KEY_STOP_RECORD = 0x271; + public static final int KEY_PAUSE_RECORD = 0x272; + public static final int KEY_VOD = 0x273; /* Video on Demand */ + public static final int KEY_UNMUTE = 0x274; + public static final int KEY_FASTREVERSE = 0x275; + public static final int KEY_SLOWREVERSE = 0x276; + /* + * Control a data application associated with the currently viewed channel, + * e.g. teletext or data broadcast application (MHEG, MHP, HbbTV, etc.) + */ + public static final int KEY_DATA = 0x277; + public static final int KEY_ONSCREEN_KEYBOARD = 0x278; + + public static final int BTN_TRIGGER_HAPPY = 0x2c0; + public static final int BTN_TRIGGER_HAPPY1 = 0x2c0; + public static final int BTN_TRIGGER_HAPPY2 = 0x2c1; + public static final int BTN_TRIGGER_HAPPY3 = 0x2c2; + public static final int BTN_TRIGGER_HAPPY4 = 0x2c3; + public static final int BTN_TRIGGER_HAPPY5 = 0x2c4; + public static final int BTN_TRIGGER_HAPPY6 = 0x2c5; + public static final int BTN_TRIGGER_HAPPY7 = 0x2c6; + public static final int BTN_TRIGGER_HAPPY8 = 0x2c7; + public static final int BTN_TRIGGER_HAPPY9 = 0x2c8; + public static final int BTN_TRIGGER_HAPPY10 = 0x2c9; + public static final int BTN_TRIGGER_HAPPY11 = 0x2ca; + public static final int BTN_TRIGGER_HAPPY12 = 0x2cb; + public static final int BTN_TRIGGER_HAPPY13 = 0x2cc; + public static final int BTN_TRIGGER_HAPPY14 = 0x2cd; + public static final int BTN_TRIGGER_HAPPY15 = 0x2ce; + public static final int BTN_TRIGGER_HAPPY16 = 0x2cf; + public static final int BTN_TRIGGER_HAPPY17 = 0x2d0; + public static final int BTN_TRIGGER_HAPPY18 = 0x2d1; + public static final int BTN_TRIGGER_HAPPY19 = 0x2d2; + public static final int BTN_TRIGGER_HAPPY20 = 0x2d3; + public static final int BTN_TRIGGER_HAPPY21 = 0x2d4; + public static final int BTN_TRIGGER_HAPPY22 = 0x2d5; + public static final int BTN_TRIGGER_HAPPY23 = 0x2d6; + public static final int BTN_TRIGGER_HAPPY24 = 0x2d7; + public static final int BTN_TRIGGER_HAPPY25 = 0x2d8; + public static final int BTN_TRIGGER_HAPPY26 = 0x2d9; + public static final int BTN_TRIGGER_HAPPY27 = 0x2da; + public static final int BTN_TRIGGER_HAPPY28 = 0x2db; + public static final int BTN_TRIGGER_HAPPY29 = 0x2dc; + public static final int BTN_TRIGGER_HAPPY30 = 0x2dd; + public static final int BTN_TRIGGER_HAPPY31 = 0x2de; + public static final int BTN_TRIGGER_HAPPY32 = 0x2df; + public static final int BTN_TRIGGER_HAPPY33 = 0x2e0; + public static final int BTN_TRIGGER_HAPPY34 = 0x2e1; + public static final int BTN_TRIGGER_HAPPY35 = 0x2e2; + public static final int BTN_TRIGGER_HAPPY36 = 0x2e3; + public static final int BTN_TRIGGER_HAPPY37 = 0x2e4; + public static final int BTN_TRIGGER_HAPPY38 = 0x2e5; + public static final int BTN_TRIGGER_HAPPY39 = 0x2e6; + public static final int BTN_TRIGGER_HAPPY40 = 0x2e7; + + /* We avoid low common keys in module aliases so they don't get huge. */ + public static final int KEY_MIN_INTERESTING = KEY_MUTE; + public static final int KEY_MAX = 0x2ff; + public static final int KEY_CNT = (KEY_MAX + 1); + +/* + * Relative axes + */ + + public static final int REL_X = 0x00; + public static final int REL_Y = 0x01; + public static final int REL_Z = 0x02; + public static final int REL_RX = 0x03; + public static final int REL_RY = 0x04; + public static final int REL_RZ = 0x05; + public static final int REL_HWHEEL = 0x06; + public static final int REL_DIAL = 0x07; + public static final int REL_WHEEL = 0x08; + public static final int REL_MISC = 0x09; + public static final int REL_MAX = 0x0f; + public static final int REL_CNT = (REL_MAX + 1); + +/* + * Absolute axes + */ + + public static final int ABS_X = 0x00; + public static final int ABS_Y = 0x01; + public static final int ABS_Z = 0x02; + public static final int ABS_RX = 0x03; + public static final int ABS_RY = 0x04; + public static final int ABS_RZ = 0x05; + public static final int ABS_THROTTLE = 0x06; + public static final int ABS_RUDDER = 0x07; + public static final int ABS_WHEEL = 0x08; + public static final int ABS_GAS = 0x09; + public static final int ABS_BRAKE = 0x0a; + public static final int ABS_HAT0X = 0x10; + public static final int ABS_HAT0Y = 0x11; + public static final int ABS_HAT1X = 0x12; + public static final int ABS_HAT1Y = 0x13; + public static final int ABS_HAT2X = 0x14; + public static final int ABS_HAT2Y = 0x15; + public static final int ABS_HAT3X = 0x16; + public static final int ABS_HAT3Y = 0x17; + public static final int ABS_PRESSURE = 0x18; + public static final int ABS_DISTANCE = 0x19; + public static final int ABS_TILT_X = 0x1a; + public static final int ABS_TILT_Y = 0x1b; + public static final int ABS_TOOL_WIDTH = 0x1c; + + public static final int ABS_VOLUME = 0x20; + + public static final int ABS_MISC = 0x28; + + public static final int ABS_MT_SLOT = 0x2f; /* MT slot being modified */ + public static final int ABS_MT_TOUCH_MAJOR = 0x30; /* Major axis of touching ellipse */ + public static final int ABS_MT_TOUCH_MINOR = 0x31; /* Minor axis (omit if circular) */ + public static final int ABS_MT_WIDTH_MAJOR = 0x32; /* Major axis of approaching ellipse */ + public static final int ABS_MT_WIDTH_MINOR = 0x33; /* Minor axis (omit if circular) */ + public static final int ABS_MT_ORIENTATION = 0x34; /* Ellipse orientation */ + public static final int ABS_MT_POSITION_X = 0x35; /* Center X touch position */ + public static final int ABS_MT_POSITION_Y = 0x36; /* Center Y touch position */ + public static final int ABS_MT_TOOL_TYPE = 0x37; /* Type of touching device */ + public static final int ABS_MT_BLOB_ID = 0x38; /* Group a set of packets as a blob */ + public static final int ABS_MT_TRACKING_ID = 0x39; /* Unique ID of initiated contact */ + public static final int ABS_MT_PRESSURE = 0x3a; /* Pressure on contact area */ + public static final int ABS_MT_DISTANCE = 0x3b; /* Contact hover distance */ + public static final int ABS_MT_TOOL_X = 0x3c; /* Center X tool position */ + public static final int ABS_MT_TOOL_Y = 0x3d; /* Center Y tool position */ + + + public static final int ABS_MAX = 0x3f; + public static final int ABS_CNT = (ABS_MAX + 1); + +/* + * Switch events + */ + + public static final int SW_LID = 0x00; /* set = lid shut */ + public static final int SW_TABLET_MODE = 0x01; /* set = tablet mode */ + public static final int SW_HEADPHONE_INSERT = 0x02; /* set = inserted */ + public static final int SW_RFKILL_ALL = 0x03; /* rfkill master switch, type "any" + set = radio enabled */ + public static final int SW_RADIO = SW_RFKILL_ALL; /* deprecated */ + public static final int SW_MICROPHONE_INSERT = 0x04; /* set = inserted */ + public static final int SW_DOCK = 0x05; /* set = plugged into dock */ + public static final int SW_LINEOUT_INSERT = 0x06; /* set = inserted */ + public static final int SW_JACK_PHYSICAL_INSERT = 0x07; /* set = mechanical switch set */ + public static final int SW_VIDEOOUT_INSERT = 0x08; /* set = inserted */ + public static final int SW_CAMERA_LENS_COVER = 0x09; /* set = lens covered */ + public static final int SW_KEYPAD_SLIDE = 0x0a; /* set = keypad slide out */ + public static final int SW_FRONT_PROXIMITY = 0x0b; /* set = front proximity sensor active */ + public static final int SW_ROTATE_LOCK = 0x0c; /* set = rotate locked/disabled */ + public static final int SW_LINEIN_INSERT = 0x0d; /* set = inserted */ + public static final int SW_MUTE_DEVICE = 0x0e; /* set = device disabled */ + public static final int SW_PEN_INSERTED = 0x0f; /* set = pen inserted */ + public static final int SW_MAX = 0x0f; + public static final int SW_CNT = (SW_MAX + 1); + +/* + * Misc events + */ + + public static final int MSC_SERIAL = 0x00; + public static final int MSC_PULSELED = 0x01; + public static final int MSC_GESTURE = 0x02; + public static final int MSC_RAW = 0x03; + public static final int MSC_SCAN = 0x04; + public static final int MSC_TIMESTAMP = 0x05; + public static final int MSC_MAX = 0x07; + public static final int MSC_CNT = (MSC_MAX + 1); + +/* + * LEDs + */ + + public static final int LED_NUML = 0x00; + public static final int LED_CAPSL = 0x01; + public static final int LED_SCROLLL = 0x02; + public static final int LED_COMPOSE = 0x03; + public static final int LED_KANA = 0x04; + public static final int LED_SLEEP = 0x05; + public static final int LED_SUSPEND = 0x06; + public static final int LED_MUTE = 0x07; + public static final int LED_MISC = 0x08; + public static final int LED_MAIL = 0x09; + public static final int LED_CHARGING = 0x0a; + public static final int LED_MAX = 0x0f; + public static final int LED_CNT = (LED_MAX + 1); + +/* + * Autorepeat values + */ + + public static final int REP_DELAY = 0x00; + public static final int REP_PERIOD = 0x01; + public static final int REP_MAX = 0x01; + public static final int REP_CNT = (REP_MAX + 1); + +/* + * Sounds + */ + + public static final int SND_CLICK = 0x00; + public static final int SND_BELL = 0x01; + public static final int SND_TONE = 0x02; + public static final int SND_MAX = 0x07; + public static final int SND_CNT = (SND_MAX + 1); +} diff --git a/app/src/main/java/com/stardust/autojs/core/inputevent/InputEventObserver.java b/app/src/main/java/com/stardust/autojs/core/inputevent/InputEventObserver.java new file mode 100644 index 00000000..e30d4858 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/inputevent/InputEventObserver.java @@ -0,0 +1,143 @@ +package com.stardust.autojs.core.inputevent; + +import android.content.Context; +import androidx.annotation.NonNull; +import android.text.TextUtils; + +import com.stardust.autojs.core.record.inputevent.EventFormatException; +import com.stardust.autojs.core.util.Shell; + +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Created by Stardust on 2017/8/4. + */ + +public class InputEventObserver { + + public static class InputEvent { + static final Pattern PATTERN = Pattern.compile("^\\[([^\\]]*)\\]\\s+([^:]*):\\s+([^\\s]*)\\s+([^\\s]*)\\s+([^\\s]*)\\s*$"); + + static InputEvent parse(String eventStr) { + Matcher matcher = PATTERN.matcher(eventStr); + if (!matcher.matches()) { + throw new EventFormatException(eventStr); + } + double time; + try { + time = Double.parseDouble(matcher.group(1)); + } catch (NumberFormatException e) { + throw new EventFormatException(eventStr, e); + } + return new InputEvent(time, matcher.group(2), matcher.group(3), matcher.group(4), matcher.group(5)); + } + + + public double time; + public String device; + public String type; + public String code; + public String value; + + public InputEvent(double time, String device, String type, String code, String value) { + this.time = time; + this.device = device; + this.type = type; + this.code = code; + this.value = value; + } + + + @Override + public String toString() { + return "Event{" + + "time=" + time + + ", device='" + device + '\'' + + ", type='" + type + '\'' + + ", code='" + code + '\'' + + ", value='" + value + '\'' + + '}'; + } + } + + public interface InputEventListener { + void onInputEvent(@NonNull InputEvent e); + } + + private static InputEventObserver sGlobal; + private CopyOnWriteArrayList mInputEventListeners = new CopyOnWriteArrayList<>(); + private Context mContext; + private Shell mShell; + + public InputEventObserver(Context context) { + mContext = context; + } + + public static InputEventObserver getGlobal(Context context) { + if (sGlobal == null) { + initGlobal(context); + } + return sGlobal; + } + + private static void initGlobal(Context context) { + if (sGlobal != null) + return; + sGlobal = new InputEventObserver(context); + sGlobal.observe(); + } + + public void observe() { + if (mShell != null) + throw new IllegalStateException("observe() should be called only once"); + mShell = new Shell(mContext, true); + mShell.setCallback(new Shell.SimpleCallback() { + @Override + public void onNewLine(String str) { + if (mShell.isInitialized()) { + onInputEvent(str); + } + } + + @Override + public void onInitialized() { + mShell.exec("getevent -t"); + } + + }); + } + + public void onInputEvent(String eventStr) { + if (TextUtils.isEmpty(eventStr) || !eventStr.startsWith("[")) + return; + try { + InputEvent event = InputEvent.parse(eventStr); + dispatchInputEvent(event); + } catch (Exception ignored) { + + } + } + + private void dispatchInputEvent(InputEvent event) { + for (InputEventListener listener : mInputEventListeners) { + listener.onInputEvent(event); + } + } + + public void addListener(InputEventListener listener) { + mInputEventListeners.add(listener); + } + + public boolean removeListener(InputEventListener listener) { + return mInputEventListeners.remove(listener); + } + + + public void recycle() { + mShell.exit(); + } + + +} diff --git a/app/src/main/java/com/stardust/autojs/core/inputevent/KeyEventDispatcher.java b/app/src/main/java/com/stardust/autojs/core/inputevent/KeyEventDispatcher.java new file mode 100644 index 00000000..02f20504 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/inputevent/KeyEventDispatcher.java @@ -0,0 +1,54 @@ +package com.stardust.autojs.core.inputevent; + +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Created by Stardust on 2017/8/6. + */ + +public class KeyEventDispatcher { + + public interface OnKeyListener { + void onVolumeUp(); + + void onVolumeDown(); + + void onCamera(); + } + + private static KeyEventDispatcher sSingleton = new KeyEventDispatcher(); + + private CopyOnWriteArrayList mOnKeyListeners = new CopyOnWriteArrayList<>(); + + public static KeyEventDispatcher getSingleton() { + return sSingleton; + } + + public void addOnKeyListener(OnKeyListener listener) { + mOnKeyListeners.add(listener); + } + + public boolean removeOnKeyListener(OnKeyListener listener) { + return mOnKeyListeners.remove(listener); + } + + public void notifyVolumeUp() { + for (OnKeyListener listener : mOnKeyListeners) { + listener.onVolumeUp(); + } + } + + public void notifyVolumeDown() { + for (OnKeyListener listener : mOnKeyListeners) { + listener.onVolumeDown(); + } + } + + public void notifyCarema() { + for (OnKeyListener listener : mOnKeyListeners) { + listener.onCamera(); + } + } + + +} diff --git a/app/src/main/java/com/stardust/autojs/core/inputevent/RootAutomator.java b/app/src/main/java/com/stardust/autojs/core/inputevent/RootAutomator.java new file mode 100644 index 00000000..28aa16db --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/inputevent/RootAutomator.java @@ -0,0 +1,308 @@ +package com.stardust.autojs.core.inputevent; + +import android.content.Context; +import android.os.SystemClock; +import androidx.annotation.Nullable; +import android.util.Log; +import android.util.SparseIntArray; +import android.view.ViewConfiguration; + +import com.stardust.autojs.core.util.Shell; +import com.stardust.autojs.engine.RootAutomatorEngine; +import com.stardust.autojs.runtime.exception.ScriptInterruptedException; +import com.stardust.util.ScreenMetrics; + +import java.io.IOException; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicInteger; + +import static com.stardust.autojs.core.inputevent.InputEventCodes.*; + +/** + * Created by Stardust on 2017/7/16. + */ + +public class RootAutomator implements Shell.Callback { + + private static final String LOG_TAG = "RootAutomator"; + + public static final byte DATA_TYPE_SLEEP = 0; + public static final byte DATA_TYPE_EVENT = 1; + public static final byte DATA_TYPE_EVENT_SYNC_REPORT = 2; + public static final byte DATA_TYPE_EVENT_TOUCH_X = 3; + public static final byte DATA_TYPE_EVENT_TOUCH_Y = 4; + + @Nullable + private ScreenMetrics mScreenMetrics; + private Shell mShell; + private int mDefaultId = 0; + private AtomicInteger mTracingId = new AtomicInteger(1); + private SparseIntArray mSlotIdMap = new SparseIntArray(); + private final Object mReadyLock = new Object(); + private volatile boolean mReady = false; + private final Context mContext; + private String mInputDevice; + + public RootAutomator(Context context, String inputDevice, boolean waitForReady) throws IOException { + mContext = context; + if (inputDevice == null) { + mInputDevice = RootAutomatorEngine.getDeviceNameOrPath(mContext, InputDevices.getTouchDeviceName()); + } else { + mInputDevice = inputDevice; + } + mShell = new Shell(true); + mShell.setCallback(this); + if (waitForReady) { + waitForReady(); + } + } + + + public void sendEvent(int type, int code, int value) throws IOException { + waitForReady(); + sendEventInternal(type, code, value); + } + + private void sendEventInternal(int type, int code, int value) { + mShell.exec(type + " " + code + " " + value); + } + + private void waitForReady() throws IOException { + if (mReady) { + return; + } + synchronized (mReadyLock) { + if (mReady) { + return; + } + try { + mReadyLock.wait(); + } catch (InterruptedException e) { + exit(); + throw new ScriptInterruptedException(); + } + } + } + + public void touch(int x, int y) throws IOException { + touchX(x); + touchY(y); + } + + public void setScreenMetrics(int width, int height) { + if (mScreenMetrics == null) { + mScreenMetrics = new ScreenMetrics(); + } + mScreenMetrics.setScreenMetrics(width, height); + } + + public void touchX(int x) throws IOException { + sendEvent(3, 53, scaleX(x)); + } + + private int scaleX(int x) { + if (mScreenMetrics == null) + return x; + return mScreenMetrics.scaleX(x); + } + + public void touchY(int y) throws IOException { + sendEvent(3, 54, scaleY(y)); + } + + public void sendSync() throws IOException { + sendEvent(EV_SYN, SYN_REPORT, 0); + } + + public void sendMtSync() throws IOException { + sendEvent(EV_SYN, SYN_MT_REPORT, 0); + } + + private int scaleY(int y) { + if (mScreenMetrics == null) + return y; + return mScreenMetrics.scaleY(y); + + } + + public void tap(int x, int y, int id) throws IOException { + touchDown(x, y, id); + touchUp(id); + } + + public void tap(int x, int y) throws IOException { + sendEvent(x, y, mDefaultId); + } + + public void swipe(int x1, int y1, int x2, int y2, int duration, int id) throws IOException { + long now = SystemClock.uptimeMillis(); + touchDown(x1, y1, id); + long startTime = now; + long endTime = startTime + duration; + while (now < endTime) { + long elapsedTime = now - startTime; + float alpha = (float) elapsedTime / duration; + touchMove((int) lerp(x1, x2, alpha), (int) lerp(y1, y2, alpha), id); + now = SystemClock.uptimeMillis(); + } + touchUp(id); + } + + public void swipe(int x1, int y1, int x2, int y2, int duration) throws IOException { + swipe(x1, y1, x2, y2, duration, mDefaultId); + } + + public void swipe(int x1, int y1, int x2, int y2) throws IOException { + swipe(x1, y1, x2, y2, 300, mDefaultId); + } + + public void press(int x, int y, int duration, int id) throws IOException { + touchDown(x, y, id); + sleep(duration); + touchUp(id); + } + + public void press(int x, int y, int duration) throws IOException { + press(x, y, duration, getDefaultId()); + } + + public void longPress(int x, int y, int id) throws IOException { + press(x, y, ViewConfiguration.getLongPressTimeout() + 200, id); + } + + public void longPress(int x, int y) throws IOException { + press(x, y, ViewConfiguration.getLongPressTimeout() + 200, getDefaultId()); + } + + public void touchDown(int x, int y, int id) throws IOException { + if (mSlotIdMap.size() == 0) { + touchDown0(x, y, id); + return; + } + int slotId = mSlotIdMap.size(); + mSlotIdMap.put(id, slotId); + sendEvent(EV_ABS, ABS_MT_SLOT, slotId); + sendEvent(EV_ABS, ABS_MT_TRACKING_ID, mTracingId.getAndIncrement()); + sendEvent(EV_ABS, ABS_MT_POSITION_X, scaleX(x)); + sendEvent(EV_ABS, ABS_MT_POSITION_Y, scaleY(y)); + sendEvent(EV_ABS, ABS_MT_TOUCH_MAJOR, 5); + sendEvent(EV_ABS, ABS_MT_WIDTH_MAJOR, 5); + sendEvent(EV_SYN, SYN_REPORT, 0); + } + + private void touchDown0(int x, int y, int id) throws IOException { + mSlotIdMap.put(id, 0); + sendEvent(EV_ABS, ABS_MT_TRACKING_ID, mTracingId.getAndIncrement()); + sendEvent(EV_KEY, BTN_TOUCH, DOWN); + //sendEvent(EV_KEY, BTN_TOOL_FINGER, 0x00000001); + sendEvent(EV_ABS, ABS_MT_POSITION_X, scaleX(x)); + sendEvent(EV_ABS, ABS_MT_POSITION_Y, scaleY(y)); + //sendEvent(EV_ABS, ABS_MT_PRESSURE, 200); + sendEvent(EV_ABS, ABS_MT_TOUCH_MAJOR, 5); + sendEvent(EV_ABS, ABS_MT_WIDTH_MAJOR, 5); + sendEvent(EV_SYN, SYN_REPORT, 0); + } + + public void touchDown(int x, int y) throws IOException { + touchDown(x, y, mDefaultId); + } + + public void touchUp(int id) throws IOException { + int slotId; + int i = mSlotIdMap.indexOfKey(id); + if (i < 0) { + slotId = 0; + } else { + slotId = mSlotIdMap.valueAt(i); + mSlotIdMap.removeAt(i); + } + sendEvent(EV_ABS, ABS_MT_SLOT, slotId); + sendEvent(EV_ABS, ABS_MT_TRACKING_ID, 0xffffffff); + if (mSlotIdMap.size() == 0) { + sendEvent(EV_KEY, BTN_TOUCH, UP); + //sendEvent(EV_KEY, BTN_TOOL_FINGER, 0x00000000); + } + sendEvent(EV_SYN, SYN_REPORT, 0x00000000); + } + + public void touchUp() throws IOException { + touchUp(mDefaultId); + } + + public void touchMove(int x, int y, int id) throws IOException { + int slotId = mSlotIdMap.get(id, 0); + sendEvent(EV_ABS, ABS_MT_SLOT, slotId); + sendEvent(EV_ABS, ABS_MT_TOUCH_MAJOR, 5); + sendEvent(EV_ABS, ABS_MT_POSITION_X, scaleX(x)); + sendEvent(EV_ABS, ABS_MT_POSITION_Y, scaleY(y)); + sendEvent(EV_SYN, SYN_REPORT, 0x00000000); + } + + public void touchMove(int x, int y) throws IOException { + touchMove(x, y, mDefaultId); + } + + public int getDefaultId() { + return mDefaultId; + } + + public void setDefaultId(int defaultId) { + mDefaultId = defaultId; + } + + private void sleep(long duration) throws IOException { + try { + Thread.sleep(duration); + } catch (InterruptedException e) { + exit(); + throw new ScriptInterruptedException(); + } + } + + private static float lerp(float a, float b, float alpha) { + return (b - a) * alpha + a; + } + + public void exit() throws IOException { + sleep(1); + sendEventInternal(0xffff, 0xffff, 0xefefefef); + mShell.exec("exit"); + mShell.exec("exit"); + mShell.exec("exit"); + mShell.exec("exit"); + mShell.exec("exit"); + mShell.exec("exit"); + mShell.exit(); + } + + @Override + public void onOutput(String str) { + + } + + @Override + public void onNewLine(String line) { + } + + @Override + public void onInitialized() { + String path = RootAutomatorEngine.getExecutablePath(mContext); + String deviceNameOrPath = RootAutomatorEngine.getDeviceNameOrPath(mContext, InputDevices.getTouchDeviceName()); + Log.d(LOG_TAG, "deviceNameOrPath: " + deviceNameOrPath); + mShell.exec("chmod 777 " + path); + String command = String.format(Locale.getDefault(), "%s -d %s -sw %d -sh %d", path, deviceNameOrPath, + ScreenMetrics.getDeviceScreenWidth(), ScreenMetrics.getDeviceScreenHeight()); + mShell.exec(command); + synchronized (mReadyLock) { + Log.d(LOG_TAG, "notify ready"); + mReady = true; + mReadyLock.notifyAll(); + } + } + + @Override + public void onInterrupted(InterruptedException e) { + + } +} + diff --git a/app/src/main/java/com/stardust/autojs/core/inputevent/ShellKeyObserver.java b/app/src/main/java/com/stardust/autojs/core/inputevent/ShellKeyObserver.java new file mode 100644 index 00000000..707f55bb --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/inputevent/ShellKeyObserver.java @@ -0,0 +1,88 @@ +package com.stardust.autojs.core.inputevent; + +import androidx.annotation.NonNull; +import android.util.SparseArray; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +/** + * Created by Stardust on 2017/5/4. + */ + +public class ShellKeyObserver implements InputEventObserver.InputEventListener { + + public interface KeyListener { + + void onKeyDown(String keyName); + + void onKeyUp(String keyName); + + } + + private static final Map keyNameToCode = new HashMap<>(); + private static final SparseArray keyCodeToName = new SparseArray<>(); + + private KeyListener mKeyListener; + + public ShellKeyObserver() { + + } + + public void setKeyListener(KeyListener keyListener) { + mKeyListener = keyListener; + } + + @Override + public void onInputEvent(@NonNull InputEventObserver.InputEvent event) { + if (!event.type.equals("0001")) { + return; + } + if (event.value.equalsIgnoreCase("00000000")) { + notifyKeyUp(keyCodeToKeyName(Integer.parseInt(event.code, 16))); + } + if (event.value.equalsIgnoreCase("00000001")) { + notifyKeyDown(keyCodeToKeyName(Integer.parseInt(event.code, 16))); + } + } + + public static String keyCodeToKeyName(int code) { + return keyCodeToName.get(code); + } + + + public static int keyNameToCode(String name) { + Integer code = keyNameToCode.get(name); + if (code == null) + return -1; + return code; + } + + private void notifyKeyDown(String keyName) { + if (mKeyListener != null) { + mKeyListener.onKeyDown(keyName); + } + } + + private void notifyKeyUp(String keyName) { + if (mKeyListener != null) { + mKeyListener.onKeyUp(keyName); + } + } + + static { + try { + for (Field field : InputEventCodes.class.getFields()) { + if (field.getName().startsWith("KEY_")) { + int keyCode = (int) field.get(null); + keyCodeToName.put(keyCode, field.getName()); + keyNameToCode.put(field.getName(), keyCode); + } + } + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + +} diff --git a/app/src/main/java/com/stardust/autojs/core/inputevent/TouchObserver.java b/app/src/main/java/com/stardust/autojs/core/inputevent/TouchObserver.java new file mode 100644 index 00000000..95a59426 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/inputevent/TouchObserver.java @@ -0,0 +1,89 @@ +package com.stardust.autojs.core.inputevent; + +import androidx.annotation.NonNull; + +import static com.stardust.autojs.core.record.inputevent.InputEventRecorder.parseDeviceNumber; + + +/** + * Created by Stardust on 2017/7/20. + */ + +public class TouchObserver implements InputEventObserver.InputEventListener { + + + public interface OnTouchEventListener { + void onTouch(int x, int y); + } + + private int mTouchX, mTouchY; + private OnTouchEventListener mOnTouchEventListener; + private int mLastTouchX = -1, mLastTouchY = -1; + private InputEventObserver mInputEventObserver; + + public TouchObserver(InputEventObserver observer) { + mInputEventObserver = observer; + } + + public void observe() { + mInputEventObserver.addListener(this); + } + + public void stop() { + mInputEventObserver.removeListener(this); + } + + public void setOnTouchEventListener(OnTouchEventListener onTouchEventListener) { + mOnTouchEventListener = onTouchEventListener; + } + + private void onTouch(int x, int y) { + mTouchX = x; + mTouchY = y; + if (mOnTouchEventListener != null) { + mOnTouchEventListener.onTouch(x, y); + } + } + + + @Override + public void onInputEvent(@NonNull InputEventObserver.InputEvent event) { + int device = parseDeviceNumber(event.device); + int type = (int) Long.parseLong(event.type, 16); + int code = (int) Long.parseLong(event.code, 16); + int value = (int) Long.parseLong(event.value, 16); + if (type != 3) { + return; + } + if (code == 53) { + onTouchX(value); + return; + } + if (code == 54) { + onTouchY(value); + return; + } + if (mLastTouchX >= 0) { + onTouch(mLastTouchX, mTouchY); + mLastTouchX = -1; + return; + } + if (mLastTouchY >= 0) { + onTouch(mTouchX, mLastTouchY); + mLastTouchY = -1; + } + } + + private void onTouchX(int value) { + mLastTouchX = value; + } + + private void onTouchY(int value) { + if (mLastTouchX > 0) { + onTouch(mLastTouchX, value); + return; + } + mLastTouchY = value; + } + +} diff --git a/app/src/main/java/com/stardust/autojs/core/internal/Functions.java b/app/src/main/java/com/stardust/autojs/core/internal/Functions.java new file mode 100644 index 00000000..05c52382 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/internal/Functions.java @@ -0,0 +1,66 @@ +package com.stardust.autojs.core.internal; + +/** + * Created by Stardust on 2017/7/19. + */ + +public class Functions { + + @SuppressWarnings("unchecked") + public static Object call(Func func, Object[] args) { + if (func instanceof Func0) { + return ((Func0) func).call(); + } else if (func instanceof Func1) { + return ((Func1) func).call(args[0]); + } else if (func instanceof Func2) { + return ((Func2) func).call(args[0], args[1]); + } else if (func instanceof Func3) { + return ((Func3) func).call(args[0], args[1], args[2]); + } else if (func instanceof Func4) { + return ((Func4) func).call(args[0], args[1], args[2], args[3]); + } + throw new IllegalArgumentException("Unknown func: " + func); + } + + public interface Func { + + } + + public interface Func0 extends Func { + R call(); + } + + public interface Func1 extends Func { + R call(T1 t1); + } + + public interface Func2 extends Func { + R call(T1 t1, T2 t2); + } + + public interface Func3 extends Func { + R call(T1 t1, T2 t2, T3 t3); + } + + public interface Func4 extends Func { + R call(T1 t1, T2 t2, T3 t3, T4 t4); + } + + public interface VoidFunc1 extends Func { + void call(T1 t1); + } + + public interface VoidFunc2 extends Func { + void call(T1 t1, T2 t2); + } + + public interface VoidFunc3 extends Func { + void call(T1 t1, T2 t2, T3 t3); + } + + public interface VoidFunc4 extends Func { + void call(T1 t1, T2 t2, T3 t3, T4 t4); + } + + +} diff --git a/app/src/main/java/com/stardust/autojs/core/looper/LooperHelper.java b/app/src/main/java/com/stardust/autojs/core/looper/LooperHelper.java new file mode 100644 index 00000000..40e329e6 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/looper/LooperHelper.java @@ -0,0 +1,30 @@ +package com.stardust.autojs.core.looper; + +import android.os.Looper; + +import java.util.concurrent.ConcurrentHashMap; + +/** + * Created by Stardust on 2017/12/27. + */ + +public class LooperHelper { + + private static volatile ConcurrentHashMap sLoopers = new ConcurrentHashMap<>(); + + public static void prepare() { + if (Looper.myLooper() == Looper.getMainLooper()) + return; + if (Looper.myLooper() == null) + Looper.prepare(); + Looper l = Looper.myLooper(); + if (l != null) + sLoopers.put(Thread.currentThread(), l); + } + + public static void quitForThread(Thread thread) { + Looper looper = sLoopers.remove(thread); + if (looper != null && looper != Looper.getMainLooper()) + looper.quit(); + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/looper/Loopers.java b/app/src/main/java/com/stardust/autojs/core/looper/Loopers.java new file mode 100644 index 00000000..c7cd424f --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/looper/Loopers.java @@ -0,0 +1,215 @@ +package com.stardust.autojs.core.looper; + +import android.os.Handler; +import android.os.Looper; +import android.os.MessageQueue; +import android.util.Log; + +import com.stardust.autojs.rhino.AutoJsContext; +import com.stardust.autojs.runtime.ScriptRuntime; +import com.stardust.autojs.runtime.api.Threads; +import com.stardust.autojs.runtime.api.Timers; +import com.stardust.autojs.runtime.exception.ScriptInterruptedException; +import com.stardust.lang.ThreadCompat; + +import org.mozilla.javascript.Context; + +import java.util.HashSet; +import java.util.concurrent.CopyOnWriteArrayList; + +import androidx.annotation.Nullable; + +/** + * Created by Stardust on 2017/7/29. + */ + +@SuppressWarnings("ConstantConditions") +public class Loopers implements MessageQueue.IdleHandler { + + private static final String LOG_TAG = "Loopers"; + + public interface LooperQuitHandler { + boolean shouldQuit(); + } + + private static final Runnable EMPTY_RUNNABLE = () -> { + }; + + private volatile ThreadLocal waitWhenIdle = new ThreadLocal() { + @Nullable + @Override + protected Boolean initialValue() { + return Looper.myLooper() == Looper.getMainLooper(); + } + }; + private volatile ThreadLocal> waitIds = new ThreadLocal>() { + @Nullable + @Override + protected HashSet initialValue() { + return new HashSet<>(); + } + }; + private volatile ThreadLocal maxWaitId = new ThreadLocal() { + @Nullable + @Override + protected Integer initialValue() { + return 0; + } + }; + private volatile ThreadLocal> looperQuitHandlers = new ThreadLocal<>(); + private volatile Looper mServantLooper; + private Timers mTimers; + private ScriptRuntime mScriptRuntime; + private LooperQuitHandler mMainLooperQuitHandler; + private Handler mMainHandler; + private Looper mMainLooper; + private Threads mThreads; + private MessageQueue mMainMessageQueue; + + public Loopers(ScriptRuntime runtime) { + mTimers = runtime.timers; + mThreads = runtime.threads; + mScriptRuntime = runtime; + prepare(); + mMainLooper = Looper.myLooper(); + mMainHandler = new Handler(); + mMainMessageQueue = Looper.myQueue(); + } + + + public Looper getMainLooper() { + return mMainLooper; + } + + public void addLooperQuitHandler(LooperQuitHandler handler) { + CopyOnWriteArrayList handlers = looperQuitHandlers.get(); + if (handlers == null) { + handlers = new CopyOnWriteArrayList<>(); + looperQuitHandlers.set(handlers); + } + handlers.add(handler); + } + + public boolean removeLooperQuitHandler(LooperQuitHandler handler) { + CopyOnWriteArrayList handlers = looperQuitHandlers.get(); + return handlers != null && handlers.remove(handler); + } + + private boolean shouldQuitLooper() { + if (Thread.currentThread().isInterrupted()) { + return true; + } + if (mTimers.hasPendingCallbacks()) { + return false; + } + if (waitWhenIdle.get() || !waitIds.get().isEmpty()) { + return false; + } + if (((AutoJsContext) Context.getCurrentContext()).hasPendingContinuation()) { + return false; + } + CopyOnWriteArrayList handlers = looperQuitHandlers.get(); + if (handlers == null) { + return true; + } + for (LooperQuitHandler handler : handlers) { + if (!handler.shouldQuit()) { + return false; + } + } + return true; + } + + + private void initServantThread() { + new ThreadCompat(() -> { + Looper.prepare(); + final Object lock = Loopers.this; + mServantLooper = Looper.myLooper(); + synchronized (lock) { + lock.notifyAll(); + } + Looper.loop(); + }).start(); + } + + public Looper getServantLooper() { + if (mServantLooper == null) { + initServantThread(); + synchronized (this) { + try { + this.wait(); + } catch (InterruptedException e) { + throw new ScriptInterruptedException(); + } + } + } + return mServantLooper; + } + + private void quitServantLooper() { + if (mServantLooper == null) + return; + mServantLooper.quit(); + } + + public int waitWhenIdle() { + int id = maxWaitId.get(); + Log.d(LOG_TAG, "waitWhenIdle: " + id); + maxWaitId.set(id + 1); + waitIds.get().add(id); + return id; + } + + public void doNotWaitWhenIdle(int waitId) { + Log.d(LOG_TAG, "doNotWaitWhenIdle: " + waitId); + waitIds.get().remove(waitId); + } + + public void waitWhenIdle(boolean b) { + waitWhenIdle.set(b); + } + + public void recycle() { + quitServantLooper(); + mMainMessageQueue.removeIdleHandler(this); + } + + public void setMainLooperQuitHandler(LooperQuitHandler mainLooperQuitHandler) { + mMainLooperQuitHandler = mainLooperQuitHandler; + } + + @Override + public boolean queueIdle() { + Looper l = Looper.myLooper(); + if (l == null) + return true; + if (l == mMainLooper) { + Log.d(LOG_TAG, "main looper queueIdle"); + if (shouldQuitLooper() && !mThreads.hasRunningThreads() && + mMainLooperQuitHandler != null && mMainLooperQuitHandler.shouldQuit()) { + Log.d(LOG_TAG, "main looper quit"); + l.quit(); + } + } else { + Log.d(LOG_TAG, "looper queueIdle: " + l); + if (shouldQuitLooper()) { + l.quit(); + } + } + return true; + } + + public void prepare() { + if (Looper.myLooper() == null) + LooperHelper.prepare(); + Looper.myQueue().addIdleHandler(this); + } + + public void notifyThreadExit(TimerThread thread) { + Log.d(LOG_TAG, "notifyThreadExit: " + thread); + //当子线程退成时,主线程需要检查自身是否退出(主线程在所有子线程执行完成后才能退出,如果主线程已经执行完任务仍然要等待所有子线程), + //此时通过向主线程发送一个空的Runnable,主线程执行完这个Runnable后会触发IdleHandler,从而检查自身是否退出 + mMainHandler.post(EMPTY_RUNNABLE); + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/looper/MainThreadProxy.java b/app/src/main/java/com/stardust/autojs/core/looper/MainThreadProxy.java new file mode 100644 index 00000000..eee76c62 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/looper/MainThreadProxy.java @@ -0,0 +1,224 @@ +package com.stardust.autojs.core.looper; + +import com.stardust.autojs.runtime.ScriptRuntime; +import com.stardust.lang.ThreadCompat; + +import java.util.Map; + +/** + * Created by Stardust on 2017/12/28. + */ + +public class MainThreadProxy { + + private final Thread mThread; + private ScriptRuntime mRuntime; + + public MainThreadProxy(Thread thread, ScriptRuntime runtime) { + mThread = thread; + mRuntime = runtime; + } + + public int setTimeout(Object callback, long delay, Object... args) { + return getMainTimer().setTimeout(callback, delay, args); + } + + private Timer getMainTimer() { + return mRuntime.timers.getMainTimer(); + } + + public boolean clearTimeout(int id) { + return getMainTimer().clearTimeout(id); + } + + public int setInterval(Object listener, long interval, Object... args) { + return getMainTimer().setInterval(listener, interval, args); + } + + public boolean clearInterval(int id) { + return getMainTimer().clearInterval(id); + } + + public int setImmediate(Object listener, Object... args) { + return getMainTimer().setImmediate(listener, args); + } + + public boolean clearImmediate(int id) { + return getMainTimer().clearImmediate(id); + } + + public static Thread currentThread() { + return Thread.currentThread(); + } + + public static void yield() { + Thread.yield(); + } + + public static void sleep(long millis) throws InterruptedException { + Thread.sleep(millis); + } + + public static void sleep(long millis, int nanos) throws InterruptedException { + Thread.sleep(millis, nanos); + } + + public void start() { + mThread.start(); + } + + public void run() { + mThread.run(); + } + + @Deprecated + public void stop() { + mThread.stop(); + } + + @Deprecated + public void stop(Throwable obj) { + mThread.stop(obj); + } + + public void interrupt() { + mThread.interrupt(); + } + + public static boolean interrupted() { + return ThreadCompat.interrupted(); + } + + public boolean isInterrupted() { + return mThread.isInterrupted(); + } + + @Deprecated + public void destroy() { + mThread.destroy(); + } + + public boolean isAlive() { + return mThread.isAlive(); + } + + @Deprecated + public void suspend() { + mThread.suspend(); + } + + @Deprecated + public void resume() { + mThread.resume(); + } + + public void setPriority(int newPriority) { + mThread.setPriority(newPriority); + } + + public int getPriority() { + return mThread.getPriority(); + } + + public void setName(String name) { + mThread.setName(name); + } + + public String getName() { + return mThread.getName(); + } + + public ThreadGroup getThreadGroup() { + return mThread.getThreadGroup(); + } + + public static int activeCount() { + return Thread.activeCount(); + } + + public static int enumerate(Thread[] tarray) { + return Thread.enumerate(tarray); + } + + @Deprecated + public int countStackFrames() { + return mThread.countStackFrames(); + } + + public void join(long millis) throws InterruptedException { + mThread.join(millis); + } + + public void join(long millis, int nanos) throws InterruptedException { + mThread.join(millis, nanos); + } + + public void join() throws InterruptedException { + mThread.join(); + } + + public static void dumpStack() { + Thread.dumpStack(); + } + + public void setDaemon(boolean on) { + mThread.setDaemon(on); + } + + public boolean isDaemon() { + return mThread.isDaemon(); + } + + public void checkAccess() { + mThread.checkAccess(); + } + + public ClassLoader getContextClassLoader() { + return mThread.getContextClassLoader(); + } + + public void setContextClassLoader(ClassLoader cl) { + mThread.setContextClassLoader(cl); + } + + public static boolean holdsLock(Object obj) { + return Thread.holdsLock(obj); + } + + public StackTraceElement[] getStackTrace() { + return mThread.getStackTrace(); + } + + public static Map getAllStackTraces() { + return Thread.getAllStackTraces(); + } + + public long getId() { + return mThread.getId(); + } + + public Thread.State getState() { + return mThread.getState(); + } + + @Override + public String toString() { + return mThread.toString(); + } + + public static void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh) { + Thread.setDefaultUncaughtExceptionHandler(eh); + } + + public static Thread.UncaughtExceptionHandler getDefaultUncaughtExceptionHandler() { + return Thread.getDefaultUncaughtExceptionHandler(); + } + + public Thread.UncaughtExceptionHandler getUncaughtExceptionHandler() { + return mThread.getUncaughtExceptionHandler(); + } + + public void setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh) { + mThread.setUncaughtExceptionHandler(eh); + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/looper/Timer.java b/app/src/main/java/com/stardust/autojs/core/looper/Timer.java new file mode 100644 index 00000000..03218186 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/looper/Timer.java @@ -0,0 +1,134 @@ +package com.stardust.autojs.core.looper; + +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import android.util.SparseArray; + +import com.stardust.autojs.runtime.ScriptRuntime; +import com.stardust.concurrent.VolatileBox; + +/** + * Created by Stardust on 2017/12/27. + */ + +public class Timer { + + private static final String LOG_TAG = "Timer"; + + private SparseArray mHandlerCallbacks = new SparseArray<>(); + private int mCallbackMaxId = 0; + private ScriptRuntime mRuntime; + private Handler mHandler; + private long mMaxCallbackUptimeMillis = 0; + private final VolatileBox mMaxCallbackMillisForAllThread; + + public Timer(ScriptRuntime runtime, VolatileBox maxCallbackMillisForAllThread) { + mRuntime = runtime; + mMaxCallbackMillisForAllThread = maxCallbackMillisForAllThread; + mHandler = new Handler(); + } + + public Timer(ScriptRuntime runtime, VolatileBox maxCallbackMillisForAllThread, Looper looper) { + mRuntime = runtime; + mMaxCallbackMillisForAllThread = maxCallbackMillisForAllThread; + mHandler = new Handler(looper); + } + + public int setTimeout(final Object callback, final long delay, final Object... args) { + mCallbackMaxId++; + final int id = mCallbackMaxId; + Runnable r = () -> { + callFunction(callback, null, args); + mHandlerCallbacks.remove(id); + }; + mHandlerCallbacks.put(id, r); + postDelayed(r, delay); + return id; + } + + private void callFunction(Object callback, Object thiz, Object[] args) { + if(Looper.myLooper() == Looper.getMainLooper()){ + try { + mRuntime.bridges.callFunction(callback, thiz, args); + }catch (Exception e){ + mRuntime.exit(e); + } + }else { + mRuntime.bridges.callFunction(callback, thiz, args); + } + } + + public boolean clearTimeout(int id) { + return clearCallback(id); + } + + public int setInterval(final Object listener, final long interval, final Object... args) { + mCallbackMaxId++; + final int id = mCallbackMaxId; + final Runnable r = new Runnable() { + @Override + public void run() { + if (mHandlerCallbacks.get(id) == null) + return; + callFunction(listener, null, args); + postDelayed(this, interval); + } + }; + mHandlerCallbacks.put(id, r); + postDelayed(r, interval); + return id; + } + + public void postDelayed(Runnable r, long interval) { + long uptime = SystemClock.uptimeMillis() + interval; + mHandler.postAtTime(r, uptime); + mMaxCallbackUptimeMillis = Math.max(mMaxCallbackUptimeMillis, uptime); + synchronized (mMaxCallbackMillisForAllThread) { + mMaxCallbackMillisForAllThread.set(Math.max(mMaxCallbackMillisForAllThread.get(), uptime)); + } + } + + public void post(Runnable r) { + + } + + public boolean clearInterval(int id) { + return clearCallback(id); + } + + public int setImmediate(final Object listener, final Object... args) { + mCallbackMaxId++; + final int id = mCallbackMaxId; + Runnable r = () -> { + callFunction(listener, null, args); + mHandlerCallbacks.remove(id); + }; + mHandlerCallbacks.put(id, r); + postDelayed(r, 0); + return id; + } + + public boolean clearImmediate(int id) { + return clearCallback(id); + } + + private boolean clearCallback(int id) { + Runnable callback = mHandlerCallbacks.get(id); + if (callback != null) { + mHandler.removeCallbacks(callback); + mHandlerCallbacks.remove(id); + return true; + } + return false; + } + + public boolean hasPendingCallbacks() { + return mMaxCallbackUptimeMillis > SystemClock.uptimeMillis(); + } + + public void removeAllCallbacks() { + mHandler.removeCallbacksAndMessages(null); + } + +} diff --git a/app/src/main/java/com/stardust/autojs/core/looper/TimerThread.java b/app/src/main/java/com/stardust/autojs/core/looper/TimerThread.java new file mode 100644 index 00000000..61dbc7e7 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/looper/TimerThread.java @@ -0,0 +1,128 @@ +package com.stardust.autojs.core.looper; + +import android.os.Handler; +import android.os.Looper; +import androidx.annotation.CallSuper; + +import com.stardust.autojs.engine.RhinoJavaScriptEngine; +import com.stardust.autojs.runtime.ScriptRuntime; +import com.stardust.autojs.runtime.exception.ScriptInterruptedException; +import com.stardust.concurrent.VolatileBox; +import com.stardust.lang.ThreadCompat; + +import java.util.concurrent.ConcurrentHashMap; + +/** + * Created by Stardust on 2017/12/27. + */ + +public class TimerThread extends ThreadCompat { + + private static ConcurrentHashMap sTimerMap = new ConcurrentHashMap<>(); + + private Timer mTimer; + private final VolatileBox mMaxCallbackUptimeMillisForAllThreads; + private final ScriptRuntime mRuntime; + private Runnable mTarget; + private boolean mRunning = false; + private final Object mRunningLock = new Object(); + + public TimerThread(ScriptRuntime runtime, VolatileBox maxCallbackUptimeMillisForAllThreads, Runnable target) { + super(target); + mRuntime = runtime; + mTarget = target; + mMaxCallbackUptimeMillisForAllThreads = maxCallbackUptimeMillisForAllThreads; + } + + @Override + public void run() { + mRuntime.loopers.prepare(); + mTimer = new Timer(mRuntime, mMaxCallbackUptimeMillisForAllThreads); + sTimerMap.put(Thread.currentThread(), mTimer); + ((RhinoJavaScriptEngine) mRuntime.engines.myEngine()).enterContext(); + notifyRunning(); + new Handler().post(mTarget); + try { + Looper.loop(); + } catch (Throwable e) { + if (!ScriptInterruptedException.causedByInterrupted(e)) { + mRuntime.console.error(Thread.currentThread().toString() + ": ", e); + } + } finally { + onExit(); + mTimer = null; + org.mozilla.javascript.Context.exit(); + sTimerMap.remove(Thread.currentThread(), mTimer); + } + } + + @Override + public void interrupt() { + LooperHelper.quitForThread(this); + super.interrupt(); + } + + private void notifyRunning() { + synchronized (mRunningLock) { + mRunning = true; + mRunningLock.notifyAll(); + } + } + + @CallSuper + protected void onExit() { + mRuntime.loopers.notifyThreadExit(this); + } + + public static Timer getTimerForThread(Thread thread) { + return sTimerMap.get(thread); + } + + public static Timer getTimerForCurrentThread() { + return getTimerForThread(Thread.currentThread()); + } + + public int setTimeout(Object callback, long delay, Object... args) { + return getTimer().setTimeout(callback, delay, args); + } + + public Timer getTimer() { + if (mTimer == null) { + throw new IllegalStateException("thread is not alive"); + } + return mTimer; + } + + public boolean clearTimeout(int id) { + return getTimer().clearTimeout(id); + } + + public int setInterval(Object listener, long interval, Object... args) { + return getTimer().setInterval(listener, interval, args); + } + + public boolean clearInterval(int id) { + return getTimer().clearInterval(id); + } + + public int setImmediate(Object listener, Object... args) { + return getTimer().setImmediate(listener, args); + } + + public boolean clearImmediate(int id) { + return getTimer().clearImmediate(id); + } + + public void waitFor() throws InterruptedException { + synchronized (mRunningLock) { + if (mRunning) + return; + mRunningLock.wait(); + } + } + + @Override + public String toString() { + return "Thread[" + getName() + "," + getPriority() + "]"; + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/opencv/Mat.java b/app/src/main/java/com/stardust/autojs/core/opencv/Mat.java new file mode 100644 index 00000000..7a9e74be --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/opencv/Mat.java @@ -0,0 +1,110 @@ +package com.stardust.autojs.core.opencv; + +import com.stardust.util.ResourceMonitor; + +import org.mozilla.javascript.ScriptRuntime; +import org.opencv.core.Range; +import org.opencv.core.Rect; +import org.opencv.core.Scalar; +import org.opencv.core.Size; + +import java.lang.reflect.Method; +import java.util.concurrent.atomic.AtomicInteger; + +public class Mat extends org.opencv.core.Mat implements ResourceMonitor.Resource { + + private static Method nClone; + + static { + try { + nClone = org.opencv.core.Mat.class.getDeclaredMethod("n_clone", long.class); + nClone.setAccessible(true); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } + } + + private static final AtomicInteger sResourceId = new AtomicInteger(); + private volatile boolean mReleased = false; + private final int mResourceId = sResourceId.incrementAndGet(); + + public Mat(long addr) { + super(addr); + ResourceMonitor.onOpen(this); + } + + public Mat() { + super(); + ResourceMonitor.onOpen(this); + } + + public Mat(int rows, int cols, int type) { + super(rows, cols, type); + ResourceMonitor.onOpen(this); + } + + public Mat(Size size, int type) { + super(size, type); + ResourceMonitor.onOpen(this); + } + + public Mat(int rows, int cols, int type, Scalar s) { + super(rows, cols, type, s); + ResourceMonitor.onOpen(this); + } + + public Mat(Size size, int type, Scalar s) { + super(size, type, s); + ResourceMonitor.onOpen(this); + } + + public Mat(Mat m, Range rowRange, Range colRange) { + super(m, rowRange, colRange); + ResourceMonitor.onOpen(this); + } + + public Mat(Mat m, Range rowRange) { + super(m, rowRange); + ResourceMonitor.onOpen(this); + } + + public Mat(Mat m, Rect roi) { + super(m, roi); + ResourceMonitor.onOpen(this); + } + + @Override + public Mat clone() { + return new Mat(n_clone(this.nativeObj)); + } + + protected long n_clone(long addr) { + try { + return (long) nClone.invoke(this, addr); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public void release() { + super.release(); + mReleased = true; + ResourceMonitor.onClose(this); + } + + @Override + protected void finalize() throws Throwable { + if (!mReleased) { + ResourceMonitor.onFinalize(this); + super.release(); + mReleased = true; + } + super.finalize(); + } + + @Override + public int getResourceId() { + return mResourceId; + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/opencv/MatOfPoint.java b/app/src/main/java/com/stardust/autojs/core/opencv/MatOfPoint.java new file mode 100644 index 00000000..1a92051a --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/opencv/MatOfPoint.java @@ -0,0 +1,56 @@ +package com.stardust.autojs.core.opencv; + +import com.stardust.util.ResourceMonitor; + +import org.opencv.core.Mat; +import org.opencv.core.Point; + +import java.util.concurrent.atomic.AtomicInteger; + +public class MatOfPoint extends org.opencv.core.MatOfPoint implements ResourceMonitor.Resource { + + private static final AtomicInteger sResourceId = new AtomicInteger(); + private volatile boolean mReleased = false; + private final int mResourceId = sResourceId.incrementAndGet(); + + public MatOfPoint() { + super(); + ResourceMonitor.onOpen(this); + } + + public MatOfPoint(long addr) { + super(addr); + ResourceMonitor.onOpen(this); + } + + public MatOfPoint(Mat m) { + super(m); + ResourceMonitor.onOpen(this); + } + + public MatOfPoint(Point... a) { + super(a); + ResourceMonitor.onOpen(this); + } + + @Override + public void release() { + super.release(); + mReleased = true; + ResourceMonitor.onClose(this); + } + + @Override + protected void finalize() throws Throwable { + if (!mReleased) { + ResourceMonitor.onFinalize(this); + super.release(); + } + super.finalize(); + } + + @Override + public int getResourceId() { + return mResourceId; + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/opencv/OpenCVHelper.java b/app/src/main/java/com/stardust/autojs/core/opencv/OpenCVHelper.java new file mode 100644 index 00000000..531091b3 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/opencv/OpenCVHelper.java @@ -0,0 +1,67 @@ +package com.stardust.autojs.core.opencv; + +import android.content.Context; + +import androidx.annotation.Nullable; + +import android.os.Looper; +import android.util.Log; + +import com.afollestad.materialdialogs.MaterialDialog; +import com.stardust.app.DialogUtils; + +import org.opencv.android.InstallCallbackInterface; +import org.opencv.android.LoaderCallbackInterface; +import org.opencv.android.OpenCVLoader; + + +/** + * Created by Stardust on 2018/4/2. + */ + +public class OpenCVHelper { + + public interface InitializeCallback { + void onInitFinish(); + } + + private static final String LOG_TAG = "OpenCVHelper"; + private static boolean sInitialized = false; + + public static MatOfPoint newMatOfPoint(Mat mat) { + return new MatOfPoint(mat); + } + + public static void release(@Nullable MatOfPoint mat) { + if (mat == null) + return; + mat.release(); + } + + public static void release(@Nullable Mat mat) { + if (mat == null) + return; + mat.release(); + } + + public synchronized static boolean isInitialized() { + return sInitialized; + } + + public synchronized static void initIfNeeded(Context context, InitializeCallback callback) { + if (sInitialized) { + callback.onInitFinish(); + return; + } + sInitialized = true; + if (Looper.getMainLooper() == Looper.myLooper()) { + new Thread(() -> { + OpenCVLoader.initDebug(); + callback.onInitFinish(); + }).start(); + } else { + OpenCVLoader.initDebug(); + callback.onInitFinish(); + } + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/permission/OnRequestPermissionsResultCallback.java b/app/src/main/java/com/stardust/autojs/core/permission/OnRequestPermissionsResultCallback.java new file mode 100644 index 00000000..7a0f386f --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/permission/OnRequestPermissionsResultCallback.java @@ -0,0 +1,8 @@ +package com.stardust.autojs.core.permission; + +import androidx.annotation.NonNull; + +public interface OnRequestPermissionsResultCallback { + + void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults); +} diff --git a/app/src/main/java/com/stardust/autojs/core/permission/PermissionRequestActivity.java b/app/src/main/java/com/stardust/autojs/core/permission/PermissionRequestActivity.java new file mode 100644 index 00000000..56b3b77c --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/permission/PermissionRequestActivity.java @@ -0,0 +1,32 @@ +package com.stardust.autojs.core.permission; + +import android.app.Activity; +import android.os.Build; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; + +public class PermissionRequestActivity extends Activity { + + public static final String EXTRA_PERMISSIONS = "permissions"; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + String[] permissions = getIntent().getStringArrayExtra(EXTRA_PERMISSIONS); + if (permissions == null || permissions.length == 0) { + finish(); + return; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + requestPermissions(permissions, Permissions.REQUEST_CODE); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + finish(); + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/permission/PermissionRequestProxyActivity.java b/app/src/main/java/com/stardust/autojs/core/permission/PermissionRequestProxyActivity.java new file mode 100644 index 00000000..d5fcce80 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/permission/PermissionRequestProxyActivity.java @@ -0,0 +1,16 @@ +package com.stardust.autojs.core.permission; + +import android.os.Build; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; + +public interface PermissionRequestProxyActivity { + + void addRequestPermissionsCallback(OnRequestPermissionsResultCallback callback); + + boolean removeRequestPermissionsCallback(OnRequestPermissionsResultCallback callback); + + @RequiresApi(Build.VERSION_CODES.M) + void requestPermissions(@NonNull String[] permissions, int requestCode); + +} diff --git a/app/src/main/java/com/stardust/autojs/core/permission/Permissions.java b/app/src/main/java/com/stardust/autojs/core/permission/Permissions.java new file mode 100644 index 00000000..dac73078 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/permission/Permissions.java @@ -0,0 +1,51 @@ +package com.stardust.autojs.core.permission; + +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import androidx.annotation.RequiresApi; + +import java.util.ArrayList; + +import static android.content.pm.PackageManager.PERMISSION_DENIED; + +public class Permissions { + + private static final String[] EMPTY_STRING_ARRAY = new String[0]; + + static final int REQUEST_CODE = 18777; + + public static String[] getPermissionsNeedToRequest(Context context, String[] permissions) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return EMPTY_STRING_ARRAY; + } + ArrayList list = new ArrayList<>(); + for (String permission : permissions) { + if (!permission.startsWith("android.permission.")) { + permission = "android.permission." + permission.toUpperCase(); + } + if (context.checkSelfPermission(permission) == PERMISSION_DENIED) { + list.add(permission); + } + } + return list.toArray(EMPTY_STRING_ARRAY); + } + + @RequiresApi(api = Build.VERSION_CODES.M) + public static void requestPermissions(PermissionRequestProxyActivity activity, String[] permissions, OnRequestPermissionsResultCallback callback) { + if (callback != null) { + activity.addRequestPermissionsCallback((code, p, grantResults) -> { + activity.removeRequestPermissionsCallback(callback); + callback.onRequestPermissionsResult(code, p, grantResults); + }); + } + activity.requestPermissions(permissions, REQUEST_CODE); + } + + @RequiresApi(api = Build.VERSION_CODES.M) + public static void requestPermissions(Context context, String[] permissions) { + context.startActivity(new Intent(context, PermissionRequestActivity.class) + .putExtra(PermissionRequestActivity.EXTRA_PERMISSIONS, permissions) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/permission/RequestPermissionCallbacks.java b/app/src/main/java/com/stardust/autojs/core/permission/RequestPermissionCallbacks.java new file mode 100644 index 00000000..cf11f92f --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/permission/RequestPermissionCallbacks.java @@ -0,0 +1,32 @@ +package com.stardust.autojs.core.permission; + +import androidx.annotation.NonNull; + + +import java.util.ArrayList; +import java.util.List; + +public class RequestPermissionCallbacks { + + private List mCallbacks = new ArrayList<>(); + + + public void addCallback(OnRequestPermissionsResultCallback callback) { + mCallbacks.add(callback); + } + + + public boolean removeCallback(OnRequestPermissionsResultCallback callback) { + return mCallbacks.remove(callback); + } + + public boolean onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (mCallbacks.size() == 0) { + return false; + } + for (OnRequestPermissionsResultCallback callback : mCallbacks) { + callback.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + return true; + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/plugin/Plugin.java b/app/src/main/java/com/stardust/autojs/core/plugin/Plugin.java new file mode 100644 index 00000000..373ce64d --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/plugin/Plugin.java @@ -0,0 +1,93 @@ +package com.stardust.autojs.core.plugin; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; + +import com.stardust.autojs.rhino.TopLevelScope; +import com.stardust.autojs.runtime.ScriptRuntime; + +import java.lang.reflect.Method; + +public class Plugin { + + public static class PluginLoadException extends RuntimeException { + public PluginLoadException(Throwable cause) { + super(cause); + } + + public PluginLoadException(String message) { + super(message); + } + } + + private static final String KEY_REGISTRY = "org.autojs.plugin.sdk.registry"; + + public static Plugin load(Context context, Context packageContext, ScriptRuntime runtime, TopLevelScope scope) { + try { + ApplicationInfo applicationInfo = packageContext.getPackageManager().getApplicationInfo(packageContext.getPackageName(), PackageManager.GET_META_DATA); + String registryClass = applicationInfo.metaData.getString(KEY_REGISTRY); + if (registryClass == null) { + throw new PluginLoadException("no registry in metadata"); + } + Class pluginClass = Class.forName(registryClass, true, packageContext.getClassLoader()); + Method loadDefault = pluginClass.getMethod("loadDefault", Context.class, Context.class, Object.class, Object.class); + return Plugin.create(loadDefault.invoke(null, context, packageContext, runtime, scope)); + } catch (PackageManager.NameNotFoundException e) { + return null; + } catch (Throwable e) { + e.printStackTrace(); + throw new PluginLoadException(e); + } + } + + private static Plugin create(Object pluginInstance) { + if (pluginInstance == null) + return null; + return new Plugin(pluginInstance); + } + + private final Object mPluginInstance; + private Method mGetVersion; + private Method mGetScriptDir; + private String mMainScriptPath; + + public Plugin(Object pluginInstance) { + mPluginInstance = pluginInstance; + findMethods(pluginInstance.getClass()); + } + + @SuppressWarnings("unchecked") + private void findMethods(Class pluginClass) { + try { + mGetVersion = pluginClass.getMethod("getVersion"); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } + try { + mGetScriptDir = pluginClass.getMethod("getAssetsScriptDir"); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } + } + + public Object unwrap() { + return mPluginInstance; + } + + public String getMainScriptPath() { + return mMainScriptPath; + } + + public void setMainScriptPath(String mainScriptPath) { + mMainScriptPath = mainScriptPath; + } + + public String getAssetsScriptDir() { + try { + return (String) mGetScriptDir.invoke(mPluginInstance); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/pref/Pref.kt b/app/src/main/java/com/stardust/autojs/core/pref/Pref.kt new file mode 100644 index 00000000..ac5bad61 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/pref/Pref.kt @@ -0,0 +1,20 @@ +@file:Suppress("DEPRECATION") + +package com.stardust.autojs.core.pref + +import android.preference.PreferenceManager +import com.stardust.app.GlobalAppContext + +object Pref { + private val preferences = PreferenceManager.getDefaultSharedPreferences(GlobalAppContext.get()) + + val isStableModeEnabled: Boolean + get() { + return preferences.getBoolean("key_stable_mode", false) + } + + val isGestureObservingEnabled: Boolean + get() { + return preferences.getBoolean("key_gesture_observing", false) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/stardust/autojs/core/record/Recorder.java b/app/src/main/java/com/stardust/autojs/core/record/Recorder.java new file mode 100644 index 00000000..b78a9a3c --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/record/Recorder.java @@ -0,0 +1,181 @@ +package com.stardust.autojs.core.record; + +import java.util.Arrays; + +/** + * Created by Stardust on 2017/3/16. + */ + +public interface Recorder { + + interface OnStateChangedListener { + + void onStart(); + + void onStop(); + + void onPause(); + + void onResume(); + + } + + class StateChangeEvent { + + private int mOldState; + private int mCurrentState; + + public StateChangeEvent(int oldState, int currentState) { + mOldState = oldState; + mCurrentState = currentState; + } + + public int getOldState() { + return mOldState; + } + + public int getCurrentState() { + return mCurrentState; + } + } + + + int STATE_NOT_START = 0; + int STATE_RECORDING = 1; + int STATE_PAUSED = 2; + int STATE_STOPPED = 3; + + void start(); + + void stop(); + + void pause(); + + void resume(); + + String getCode(); + + String getPath(); + + int getState(); + + void setOnStateChangedListener(OnStateChangedListener onStateChangedListener); + + abstract class AbstractRecorder implements Recorder { + + private static final OnStateChangedListener NO_OPERATION_LISTENER = new OnStateChangedListener() { + @Override + public void onStart() { + + } + + @Override + public void onStop() { + + } + + @Override + public void onPause() { + + } + + @Override + public void onResume() { + + } + }; + + + private OnStateChangedListener mOnStateChangedListener = NO_OPERATION_LISTENER; + + private final boolean mSync; + private int mState = STATE_NOT_START; + + public AbstractRecorder(boolean syncOfState) { + mSync = syncOfState; + } + + public AbstractRecorder() { + this(false); + } + + public void start() { + ensureIsStateOf(STATE_NOT_START); + setState(STATE_RECORDING); + startImpl(); + mOnStateChangedListener.onStart(); + } + + + private void ensureIsStateOf(int... expectedStates) { + for (int expectedState : expectedStates) { + if (mState == expectedState) + return; + } + throw new IllegalStateException("expected=" + Arrays.toString(expectedStates) + " state=" + mState); + } + + + protected abstract void startImpl(); + + public void stop() { + ensureIsStateOf(STATE_RECORDING, STATE_PAUSED); + setState(STATE_STOPPED); + stopImpl(); + mOnStateChangedListener.onStop(); + } + + protected abstract void stopImpl(); + + public void pause() { + ensureIsStateOf(STATE_RECORDING); + setState(STATE_PAUSED); + pauseImpl(); + mOnStateChangedListener.onPause(); + } + + protected synchronized void setState(int state) { + if (mSync) { + synchronized (this) { + mState = state; + } + } else { + mState = state; + } + } + + public synchronized int getState() { + if (mSync) { + synchronized (this) { + return mState; + } + } else { + return mState; + } + } + + protected void pauseImpl() { + + } + + public void resume() { + ensureIsStateOf(STATE_PAUSED); + setState(STATE_RECORDING); + resumeImpl(); + mOnStateChangedListener.onResume(); + } + + protected void resumeImpl() { + + } + + public void setOnStateChangedListener(OnStateChangedListener onStateChangedListener) { + mOnStateChangedListener = onStateChangedListener == null ? NO_OPERATION_LISTENER : onStateChangedListener; + } + + @Override + public String getPath() { + return null; + } + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/record/accessibility/AccessibilityActionConverter.java b/app/src/main/java/com/stardust/autojs/core/record/accessibility/AccessibilityActionConverter.java new file mode 100644 index 00000000..ff346ba3 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/record/accessibility/AccessibilityActionConverter.java @@ -0,0 +1,144 @@ +package com.stardust.autojs.core.record.accessibility; + +import android.accessibilityservice.AccessibilityService; +import android.os.Build; +import android.util.SparseArray; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; + +import com.stardust.automator.UiObject; +import com.stardust.automator.simple_action.FilterAction; +import com.stardust.util.SparseArrayEntries; +import com.stardust.view.accessibility.AccessibilityNodeInfoHelper; +import com.stardust.view.accessibility.NodeInfo; + +import org.greenrobot.eventbus.EventBus; + +import java.util.List; + + +/** + * Created by Stardust on 2017/2/14. + */ + +public class AccessibilityActionConverter { + + private static final SparseArray CONVERTER_MAP = new SparseArrayEntries() + .entry(AccessibilityEvent.TYPE_VIEW_CLICKED, new DoUtilSucceedConverter("click")) + .entry(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED, new DoUtilSucceedConverter("longClick")) + .entry(AccessibilityEvent.TYPE_VIEW_SCROLLED, new DoOnceConverter("//scroll???")) + .entry(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED, new SetTextEventConverter()) + .sparseArray(); + + static { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + CONVERTER_MAP.put(AccessibilityEvent.TYPE_VIEW_CONTEXT_CLICKED, new DoOnceConverter("contextClick")); + } + } + + private StringBuilder mScript = new StringBuilder(); + private boolean mFirstAction = true; + + public AccessibilityActionConverter(boolean shouldIgnoreFirstAction) { + mShouldIgnoreFirstAction = shouldIgnoreFirstAction; + } + + private boolean mShouldIgnoreFirstAction = false; + + public void record(AccessibilityService service, AccessibilityEvent event) { + EventToScriptConverter converter = CONVERTER_MAP.get(event.getEventType()); + if (converter != null) { + if (mFirstAction && mShouldIgnoreFirstAction) { + mFirstAction = false; + return; + } + converter.onAccessibilityEvent(service, event, mScript); + mScript.append("\n"); + EventBus.getDefault().post(new AccessibilityActionRecorder.AccessibilityActionRecordEvent(event)); + } + } + + public String getScript() { + return mScript.toString(); + } + + public void onResume() { + mFirstAction = true; + } + + interface EventToScriptConverter { + + void onAccessibilityEvent(AccessibilityService service, AccessibilityEvent event, StringBuilder sb); + } + + private static abstract class BoundsEventConverter implements EventToScriptConverter { + + @Override + public void onAccessibilityEvent(AccessibilityService service, AccessibilityEvent event, StringBuilder sb) { + AccessibilityNodeInfo source = event.getSource(); + if (source == null) + return; + String bounds = NodeInfo.Companion.boundsToString(AccessibilityNodeInfoHelper.INSTANCE.getBoundsInScreen(source)); + source.recycle(); + onAccessibilityEvent(event, bounds, sb); + } + + protected abstract void onAccessibilityEvent(AccessibilityEvent event, String bounds, StringBuilder sb); + + } + + private static class DoOnceConverter extends BoundsEventConverter { + + private String mActionFunction; + + DoOnceConverter(String actionFunction) { + mActionFunction = actionFunction; + } + + @Override + protected void onAccessibilityEvent(AccessibilityEvent event, String bounds, StringBuilder sb) { + sb.append(mActionFunction).append(bounds).append(";"); + } + } + + private static class DoUtilSucceedConverter extends BoundsEventConverter { + + private String mActionFunction; + + DoUtilSucceedConverter(String actionFunction) { + mActionFunction = actionFunction; + } + + @Override + protected void onAccessibilityEvent(AccessibilityEvent event, String bounds, StringBuilder sb) { + sb.append("while(!").append(mActionFunction).append(bounds).append(");"); + } + } + + private static class SetTextEventConverter implements EventToScriptConverter { + + @Override + public void onAccessibilityEvent(AccessibilityService service, AccessibilityEvent event, StringBuilder sb) { + AccessibilityNodeInfo source = event.getSource(); + if (source == null) + return; + UiObject uiObject = UiObject.Companion.createRoot(service.getRootInActiveWindow()); + List editableList = FilterAction.EditableFilter.Companion.findEditable(uiObject); + int i = findInEditableList(editableList, source); + sb.append("while(!input(").append(i).append(", \"").append(source.getText()).append("\"));"); + source.recycle(); + } + + private static int findInEditableList(List editableList, AccessibilityNodeInfo editable) { + int i = 0; + for (UiObject nodeInfo : editableList) { + if (AccessibilityNodeInfoHelper.INSTANCE.getBoundsInScreen(nodeInfo).equals(AccessibilityNodeInfoHelper.INSTANCE.getBoundsInScreen(editable))) { + return i; + } + i++; + } + return -1; + } + } + +} diff --git a/app/src/main/java/com/stardust/autojs/core/record/accessibility/AccessibilityActionRecorder.java b/app/src/main/java/com/stardust/autojs/core/record/accessibility/AccessibilityActionRecorder.java new file mode 100644 index 00000000..b5eba3a5 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/record/accessibility/AccessibilityActionRecorder.java @@ -0,0 +1,92 @@ +package com.stardust.autojs.core.record.accessibility; + +import android.accessibilityservice.AccessibilityService; +import android.view.accessibility.AccessibilityEvent; + +import com.stardust.autojs.core.record.Recorder; +import com.stardust.view.accessibility.AccessibilityDelegate; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + + +/** + * Created by Stardust on 2017/2/14. + */ + +public class AccessibilityActionRecorder extends Recorder.AbstractRecorder implements AccessibilityDelegate { + + public static class AccessibilityActionRecordEvent { + + private final AccessibilityEvent mAccessibilityEvent; + + public AccessibilityActionRecordEvent(AccessibilityEvent event) { + mAccessibilityEvent = event; + } + + public AccessibilityEvent getAccessibilityEvent() { + return mAccessibilityEvent; + } + } + + private static final Set EVENT_TYPES = new HashSet<>(Arrays.asList(AccessibilityEvent.TYPE_VIEW_CLICKED, AccessibilityEvent.TYPE_VIEW_LONG_CLICKED, AccessibilityEvent.TYPE_VIEW_SCROLLED, AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED)); + + private static final long RECORD_TIME_OUT = 10 * 60 * 1000; + + private boolean mShouldIgnoreFirstAction = false; + + + private AccessibilityActionConverter mConverter; + private long mRecordStartMillis; + + public AccessibilityActionRecorder() { + super(true); + } + + @Override + protected void startImpl() { + mConverter = new AccessibilityActionConverter(mShouldIgnoreFirstAction); + mRecordStartMillis = System.currentTimeMillis(); + } + + @Override + protected void stopImpl() { + setState(STATE_NOT_START); + } + + @Override + protected void resumeImpl() { + mConverter.onResume(); + } + + @Override + public boolean onAccessibilityEvent(AccessibilityService service, AccessibilityEvent event) { + if (getState() == STATE_RECORDING) { + mConverter.record(service, event); + checkTimeOut(); + } + return false; + } + + @Override + public Set getEventTypes() { + return EVENT_TYPES; + } + + private void checkTimeOut() { + if (System.currentTimeMillis() - mRecordStartMillis > RECORD_TIME_OUT) { + stop(); + } + } + + @Override + public String getCode() { + return mConverter.getScript(); + } + + public void setShouldIgnoreFirstAction(boolean shouldIgnoreFirstAction) { + mShouldIgnoreFirstAction = shouldIgnoreFirstAction; + } + +} diff --git a/app/src/main/java/com/stardust/autojs/core/record/inputevent/EventFormatException.java b/app/src/main/java/com/stardust/autojs/core/record/inputevent/EventFormatException.java new file mode 100644 index 00000000..e3998940 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/record/inputevent/EventFormatException.java @@ -0,0 +1,25 @@ +package com.stardust.autojs.core.record.inputevent; + +/** + * Created by Stardust on 2017/3/7. + */ + +public class EventFormatException extends RuntimeException { + + + public static EventFormatException forEventStr(String eventStr, NumberFormatException e) { + return new EventFormatException(eventStr, e); + } + + public EventFormatException(Exception e) { + super(e); + } + + public EventFormatException(String eventStr, Exception e) { + super("eventStr=" + eventStr, e); + } + + public EventFormatException(String eventStr) { + super("eventStr=" + eventStr); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/stardust/autojs/core/record/inputevent/InputEventRecorder.java b/app/src/main/java/com/stardust/autojs/core/record/inputevent/InputEventRecorder.java new file mode 100644 index 00000000..b93bb4e2 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/record/inputevent/InputEventRecorder.java @@ -0,0 +1,60 @@ +package com.stardust.autojs.core.record.inputevent; + +import androidx.annotation.NonNull; + +import com.stardust.autojs.core.inputevent.InputEventObserver; +import com.stardust.autojs.core.record.Recorder; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Created by Stardust on 2017/3/7. + */ + +public abstract class InputEventRecorder extends Recorder.AbstractRecorder implements InputEventObserver.InputEventListener { + + + private final static Pattern LAST_INT_PATTERN = Pattern.compile("[^0-9]+([0-9]+)$"); + + + protected boolean mRecording = false; + + protected void startImpl() { + mRecording = true; + } + + protected void resumeImpl() { + mRecording = true; + } + + protected void pauseImpl() { + mRecording = false; + } + + protected void stopImpl() { + mRecording = false; + } + + public abstract String getCode(); + + public static int parseDeviceNumber(String device) { + Matcher matcher = LAST_INT_PATTERN.matcher(device); + if (matcher.find()) { + String someNumberStr = matcher.group(1); + return Integer.parseInt(someNumberStr); + } + return -1; + } + + + @Override + public void onInputEvent(@NonNull InputEventObserver.InputEvent e) { + if (!mRecording) { + return; + } + recordInputEvent(e); + } + + protected abstract void recordInputEvent(InputEventObserver.InputEvent e); +} diff --git a/app/src/main/java/com/stardust/autojs/core/record/inputevent/InputEventToAutoFileRecorder.java b/app/src/main/java/com/stardust/autojs/core/record/inputevent/InputEventToAutoFileRecorder.java new file mode 100644 index 00000000..d04c74e1 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/record/inputevent/InputEventToAutoFileRecorder.java @@ -0,0 +1,139 @@ +package com.stardust.autojs.core.record.inputevent; + +import android.content.Context; +import androidx.annotation.NonNull; +import android.util.Log; + +import com.stardust.autojs.core.inputevent.InputEventCodes; +import com.stardust.autojs.core.inputevent.InputEventObserver; +import com.stardust.autojs.core.inputevent.RootAutomator; +import com.stardust.autojs.engine.RootAutomatorEngine; +import com.stardust.pio.UncheckedIOException; +import com.stardust.util.ScreenMetrics; + +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * Created by Stardust on 2017/8/2. + */ + +public class InputEventToAutoFileRecorder extends InputEventRecorder { + + private static final String LOG_TAG = "InputEventToAutoFileRec"; + private double mLastEventTime; + private int mTouchDevice = -1; + private DataOutputStream mDataOutputStream; + private File mTmpFile; + + public InputEventToAutoFileRecorder(Context context) { + try { + mTmpFile = new File(context.getCacheDir(), SimpleDateFormat.getDateTimeInstance().format(new Date()) + ".auto"); + mTmpFile.deleteOnExit(); + mDataOutputStream = new DataOutputStream(new FileOutputStream(mTmpFile)); + writeFileHeader(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private void writeFileHeader() throws IOException { + mDataOutputStream.writeInt(0x00B87B6D); + mDataOutputStream.writeInt(RootAutomatorEngine.VERSION); + mDataOutputStream.writeInt(ScreenMetrics.getDeviceScreenWidth()); + mDataOutputStream.writeInt(ScreenMetrics.getDeviceScreenHeight()); + for (int i = 0; i < 240; i++) { + mDataOutputStream.writeByte(0); + } + } + + + @Override + public void recordInputEvent(@NonNull InputEventObserver.InputEvent event) { + try { + convertEventOrThrow(event); + Log.d(LOG_TAG, "recordInputEvent: " + event); + } catch (IOException e) { + e.printStackTrace(); + } + + } + + private void convertEventOrThrow(InputEventObserver.InputEvent event) throws IOException { + if (mLastEventTime == 0) { + mLastEventTime = event.time; + } else if (event.time - mLastEventTime > 0.001) { + writeSleep((int) (1000L * (event.time - mLastEventTime))); + mLastEventTime = event.time; + } + int device = parseDeviceNumber(event.device); + short type = (short) Long.parseLong(event.type, 16); + short code = (short) Long.parseLong(event.code, 16); + int value = (int) Long.parseLong(event.value, 16); + if (type == InputEventCodes.EV_ABS) { + if (code == InputEventCodes.ABS_MT_POSITION_X || code == InputEventCodes.ABS_MT_POSITION_Y) { + mTouchDevice = device; + RootAutomatorEngine.setTouchDevice(device); + writeTouch(code, value); + return; + } + } + if (type == InputEventCodes.EV_SYN && code == InputEventCodes.SYN_REPORT && value == 0) { + writeSyncReport(); + return; + } + if (device != mTouchDevice) { + return; + } + mDataOutputStream.writeByte(RootAutomator.DATA_TYPE_EVENT); + mDataOutputStream.writeShort(type); + mDataOutputStream.writeShort(code); + mDataOutputStream.writeInt(value); + Log.d(LOG_TAG, "write event: " + event); + } + + private void writeSleep(int millis) throws IOException { + mDataOutputStream.writeByte(RootAutomator.DATA_TYPE_SLEEP); + mDataOutputStream.writeInt(millis); + Log.d(LOG_TAG, "write sleep: " + millis); + } + + private void writeSyncReport() throws IOException { + mDataOutputStream.writeByte(RootAutomator.DATA_TYPE_EVENT_SYNC_REPORT); + Log.d(LOG_TAG, "write sync report"); + } + + private void writeTouch(short code, int value) throws IOException { + if (code == InputEventCodes.ABS_MT_POSITION_X) { + mDataOutputStream.writeByte(RootAutomator.DATA_TYPE_EVENT_TOUCH_X); + Log.d(LOG_TAG, "write touch x: " + value); + } else { + mDataOutputStream.writeByte(RootAutomator.DATA_TYPE_EVENT_TOUCH_Y); + Log.d(LOG_TAG, "write touch y: " + value); + } + mDataOutputStream.writeInt(value); + } + + public String getCode() { + return null; + } + + @Override + public String getPath() { + return mTmpFile.getAbsolutePath(); + } + + @Override + public void stop() { + super.stop(); + try { + mDataOutputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/record/inputevent/InputEventToRootAutomatorRecorder.java b/app/src/main/java/com/stardust/autojs/core/record/inputevent/InputEventToRootAutomatorRecorder.java new file mode 100644 index 00000000..ab45ae84 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/record/inputevent/InputEventToRootAutomatorRecorder.java @@ -0,0 +1,81 @@ +package com.stardust.autojs.core.record.inputevent; + +import androidx.annotation.NonNull; + +import com.stardust.autojs.core.inputevent.InputEventCodes; +import com.stardust.autojs.core.inputevent.InputEventObserver; +import com.stardust.autojs.engine.RootAutomatorEngine; + +import static com.stardust.util.ScreenMetrics.getDeviceScreenHeight; +import static com.stardust.util.ScreenMetrics.getDeviceScreenWidth; + +/** + * Created by Stardust on 2017/8/1. + */ + +public class InputEventToRootAutomatorRecorder extends InputEventRecorder { + + + private double mLastEventTime; + private StringBuilder mCode = new StringBuilder(); + private int mTouchDevice = -1; + + public InputEventToRootAutomatorRecorder() { + mCode.append("var ra = new RootAutomator();\n") + .append("ra.setScreenMetrics(").append(getDeviceScreenWidth()).append(", ") + .append(getDeviceScreenHeight()).append(");\n"); + } + + @Override + public void recordInputEvent(@NonNull InputEventObserver.InputEvent event) { + if (mLastEventTime == 0) { + mLastEventTime = event.time; + } else if (event.time - mLastEventTime > 0.001) { + mCode.append("sleep(").append((long) (1000L * (event.time - mLastEventTime))).append(");\n"); + mLastEventTime = event.time; + } + int device = parseDeviceNumber(event.device); + int type = (int) Long.parseLong(event.type, 16); + int code = (int) Long.parseLong(event.code, 16); + int value = (int) Long.parseLong(event.value, 16); + if (type == InputEventCodes.EV_ABS) { + if (code == InputEventCodes.ABS_MT_POSITION_X || code == InputEventCodes.ABS_MT_POSITION_Y) { + mTouchDevice = device; + RootAutomatorEngine.setTouchDevice(device); + onTouch(code, value); + return; + } + } + if (device != mTouchDevice) { + return; + } + if (type == InputEventCodes.EV_SYN && code == InputEventCodes.SYN_REPORT && value == 0) { + mCode.append("ra.sendSync();\n"); + return; + } + mCode.append("ra.sendEvent("); + mCode.append(type).append(", ") + .append(code).append(", ") + .append(value).append(");\n"); + } + + private void onTouch(int code, int value) { + if (code == InputEventCodes.ABS_MT_POSITION_X) { + mCode.append("ra.touchX(").append(value).append(");\n"); + } else { + mCode.append("ra.touchY(").append(value).append(");\n"); + } + } + + public String getCode() { + return mCode.toString(); + } + + @Override + public void stop() { + super.stop(); + mCode.append("ra.exit();"); + } + + +} diff --git a/app/src/main/java/com/stardust/autojs/core/record/inputevent/InputEventToSendEventJsRecorder.java b/app/src/main/java/com/stardust/autojs/core/record/inputevent/InputEventToSendEventJsRecorder.java new file mode 100644 index 00000000..e62ca55c --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/record/inputevent/InputEventToSendEventJsRecorder.java @@ -0,0 +1,109 @@ +package com.stardust.autojs.core.record.inputevent; + +import androidx.annotation.NonNull; + +import com.stardust.autojs.core.inputevent.InputEventObserver; + +import static com.stardust.util.ScreenMetrics.getDeviceScreenHeight; +import static com.stardust.util.ScreenMetrics.getDeviceScreenWidth; + +/** + * Created by Stardust on 2017/5/3. + */ + +public class InputEventToSendEventJsRecorder extends InputEventRecorder { + + private double mLastEventTime; + private StringBuilder mCode = new StringBuilder(); + private int mTouchDevice = -1; + private int mLastTouchX = -1; + private int mLastTouchY = -1; + + public InputEventToSendEventJsRecorder() { + mCode.append("var sh = new Shell(true);\n") + .append("sh.SetScreenMetrics(").append(getDeviceScreenWidth()).append(", ") + .append(getDeviceScreenHeight()).append(");\n"); + } + + + @Override + public void recordInputEvent(@NonNull InputEventObserver.InputEvent event) { + if (mLastEventTime == 0) { + mLastEventTime = event.time; + } else if (event.time - mLastEventTime > 0.03) { + mCode.append("sh.usleep(").append((long) (1000000 * (event.time - mLastEventTime))).append(");\n"); + mLastEventTime = event.time; + } + int device = parseDeviceNumber(event.device); + int type = (int) Long.parseLong(event.type, 16); + int code = (int) Long.parseLong(event.code, 16); + int value = (int) Long.parseLong(event.value, 16); + if (type == 3) { + if (code == 53) { + onTouchX(device, value); + return; + } + if (code == 54) { + onTouchY(device, value); + return; + } + } + checkLastTouch(); + mCode.append("sh.SendEvent("); + if (device != mTouchDevice) { + mCode.append(device).append(", "); + } + mCode.append(type).append(", ") + .append(code).append(", ") + .append(value).append(");\n"); + } + + private void checkLastTouch() { + if (mLastTouchX >= 0) { + mCode.append("sh.TouchX(").append(mLastTouchX).append(");\n"); + mLastTouchX = -1; + } + if (mLastTouchY >= 0) { + mCode.append("sh.TouchY(").append(mLastTouchY).append(");\n"); + mLastTouchY = -1; + } + } + + + private void onTouchX(int device, int value) { + if (mTouchDevice == -1) { + setTouchDevice(device); + } + mLastTouchX = value; + } + + private void onTouchY(int device, int value) { + if (mTouchDevice == -1) { + setTouchDevice(device); + } + if (mLastTouchX >= 0) { + mCode.append("sh.Touch(") + .append(mLastTouchX).append(", ") + .append(value).append(");\n"); + mLastTouchX = -1; + } else { + mLastTouchY = value; + } + } + + private void setTouchDevice(int i) { + mCode.append("sh.SetTouchDevice(").append(i).append(");\n"); + mTouchDevice = i; + } + + public String getCode() { + return mCode.toString(); + } + + @Override + public void stop() { + super.stop(); + mCode.append("sh.exitAndWaitFor();"); + } + +} diff --git a/app/src/main/java/com/stardust/autojs/core/record/inputevent/TouchRecorder.java b/app/src/main/java/com/stardust/autojs/core/record/inputevent/TouchRecorder.java new file mode 100644 index 00000000..9b610679 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/record/inputevent/TouchRecorder.java @@ -0,0 +1,72 @@ +package com.stardust.autojs.core.record.inputevent; + +import android.content.Context; + +import com.stardust.autojs.core.inputevent.InputEventObserver; +import com.stardust.autojs.core.record.Recorder; + +/** + * Created by Stardust on 2017/3/16. + */ + +public class TouchRecorder extends Recorder.AbstractRecorder { + + private InputEventRecorder mInputEventRecorder; + private Context mContext; + private InputEventObserver mInputEventObserver; + + public TouchRecorder(Context context, InputEventObserver observer) { + mContext = context; + mInputEventObserver = observer; + } + + public TouchRecorder(Context context) { + mContext = context; + mInputEventObserver = new InputEventObserver(context); + mInputEventObserver.observe(); + } + + @Override + protected void startImpl() { + mInputEventRecorder = createInputEventRecorder(); + mInputEventObserver.addListener(mInputEventRecorder); + mInputEventRecorder.start(); + } + + protected InputEventRecorder createInputEventRecorder() { + return new InputEventToAutoFileRecorder(mContext); + } + + @Override + protected void pauseImpl() { + super.pauseImpl(); + mInputEventRecorder.pause(); + } + + @Override + protected void resumeImpl() { + super.resumeImpl(); + mInputEventRecorder.resume(); + } + + @Override + protected void stopImpl() { + mInputEventRecorder.stop(); + mInputEventObserver.removeListener(mInputEventRecorder); + } + + @Override + public String getCode() { + return mInputEventRecorder.getCode(); + } + + + @Override + public String getPath() { + return mInputEventRecorder.getPath(); + } + + public void reset() { + setState(STATE_NOT_START); + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/storage/LocalStorage.java b/app/src/main/java/com/stardust/autojs/core/storage/LocalStorage.java new file mode 100644 index 00000000..b12b6632 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/storage/LocalStorage.java @@ -0,0 +1,75 @@ +package com.stardust.autojs.core.storage; + +import android.content.Context; +import android.content.SharedPreferences; + +/** + * Created by Stardust on 2017/12/3. + */ + +public class LocalStorage { + + private static final String NAME_PREFIX = "autojs.localstorage."; + private SharedPreferences mSharedPreferences; + + public LocalStorage(Context context, String name) { + mSharedPreferences = context.getSharedPreferences(NAME_PREFIX + name, Context.MODE_PRIVATE); + } + + public LocalStorage put(String key, String value) { + mSharedPreferences.edit() + .putString(key, value) + .apply(); + return this; + } + + public LocalStorage put(String key, long value) { + mSharedPreferences.edit() + .putLong(key, value) + .apply(); + return this; + } + + public LocalStorage put(String key, boolean value) { + mSharedPreferences.edit() + .putBoolean(key, value) + .apply(); + return this; + } + + public long getNumber(String key, long defaultValue) { + return mSharedPreferences.getLong(key, defaultValue); + } + + public boolean getBoolean(String key, boolean defaultValue) { + return mSharedPreferences.getBoolean(key, defaultValue); + } + + public String getString(String key, String defaultValue) { + return mSharedPreferences.getString(key, defaultValue); + } + + public long getNumber(String key) { + return getNumber(key, 0); + } + + public boolean getBoolean(String key) { + return getBoolean(key, false); + } + + public String getString(String key) { + return getString(key, null); + } + + public void remove(String key) { + mSharedPreferences.edit().remove(key).apply(); + } + + public boolean contains(String key) { + return mSharedPreferences.contains(key); + } + + public void clear() { + mSharedPreferences.edit().clear().apply(); + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/BaseEvent.java b/app/src/main/java/com/stardust/autojs/core/ui/BaseEvent.java new file mode 100644 index 00000000..36ab3978 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/BaseEvent.java @@ -0,0 +1,53 @@ +package com.stardust.autojs.core.ui; + +import com.stardust.autojs.rhino.NativeJavaObjectWithPrototype; + +import org.mozilla.javascript.NativeJavaObject; +import org.mozilla.javascript.ScriptRuntime; +import org.mozilla.javascript.Scriptable; + +public class BaseEvent extends NativeJavaObjectWithPrototype { + + private boolean mConsumed = false; + + public BaseEvent(Scriptable scope, Object javaObject, Class staticType) { + super(scope, javaObject, staticType); + } + + public BaseEvent(Scriptable scope, Object javaObject, Class staticType, boolean isAdapter) { + super(scope, javaObject, staticType, isAdapter); + } + + + + public BaseEvent(Scriptable scope, Object javaObject) { + super(scope, javaObject, javaObject.getClass()); + } + + + @Override + public boolean has(String name, Scriptable start) { + return super.has(name, start) || "consumed".equals(name); + } + + @Override + public Object get(String name, Scriptable start) { + if ("consumed".equals(name)) { + return mConsumed; + } + return super.get(name, start); + } + + @Override + public void put(String name, Scriptable start, Object value) { + if ("consumed".equals(name)) { + mConsumed = ScriptRuntime.toBoolean(value); + return; + } + super.put(name, start, value); + } + + public boolean isConsumed() { + return mConsumed; + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/JsViewHelper.java b/app/src/main/java/com/stardust/autojs/core/ui/JsViewHelper.java new file mode 100644 index 00000000..b58b2172 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/JsViewHelper.java @@ -0,0 +1,32 @@ +package com.stardust.autojs.core.ui; + +import androidx.annotation.Nullable; +import android.view.View; +import android.view.ViewGroup; + +import com.stardust.autojs.core.ui.inflater.util.Ids; + + +/** + * Created by Stardust on 2017/5/14. + */ + +public class JsViewHelper { + + @Nullable + public static View findViewByStringId(View view, String id) { + View result = view.findViewById(Ids.parse(id)); + if (result != null) + return result; + if (!(view instanceof ViewGroup)) { + return null; + } + ViewGroup group = (ViewGroup) view; + for (int i = 0; i < group.getChildCount(); i++) { + result = findViewByStringId(group.getChildAt(i), id); + if (result != null) + return result; + } + return null; + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/ViewExtras.java b/app/src/main/java/com/stardust/autojs/core/ui/ViewExtras.java new file mode 100644 index 00000000..89359c31 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/ViewExtras.java @@ -0,0 +1,77 @@ +package com.stardust.autojs.core.ui; + +import android.util.Log; +import android.view.View; + +import org.autojs.autojs6.R; +import com.stardust.autojs.core.ui.attribute.ViewAttributes; +import com.stardust.autojs.core.ui.attribute.ViewAttributesFactory; +import com.stardust.autojs.core.ui.inflater.ResourceParser; +import com.stardust.autojs.core.ui.nativeview.NativeView; +import com.stardust.autojs.runtime.ScriptRuntime; + +import org.mozilla.javascript.Scriptable; + +public class ViewExtras { + + private static final String LOG_TAG = "ViewExtras"; + private NativeView mNativeView; + + private ViewAttributes mViewAttributes; + + public static ViewExtras get(View view) { + ViewExtras extras; + Object tag = view.getTag(R.id.view_tag_view_extras); + Log.d(LOG_TAG, "view = " + view + ", tag = " + tag); + if (tag instanceof ViewExtras) { + extras = (ViewExtras) tag; + } else { + extras = new ViewExtras(); + view.setTag(R.id.view_tag_view_extras, extras); + } + return extras; + } + + + public static ViewAttributes getViewAttributes(View view, ResourceParser parser) { + ViewExtras extras = get(view); + ViewAttributes attributes = extras.getViewAttributes(); + if (attributes == null) { + attributes = ViewAttributesFactory.create(parser, view); + extras.setViewAttributes(attributes); + } + return attributes; + } + + + public static NativeView getNativeView(Scriptable scope, View view, Class staticType, ScriptRuntime runtime) { + ViewExtras extras = get(view); + NativeView nativeView = extras.getNativeView(); + if (nativeView == null) { + nativeView = new NativeView(scope, view, staticType, runtime); + extras.setNativeView(nativeView); + } + return nativeView; + } + + public static NativeView getNativeView(View view) { + ViewExtras extras = get(view); + return extras.getNativeView(); + } + + public final NativeView getNativeView() { + return mNativeView; + } + + public final ViewAttributes getViewAttributes() { + return mViewAttributes; + } + + public final void setNativeView(NativeView nativeView) { + mNativeView = nativeView; + } + + public final void setViewAttributes(ViewAttributes viewAttributes) { + mViewAttributes = viewAttributes; + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/attribute/AppbarAttributes.java b/app/src/main/java/com/stardust/autojs/core/ui/attribute/AppbarAttributes.java new file mode 100644 index 00000000..ffb4a1ac --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/attribute/AppbarAttributes.java @@ -0,0 +1,24 @@ +package com.stardust.autojs.core.ui.attribute; + +import android.view.View; + +import com.google.android.material.appbar.AppBarLayout; +import com.stardust.autojs.core.ui.inflater.ResourceParser; + +public class AppbarAttributes extends ViewAttributes { + public AppbarAttributes(ResourceParser resourceParser, View view) { + super(resourceParser, view); + } + + @Override + protected void onRegisterAttrs() { + super.onRegisterAttrs(); + registerPixelAttr("elevation", getView()::setTargetElevation); + registerBooleanAttr("expanded", getView()::setExpanded); + } + + @Override + public AppBarLayout getView() { + return (AppBarLayout) super.getView(); + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/attribute/CardAttributes.java b/app/src/main/java/com/stardust/autojs/core/ui/attribute/CardAttributes.java new file mode 100644 index 00000000..e8f622ca --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/attribute/CardAttributes.java @@ -0,0 +1,72 @@ +package com.stardust.autojs.core.ui.attribute; + +import android.graphics.Color; +import android.view.View; + +import com.stardust.autojs.core.ui.inflater.ResourceParser; +import com.stardust.autojs.core.ui.inflater.util.Dimensions; + +import androidx.cardview.widget.CardView; + +public class CardAttributes extends ViewAttributes { + + public CardAttributes(ResourceParser resourceParser, View view) { + super(resourceParser, view); + } + + @Override + protected void onRegisterAttrs() { + super.onRegisterAttrs(); + registerAttr("cardBackgroundColor", Color::parseColor, getView()::setCardBackgroundColor); + registerPixelAttr("cardCornerRadius", getView()::setRadius); + registerPixelAttr("cardElevation", getView()::setCardElevation); + registerPixelAttr("cardMaxElevation", getView()::setMaxCardElevation); + registerBooleanAttr("cardPreventCornerOverlap", getView()::setPreventCornerOverlap); + registerBooleanAttr("cardUseCompatPadding", getView()::setUseCompatPadding); + registerAttr("contentPadding", this::setContentPadding); + registerIntPixelAttr("contentPaddingBottom", this::setContentPaddingBottom); + registerIntPixelAttr("contentPaddingLeft", this::setContentPaddingLeft); + registerIntPixelAttr("contentPaddingTop", this::setContentPaddingTop); + registerIntPixelAttr("contentPaddingRight", this::setContentPaddingRight); + } + + private void setContentPadding(String value) { + int[] pixels = Dimensions.parseToIntPixelArray(getView(), value); + getView().setContentPadding(pixels[0], pixels[1], pixels[2], pixels[3]); + } + + + private void setContentPaddingBottom(int value) { + CardView cardView = getView(); + cardView.setContentPadding(cardView.getContentPaddingLeft(), cardView.getContentPaddingTop(), + cardView.getContentPaddingRight(), value); + } + + + private void setContentPaddingLeft(int value) { + CardView cardView = getView(); + cardView.setContentPadding(value, cardView.getContentPaddingTop(), + cardView.getContentPaddingRight(), cardView.getContentPaddingBottom()); + } + + + private void setContentPaddingTop(int value) { + CardView cardView = getView(); + cardView.setContentPadding(cardView.getContentPaddingLeft(), value, + cardView.getContentPaddingRight(), cardView.getContentPaddingBottom()); + } + + + private void setContentPaddingRight(int value) { + CardView cardView = getView(); + cardView.setContentPadding(cardView.getContentPaddingLeft(), cardView.getContentPaddingTop(), + value, cardView.getContentPaddingBottom()); + } + + + @Override + public CardView getView() { + return (CardView) super.getView(); + } + +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/attribute/FabViewAttributes.java b/app/src/main/java/com/stardust/autojs/core/ui/attribute/FabViewAttributes.java new file mode 100644 index 00000000..f015a985 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/attribute/FabViewAttributes.java @@ -0,0 +1,31 @@ +package com.stardust.autojs.core.ui.attribute; + +import android.graphics.Color; +import android.view.View; + +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.stardust.autojs.core.ui.inflater.ResourceParser; + +public class FabViewAttributes extends ImageViewAttributes { + + public FabViewAttributes(ResourceParser resourceParser, View view) { + super(resourceParser, view); + } + + @Override + protected void onRegisterAttrs() { + super.onRegisterAttrs(); + registerPixelAttr("elevation", getView()::setCompatElevation); + registerIntPixelAttr("fabCustomSize", getView()::setCustomSize); + registerIntPixelAttr("fabSize", getView()::setSize); + registerAttr("rippleColor", Color::parseColor, getView()::setRippleColor); + registerBooleanAttr("useCompatPadding", getView()::setUseCompatPadding); + } + + @Override + public FloatingActionButton getView() { + return (FloatingActionButton) super.getView(); + } + + +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/attribute/ImageViewAttributes.java b/app/src/main/java/com/stardust/autojs/core/ui/attribute/ImageViewAttributes.java new file mode 100644 index 00000000..292b1464 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/attribute/ImageViewAttributes.java @@ -0,0 +1,80 @@ +package com.stardust.autojs.core.ui.attribute; + +import android.graphics.Color; +import android.graphics.PorterDuff; + +import androidx.core.widget.ImageViewCompat; +import android.view.View; +import android.widget.ImageView; + +import com.stardust.autojs.core.ui.inflater.ResourceParser; +import com.stardust.util.BiMap; +import com.stardust.util.BiMaps; + +import static com.stardust.autojs.core.ui.inflater.inflaters.BaseViewInflater.TINT_MODES; + +public class ImageViewAttributes extends ViewAttributes { + + private static final BiMap SCALE_TYPES = BiMaps.newBuilder() + .put("center", ImageView.ScaleType.CENTER) + .put("centerCrop", ImageView.ScaleType.CENTER_CROP) + .put("centerInside", ImageView.ScaleType.CENTER_INSIDE) + .put("fitCenter", ImageView.ScaleType.FIT_CENTER) + .put("fitEnd", ImageView.ScaleType.FIT_END) + .put("fitStart", ImageView.ScaleType.FIT_START) + .put("fitXY", ImageView.ScaleType.FIT_XY) + .put("matrix", ImageView.ScaleType.MATRIX) + .build(); + + public ImageViewAttributes(ResourceParser resourceParser, View view) { + super(resourceParser, view); + } + + @Override + protected void onRegisterAttrs() { + super.onRegisterAttrs(); + registerBooleanAttr("adjustViewBounds", getView()::setAdjustViewBounds); + registerIntPixelAttr("baseline", getView()::setBaseline); + registerBooleanAttr("baselineAlignBottom", getView()::setBaselineAlignBottom); + registerBooleanAttr("cropToPadding", getView()::setCropToPadding); + registerIntPixelAttr("maxHeight", getView()::setMaxHeight); + registerIntPixelAttr("maxWidth", getView()::setMaxWidth); + registerAttr("path", value -> + getDrawables().setupWithImage(getView(), wrapAsPath(value)) + ); + registerAttr("scaleType", getView()::getScaleType, getView()::setScaleType, SCALE_TYPES); + registerAttr("src", value -> + getDrawables().setupWithImage(getView(), value) + ); + registerAttr("tint", Color::parseColor, value -> { + // FIXME: 2018/10/13 setImageTineList not working + PorterDuff.Mode mode = ImageViewCompat.getImageTintMode(getView()); + getView().setColorFilter(value, mode == null ? PorterDuff.Mode.SRC_ATOP : mode); + }); + registerAttr("tintMode", TINT_MODES::get, value -> { + ImageViewCompat.setImageTintMode(getView(), value); + }); + registerAttr("url", value -> + getDrawables().setupWithImage(getView(), wrapAsUrl(value)) + ); + } + + private String wrapAsPath(String value) { + if (!value.startsWith("file://")) { + return "file://" + value; + } + return value; + } + + private String wrapAsUrl(String value) { + if (!value.startsWith("http://") && !value.startsWith("https://")) { + return "http://" + value; + } + return value; + } + + @Override + public ImageView getView() { + return (ImageView) super.getView(); + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/attribute/TextViewAttributes.java b/app/src/main/java/com/stardust/autojs/core/ui/attribute/TextViewAttributes.java new file mode 100644 index 00000000..91768efc --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/attribute/TextViewAttributes.java @@ -0,0 +1,23 @@ +package com.stardust.autojs.core.ui.attribute; + +import android.view.View; +import android.widget.TextView; + +import com.stardust.autojs.core.ui.inflater.ResourceParser; + +public class TextViewAttributes extends ViewAttributes { + + public TextViewAttributes(ResourceParser resourceParser, View view) { + super(resourceParser, view); + } + + @Override + protected void onRegisterAttrs() { + super.onRegisterAttrs(); + } + + @Override + public TextView getView() { + return (TextView) super.getView(); + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/attribute/ViewAttributeDelegate.java b/app/src/main/java/com/stardust/autojs/core/ui/attribute/ViewAttributeDelegate.java new file mode 100644 index 00000000..f643cddf --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/attribute/ViewAttributeDelegate.java @@ -0,0 +1,21 @@ +package com.stardust.autojs.core.ui.attribute; + +import android.view.View; + +public interface ViewAttributeDelegate { + + interface ViewAttributeGetter { + String get(String name); + } + + interface ViewAttributeSetter { + void set(String name, String value); + } + + boolean has(String name); + + String get(View view, String name, ViewAttributeGetter defaultGetter); + + void set(View view, String name, String value, ViewAttributeSetter defaultSetter); + +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/attribute/ViewAttributes.java b/app/src/main/java/com/stardust/autojs/core/ui/attribute/ViewAttributes.java new file mode 100644 index 00000000..a3579668 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/attribute/ViewAttributes.java @@ -0,0 +1,571 @@ +package com.stardust.autojs.core.ui.attribute; + +import android.annotation.SuppressLint; +import android.content.res.ColorStateList; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.os.Build; +import androidx.annotation.CallSuper; +import androidx.core.view.ViewCompat; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.widget.CompoundButton; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import com.stardust.autojs.core.internal.Functions; +import com.stardust.autojs.core.ui.inflater.ResourceParser; +import com.stardust.autojs.core.ui.inflater.util.Dimensions; +import com.stardust.autojs.core.ui.inflater.util.Drawables; +import com.stardust.autojs.core.ui.inflater.util.Gravities; +import com.stardust.autojs.core.ui.inflater.util.Ids; +import com.stardust.autojs.core.ui.inflater.util.Strings; +import com.stardust.util.BiMap; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import static com.stardust.autojs.core.ui.inflater.inflaters.BaseViewInflater.DRAWABLE_CACHE_QUALITIES; +import static com.stardust.autojs.core.ui.inflater.inflaters.BaseViewInflater.IMPORTANT_FOR_ACCESSIBILITY; +import static com.stardust.autojs.core.ui.inflater.inflaters.BaseViewInflater.LAYOUT_DIRECTIONS; +import static com.stardust.autojs.core.ui.inflater.inflaters.BaseViewInflater.SCROLLBARS_STYLES; +import static com.stardust.autojs.core.ui.inflater.inflaters.BaseViewInflater.SCROLL_INDICATORS; +import static com.stardust.autojs.core.ui.inflater.inflaters.BaseViewInflater.TEXT_ALIGNMENTS; +import static com.stardust.autojs.core.ui.inflater.inflaters.BaseViewInflater.TEXT_DIRECTIONS; +import static com.stardust.autojs.core.ui.inflater.inflaters.BaseViewInflater.TINT_MODES; +import static com.stardust.autojs.core.ui.inflater.inflaters.BaseViewInflater.VISIBILITY; + +public class ViewAttributes { + + public interface Getter { + T get(); + } + + public interface Setter { + void set(T value); + } + + public interface Attribute { + String get(); + + void set(String value); + } + + + protected class BaseAttribute implements Attribute { + + private String mValue; + private final AttributeSetter mAttributeSetter; + + public BaseAttribute(AttributeSetter attributeSetter) { + mAttributeSetter = attributeSetter; + } + + @Override + public String get() { + return mValue; + } + + @Override + public void set(String value) { + mValue = value; + mAttributeSetter.set(value); + } + } + + protected interface AttributeGetter extends Getter { + } + + + protected interface AttributeSetter extends Setter { + } + + protected interface ValueConverter { + T convert(String value); + } + + protected static class MappingAttributeSetter implements AttributeSetter { + + private final ValueConverter mValueConverter; + private final Setter mSetter; + + public MappingAttributeSetter(ValueConverter valueConverter, Setter Setter) { + mValueConverter = valueConverter; + mSetter = Setter; + } + + @Override + public void set(String value) { + mSetter.set(mValueConverter.convert(value)); + } + } + + + private Map mAttributes = new HashMap<>(); + private final Drawables mDrawables; + private final View mView; + private ViewAttributeDelegate mViewAttributeDelegate; + + public ViewAttributes(ResourceParser resourceParser, View view) { + mDrawables = resourceParser.getDrawables(); + mView = view; + init(); + } + + public void setViewAttributeDelegate(ViewAttributeDelegate viewAttributeDelegate) { + mViewAttributeDelegate = viewAttributeDelegate; + } + + public Drawables getDrawables() { + return mDrawables; + } + + public View getView() { + return mView; + } + + public boolean contains(String name) { + return mAttributes.containsKey(name) || + (mViewAttributeDelegate != null && mViewAttributeDelegate.has(name)); + } + + public Attribute get(String name) { + if (mViewAttributeDelegate != null && mViewAttributeDelegate.has(name)) { + return new Attribute() { + @Override + public String get() { + return mViewAttributeDelegate.get(getView(), name, ViewAttributes.this::getAttrValue); + } + + @Override + public void set(String value) { + mViewAttributeDelegate.set(getView(), name, value, ViewAttributes.this::setAttrValue); + } + }; + } + return mAttributes.get(name); + } + + public String getAttrValue(String name) { + Attribute attribute = mAttributes.get(name); + if (attribute != null) { + return attribute.get(); + } + return null; + } + + public void setAttrValue(String name, String value) { + Attribute attribute = mAttributes.get(name); + if (attribute != null) { + attribute.set(value); + } + } + + @SuppressLint("ClickableViewAccessibility") + private void init() { + onRegisterAttrs(); + } + + + @CallSuper + protected void onRegisterAttrs() { + registerAttr("id", Ids::parse, mView::setId); + registerAttr("gravity", Gravities::parse, this::setGravity); + registerAttrs(new String[]{"width", "layout_width", "w"}, this::parseDimension, this::setWidth); + registerAttrs(new String[]{"height", "layout_height", "h"}, this::parseDimension, this::setHeight); + registerDrawableAttrs(new String[]{"bg", "background"}, mView::setBackground); + registerAttr("layout_gravity", Gravities::parse, this::setLayoutGravity); + registerAttr("layout_weight", Float::parseFloat, this::setLayoutWeight); + registerAttr("layout_margin", this::setMargin); + registerAttr("layout_marginLeft", this::parseDimension, this::setMarginLeft); + registerAttr("layout_marginRight", this::parseDimension, this::setMarginRight); + registerAttr("layout_marginTop", this::parseDimension, this::setMarginTop); + registerAttr("layout_marginBottom", this::parseDimension, this::setMarginBottom); + registerAttr("layout_marginStart", this::parseDimension, this::setMarginStart); + registerAttr("layout_marginEnd", this::parseDimension, this::setMarginEnd); + registerAttr("padding",this::setPadding); + registerAttr("paddingLeft", this::parseDimension, this::setPaddingLeft); + registerAttr("paddingRight", this::parseDimension, this::setPaddingRight); + registerAttr("paddingTop", this::parseDimension, this::setPaddingTop); + registerAttr("paddingBottom", this::parseDimension, this::setPaddingBottom); + registerAttr("paddingStart", this::parseDimension, this::setPaddingStart); + registerAttr("paddingEnd", this::parseDimension, this::setPaddingEnd); + registerAttr("alpha", Float::parseFloat, mView::setAlpha); + registerAttr("backgroundTint", Color::parseColor, this::setBackgroundTint); + registerAttr("backgroundTintMode", TINT_MODES::get, + bind(ViewCompat::setBackgroundTintMode, mView)); + registerAttr("clickable", Boolean::parseBoolean, mView::setClickable); + registerAttr("checked", Boolean::parseBoolean, this::setChecked); + registerAttr("contentDescription", this::parseString, mView::setContentDescription); + registerAttr("contextClickable", Boolean::parseBoolean, this::setContextClickable); + registerAttr("scaleX", Float::parseFloat, mView::setScaleX); + registerAttr("scaleY", Float::parseFloat, mView::setScaleY); + registerAttr("rotation", Float::parseFloat, mView::setRotation); + registerAttr("rotationX", Float::parseFloat, mView::setRotationX); + registerAttr("rotationY", Float::parseFloat, mView::setRotationY); + registerAttr("saveEnabled", Boolean::parseBoolean, mView::setSaveEnabled); + registerAttr("transformPivotX", this::parseDimensionToPixel, mView::setPivotX); + registerAttr("transformPivotY", this::parseDimensionToPixel, mView::setPivotY); + registerAttr("translationX", this::parseDimensionToPixel, mView::setTranslationX); + registerAttr("translationY", this::parseDimensionToPixel, mView::setTranslationY); + registerAttr("visibility", VISIBILITY::get, mView::setVisibility); + registerAttr("tag", this::parseString, mView::setTag); + registerAttr("soundEffectsEnabled", Boolean::parseBoolean, mView::setSoundEffectsEnabled); + registerAttr("scrollbarStyle", SCROLLBARS_STYLES::get, mView::setScrollBarStyle); + registerAttr("scrollX", this::parseDimensionToIntPixel, mView::setScrollX); + registerAttr("scrollY", this::parseDimensionToIntPixel, mView::setScrollY); + registerAttr("scrollIndicators", SCROLL_INDICATORS::get, bind(ViewCompat::setScrollIndicators, mView)); + registerAttr("scrollbarDefaultDelayBeforeFade", Integer::valueOf, mView::setScrollBarDefaultDelayBeforeFade); + registerAttr("scrollbarFadeDuration", Integer::valueOf, mView::setScrollBarFadeDuration); + registerAttr("scrollbarSize", this::parseDimensionToIntPixel, mView::setScrollBarSize); + registerAttr("textAlignment", TEXT_ALIGNMENTS::get, mView::setTextAlignment); + registerAttr("textDirection", TEXT_DIRECTIONS::get, mView::setTextDirection); + registerAttr("transitionName", this::parseString, bind(ViewCompat::setTransitionName, mView)); + registerAttr("translationZ", this::parseDimensionToPixel, bind(ViewCompat::setTranslationZ, mView)); + registerAttr("scrollbars", this::setScrollbars); + registerAttr("drawingCacheQuality", DRAWABLE_CACHE_QUALITIES::get, mView::setDrawingCacheQuality); + registerAttr("duplicateParentState", Boolean::parseBoolean, mView::setDuplicateParentStateEnabled); + registerAttr("fadeScrollbars", Boolean::valueOf, mView::setScrollbarFadingEnabled); + registerAttr("fadingEdgeLength", this::parseDimensionToIntPixel, mView::setFadingEdgeLength); + registerAttr("filterTouchesWhenObscured", Boolean::valueOf, mView::setFilterTouchesWhenObscured); + registerAttr("fitsSystemWindows", Boolean::valueOf, mView::setFitsSystemWindows); + registerAttr("focusable", Boolean::valueOf, mView::setFocusable); + registerAttr("focusableInTouchMode", Boolean::valueOf, mView::setFocusableInTouchMode); + registerAttr("hapticFeedbackEnabled", Boolean::valueOf, mView::setHapticFeedbackEnabled); + registerAttr("isScrollContainer", Boolean::valueOf, mView::setScrollContainer); + registerAttr("keepScreenOn", Boolean::valueOf, mView::setKeepScreenOn); + registerAttr("longClickable", Boolean::valueOf, mView::setLongClickable); + registerAttr("minHeight", this::parseDimensionToIntPixel, mView::setMinimumHeight); + registerAttr("minWidth", this::parseDimensionToIntPixel, mView::setMinimumWidth); + registerAttr("elevation", this::parseDimensionToIntPixel, this::setElevation); + registerAttr("forceHasOverlappingRendering", Boolean::valueOf, this::forceHasOverlappingRendering); + registerDrawableAttr("foreground", this::setForeground); + registerAttr("foregroundGravity", Gravities::parse, this::setForegroundGravity); + registerAttr("foregroundTintMode", TINT_MODES::get, this::setForegroundTintMode); + registerAttr("importantForAccessibility", IMPORTANT_FOR_ACCESSIBILITY::get, mView::setImportantForAccessibility); + registerAttr("layoutDirection", LAYOUT_DIRECTIONS::get, mView::setLayoutDirection); + } + + protected void setForegroundTintMode(PorterDuff.Mode mode) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + mView.setForegroundTintMode(mode); + } + } + + protected void setForegroundGravity(int g) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + mView.setForegroundGravity(g); + } + } + + protected void setForeground(Drawable foreground) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + mView.setForeground(foreground); + } + } + + protected void forceHasOverlappingRendering(boolean b) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + mView.forceHasOverlappingRendering(b); + } + } + + protected void setElevation(int e) { + ViewCompat.setElevation(mView, e); + } + + protected void setScrollbars(String scrollbars) { + for (String str : scrollbars.split("\\|")) { + if (str.equals("horizontal")) { + mView.setHorizontalScrollBarEnabled(true); + } else if (str.equals("vertical")) { + mView.setVerticalScrollBarEnabled(true); + } + } + } + + + protected float parseDimensionToPixel(String value) { + return Dimensions.parseToPixel(mView, value); + } + + protected int parseDimensionToIntPixel(String value) { + return Dimensions.parseToIntPixel(value, mView); + } + + + protected int parseDimension(String dim) { + switch (dim) { + case "wrap_content": + return ViewGroup.LayoutParams.WRAP_CONTENT; + case "fill_parent": + case "match_parent": + return ViewGroup.LayoutParams.MATCH_PARENT; + default: + return Dimensions.parseToPixel(dim, mView, (ViewGroup) mView.getParent(), true); + } + } + + protected void registerAttr(String name, Attribute attribute) { + mAttributes.put(name, attribute); + } + + protected void registerAttr(String name, Getter getter, Setter setter, BiMap biMap) { + mAttributes.put(name, new Attribute() { + @Override + public String get() { + return biMap.getKey(getter.get()); + } + + @Override + public void set(String value) { + V v = biMap.get(value); + setter.set(v); + } + }); + } + + protected void registerAttr(String name, AttributeGetter getter, AttributeSetter setter) { + mAttributes.put(name, new Attribute() { + @Override + public String get() { + return getter.get(); + } + + @Override + public void set(String value) { + setter.set(value); + } + }); + } + + protected void registerAttr(String name, AttributeSetter setter) { + mAttributes.put(name, new BaseAttribute(setter)); + } + + protected void registerAttr(String name, ValueConverter converter, Setter applier) { + mAttributes.put(name, new BaseAttribute(new MappingAttributeSetter<>(converter, applier))); + } + + protected void registerAttrs(String[] names, ValueConverter converter, Setter applier) { + registerAttrs(names, new MappingAttributeSetter<>(converter, applier)); + } + + protected void registerAttrs(String[] names, AttributeSetter setter) { + registerAttrs(names, new BaseAttribute(setter)); + } + + protected void registerAttrs(String[] names, Attribute attribute) { + for (String name : names) { + mAttributes.put(name, attribute); + } + } + + protected void registerDrawableAttr(String name, Setter applier) { + mAttributes.put(name, new BaseAttribute(new MappingAttributeSetter<>( + this::parseDrawable, applier))); + } + + + protected void registerDrawableAttrs(String[] names, Setter applier) { + registerAttrs(names, new BaseAttribute(new MappingAttributeSetter<>( + this::parseDrawable, applier))); + } + + protected void registerPixelAttr(String name, Setter applier) { + registerAttr(name, this::parseDimensionToPixel, applier); + } + + protected void registerIntPixelAttr(String name, Setter applier) { + registerAttr(name, this::parseDimensionToIntPixel, applier); + } + + protected void registerIdAttr(String name, Setter applier) { + registerAttr(name, Ids::parse, applier); + } + + protected void registerBooleanAttr(String name, Setter applier) { + registerAttr(name, Boolean::parseBoolean, applier); + } + + + protected Drawable parseDrawable(String value) { + return mDrawables.parse(mView, value); + } + + protected boolean setGravity(int g) { + try { + Method setGravity = mView.getClass().getMethod("setGravity", int.class); + setGravity.invoke(mView, g); + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + protected void setMargin(String margin) { + if (mView.getLayoutParams() instanceof ViewGroup.MarginLayoutParams) { + ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mView.getLayoutParams(); + int[] pixels = Dimensions.parseToIntPixelArray(getView(), margin); + params.setMargins(pixels[0], pixels[1], pixels[2], pixels[3]); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + params.setMarginStart(pixels[0]); + params.setMarginEnd(pixels[2]); + } + getView().setLayoutParams(params); + } + } + + protected void setMarginLeft(int margin) { + if (mView.getLayoutParams() instanceof ViewGroup.MarginLayoutParams) { + ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mView.getLayoutParams(); + params.leftMargin = margin; + } + } + + protected void setMarginRight(int margin) { + if (mView.getLayoutParams() instanceof ViewGroup.MarginLayoutParams) { + ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mView.getLayoutParams(); + params.rightMargin = margin; + } + } + + protected void setMarginTop(int margin) { + if (mView.getLayoutParams() instanceof ViewGroup.MarginLayoutParams) { + ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mView.getLayoutParams(); + params.topMargin = margin; + } + } + + protected void setMarginBottom(int margin) { + if (mView.getLayoutParams() instanceof ViewGroup.MarginLayoutParams) { + ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mView.getLayoutParams(); + params.bottomMargin = margin; + + + } + } + + protected void setMarginStart(int margin) { + if (mView.getLayoutParams() instanceof ViewGroup.MarginLayoutParams) { + ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mView.getLayoutParams(); + params.setMarginStart(margin); + } + } + + protected void setMarginEnd(int margin) { + if (mView.getLayoutParams() instanceof ViewGroup.MarginLayoutParams) { + ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mView.getLayoutParams(); + params.setMarginEnd(margin); + } + } + + protected void setPadding(String padding) { + int[] pixels = Dimensions.parseToIntPixelArray(getView(), padding); + mView.setPadding(pixels[0], pixels[1], pixels[2], pixels[3]); + } + + protected void setPaddingLeft(int padding) { + mView.setPadding(padding, mView.getPaddingTop(), mView.getPaddingRight(), mView.getPaddingBottom()); + } + + + protected void setPaddingRight(int padding) { + mView.setPadding(mView.getPaddingLeft(), mView.getPaddingTop(), padding, mView.getPaddingBottom()); + } + + + protected void setPaddingTop(int padding) { + mView.setPadding(mView.getPaddingLeft(), padding, mView.getPaddingRight(), mView.getPaddingBottom()); + } + + + protected void setPaddingStart(int padding) { + mView.setPaddingRelative(padding, mView.getPaddingTop(), mView.getPaddingEnd(), mView.getPaddingBottom()); + } + + + protected void setPaddingEnd(int padding) { + mView.setPaddingRelative(mView.getPaddingStart(), mView.getPaddingTop(), padding, mView.getPaddingBottom()); + } + + + protected void setPaddingBottom(int padding) { + mView.setPadding(mView.getPaddingLeft(), mView.getPaddingTop(), mView.getPaddingRight(), padding); + } + + protected void setBackgroundTint(int color) { + ViewCompat.setBackgroundTintList(mView, ColorStateList.valueOf(color)); + } + + protected void setContextClickable(boolean clickable) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + mView.setContextClickable(clickable); + } + + } + + protected void setChecked(boolean checked) { + if (mView instanceof CompoundButton) { + ((CompoundButton) mView).setChecked(checked); + } + } + + protected void setLayoutGravity(int gravity) { + ViewParent parent = mView.getParent(); + ViewGroup.LayoutParams layoutParams = mView.getLayoutParams(); + if (parent instanceof LinearLayout) { + ((LinearLayout.LayoutParams) layoutParams).gravity = gravity; + mView.setLayoutParams(layoutParams); + } else if (parent instanceof FrameLayout) { + ((FrameLayout.LayoutParams) layoutParams).gravity = gravity; + mView.setLayoutParams(layoutParams); + } else { + try { + Field field = layoutParams.getClass().getField("gravity"); + field.set(layoutParams, gravity); + mView.setLayoutParams(layoutParams); + } catch (Exception e) { + e.printStackTrace(); + //TODO throw or ? + } + } + } + + protected void setLayoutWeight(float weight) { + ViewParent parent = mView.getParent(); + ViewGroup.LayoutParams layoutParams = mView.getLayoutParams(); + if (parent instanceof LinearLayout) { + ((LinearLayout.LayoutParams) layoutParams).weight = weight; + mView.setLayoutParams(layoutParams); + } + } + + + protected void setWidth(int width) { + ViewGroup.LayoutParams layoutParams = mView.getLayoutParams(); + layoutParams.width = width; + mView.setLayoutParams(layoutParams); + } + + + protected void setHeight(int height) { + ViewGroup.LayoutParams layoutParams = mView.getLayoutParams(); + layoutParams.height = height; + mView.setLayoutParams(layoutParams); + } + + protected String parseString(String value) { + return Strings.parse(mView, value); + } + + protected static Setter bind(Functions.VoidFunc2 func2, T1 t1) { + return value -> func2.call(t1, value); + } + +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/attribute/ViewAttributesFactory.java b/app/src/main/java/com/stardust/autojs/core/ui/attribute/ViewAttributesFactory.java new file mode 100644 index 00000000..c1db5335 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/attribute/ViewAttributesFactory.java @@ -0,0 +1,47 @@ +package com.stardust.autojs.core.ui.attribute; + +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import com.google.android.material.appbar.AppBarLayout; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.stardust.autojs.core.ui.inflater.ResourceParser; + +import java.util.HashMap; + +import androidx.cardview.widget.CardView; + + +public class ViewAttributesFactory { + + interface ViewAttributesCreator { + ViewAttributes create(ResourceParser resourceParser, View view); + } + + private static HashMap, ViewAttributesCreator> sViewAttributesCreators = new HashMap<>(); + + static { + sViewAttributesCreators.put(ImageView.class, ImageViewAttributes::new); + sViewAttributesCreators.put(AppBarLayout.class, AppbarAttributes::new); + sViewAttributesCreators.put(TextView.class, TextViewAttributes::new); + sViewAttributesCreators.put(CardView.class, CardAttributes::new); + sViewAttributesCreators.put(FloatingActionButton.class, FabViewAttributes::new); + } + + public static void put(Class clazz, ViewAttributesCreator creator) { + sViewAttributesCreators.put(clazz, creator); + } + + public static ViewAttributes create(ResourceParser resourceParser, View view) { + Class viewClass = view.getClass(); + while (viewClass != null && !viewClass.equals(Object.class)) { + ViewAttributesCreator creator = sViewAttributesCreators.get(viewClass); + if (creator != null) { + return creator.create(resourceParser, view); + } + viewClass = viewClass.getSuperclass(); + } + return new ViewAttributes(resourceParser, view); + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/dialog/BlockedMaterialDialog.java b/app/src/main/java/com/stardust/autojs/core/ui/dialog/BlockedMaterialDialog.java new file mode 100644 index 00000000..fb5f5766 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/dialog/BlockedMaterialDialog.java @@ -0,0 +1,187 @@ +package com.stardust.autojs.core.ui.dialog; + +import android.app.Activity; +import android.content.Context; +import android.content.ContextWrapper; +import android.os.Build; +import android.os.Looper; + +import androidx.annotation.Nullable; + +import android.view.WindowManager; + +import com.afollestad.materialdialogs.DialogAction; +import com.afollestad.materialdialogs.MaterialDialog; +import com.afollestad.materialdialogs.Theme; +import com.stardust.autojs.rhino.continuation.Continuation; +import com.stardust.autojs.runtime.ScriptBridges; +import com.stardust.autojs.runtime.ScriptRuntime; +import com.stardust.autojs.runtime.exception.ScriptInterruptedException; +import com.stardust.concurrent.VolatileDispose; +import com.stardust.util.ArrayUtils; +import com.stardust.util.UiHandler; + +/** + * Created by Stardust on 2017/5/8. + */ + +public class BlockedMaterialDialog extends MaterialDialog { + + protected BlockedMaterialDialog(MaterialDialog.Builder builder) { + super(builder); + } + + @Override + public void show() { + if (!isActivityContext(getContext())) { + int type; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + } else { + type = WindowManager.LayoutParams.TYPE_PHONE; + } + getWindow().setType(type); + } + super.show(); + } + + private boolean isActivityContext(Context context) { + if (context == null) + return false; + if (context instanceof Activity) { + return !((Activity) context).isFinishing(); + } + if (context instanceof ContextWrapper) { + return isActivityContext(((ContextWrapper) context).getBaseContext()); + } + return false; + } + + + public static class Builder extends MaterialDialog.Builder { + + private VolatileDispose mResultBox; + private UiHandler mUiHandler; + private Object mCallback; + private ScriptBridges mScriptBridges; + private boolean mNotified = false; + + public Builder(Context context, ScriptRuntime runtime, Object callback) { + super(context); + super.theme(Theme.LIGHT); + mUiHandler = runtime.uiHandler; + mScriptBridges = runtime.bridges; + mCallback = callback; + if (Looper.getMainLooper() != Looper.myLooper()) { + mResultBox = new VolatileDispose<>(); + } + } + + public MaterialDialog.Builder input(@Nullable CharSequence hint, @Nullable CharSequence prefill, boolean allowEmptyInput) { + super.input(hint, prefill, allowEmptyInput, (dialog, input) -> setAndNotify(input.toString())); + cancelListener(dialog -> setAndNotify(null)); + return this; + } + + private void setAndNotify(Object r) { + if (mNotified) { + return; + } + mNotified = true; + if (mCallback != null) { + mScriptBridges.callFunction(mCallback, null, new Object[]{r}); + } + if (mResultBox != null) { + mResultBox.setAndNotify(r); + } + } + + private void setAndNotify(int r) { + if (mNotified) { + return; + } + mNotified = true; + if (mCallback != null) { + mScriptBridges.callFunction(mCallback, null, new int[]{r}); + } + if (mResultBox != null) { + mResultBox.setAndNotify(r); + } + } + + private void setAndNotify(boolean r) { + if (mNotified) { + return; + } + mNotified = true; + if (mCallback != null) { + mScriptBridges.callFunction(mCallback, null, new boolean[]{r}); + } + if (mResultBox != null) { + mResultBox.setAndNotify(r); + } + } + + public Builder alert() { + dismissListener(dialog -> setAndNotify(null)); + onAny((dialog, which) -> setAndNotify(null)); + return this; + } + + public Builder confirm() { + dismissListener(dialog -> setAndNotify(false)); + onAny((dialog, which) -> { + if (which == DialogAction.POSITIVE) { + setAndNotify(true); + } else { + setAndNotify(false); + } + }); + return this; + } + + public MaterialDialog.Builder itemsCallback() { + dismissListener(dialog -> setAndNotify(-1)); + super.itemsCallback((dialog, itemView, position, text) -> setAndNotify(position)); + return this; + } + + public MaterialDialog.Builder itemsCallbackMultiChoice(@Nullable Integer[] selectedIndices) { + dismissListener(dialog -> setAndNotify(new int[0])); + super.itemsCallbackMultiChoice(selectedIndices, (dialog, which, text) -> { + setAndNotify(ArrayUtils.unbox(which)); + return true; + }); + return this; + } + + public MaterialDialog.Builder itemsCallbackSingleChoice(int selectedIndex) { + dismissListener(dialog -> setAndNotify(-1)); + super.itemsCallbackSingleChoice(selectedIndex, (dialog, itemView, which, text) -> { + setAndNotify(which); + return true; + }); + return this; + } + + + public Object showAndGet() { + if (Looper.myLooper() == Looper.getMainLooper()) { + super.show(); + } else { + mUiHandler.post(Builder.super::show); + } + if (mResultBox != null) { + return mResultBox.blockedGetOrThrow(ScriptInterruptedException.class); + } else { + return null; + } + } + + @Override + public MaterialDialog build() { + return new BlockedMaterialDialog(this); + } + + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/dialog/JsDialog.java b/app/src/main/java/com/stardust/autojs/core/ui/dialog/JsDialog.java new file mode 100644 index 00000000..20f3ac03 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/dialog/JsDialog.java @@ -0,0 +1,812 @@ +package com.stardust.autojs.core.ui.dialog; + +import android.app.ActionBar; +import android.app.Activity; +import android.content.Context; +import android.content.DialogInterface; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Looper; +import android.os.Message; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.UiThread; +import androidx.recyclerview.widget.RecyclerView; + +import android.view.ActionMode; +import android.view.ContextMenu; +import android.view.KeyEvent; +import android.view.KeyboardShortcutGroup; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.SearchEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityEvent; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; + +import com.afollestad.materialdialogs.DialogAction; +import com.afollestad.materialdialogs.MaterialDialog; +import com.afollestad.materialdialogs.internal.MDButton; +import com.stardust.app.DialogUtils; +import com.stardust.autojs.core.eventloop.EventEmitter; +import com.stardust.util.UiHandler; + +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Stardust on 2018/4/17. + */ + +public class JsDialog { + + private final EventEmitter mEmitter; + private final UiHandler mUiHandler; + private final MaterialDialog mDialog; + private final JsDialogBuilder mBuilder; + + public JsDialog(JsDialogBuilder builder, EventEmitter emitter, UiHandler uiHandler) { + mBuilder = builder; + mDialog = builder.build(); + mEmitter = emitter; + mUiHandler = uiHandler; + } + + public JsDialog show() { + checkWindowType(); + if (Looper.myLooper() == Looper.getMainLooper()) { + mDialog.show(); + } else { + mUiHandler.post(mDialog::show); + } + mBuilder.onShowCalled(); + return this; + } + + private void checkWindowType() { + Context context = mDialog.getContext(); + if (!DialogUtils.isActivityContext(context)) { + Window window = mDialog.getWindow(); + if (window != null){ + int type; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + } else { + type = WindowManager.LayoutParams.TYPE_PHONE; + } + window.setType(type); + } + } + } + + private DialogAction getDialogAction(String action) { + switch (action) { + case "positive": + return DialogAction.POSITIVE; + case "negative": + return DialogAction.NEGATIVE; + case "neutral": + return DialogAction.NEUTRAL; + default: + throw new IllegalArgumentException("unknown action " + action); + } + } + + public int getProgress() { + return getCurrentProgress(); + } + + public String getActionButton(String action) { + return getActionButton(getDialogAction(action)).getText().toString(); + } + + public void setActionButton(String action, String text) { + if (Looper.myLooper() == Looper.getMainLooper()) { + setActionButton(getDialogAction(action), text); + } else { + mUiHandler.post(() -> setActionButton(getDialogAction(action), text)); + } + } + + + public MaterialDialog.Builder getBuilder() { + return mDialog.getBuilder(); + } + + public void setTypeface(TextView target, Typeface t) { + mDialog.setTypeface(target, t); + } + + @Nullable + public Object getTag() { + return mDialog.getTag(); + } + + public boolean onItemSelected(MaterialDialog dialog, View view, int position, CharSequence text, boolean longPress) { + return mDialog.onItemSelected(dialog, view, position, text, longPress); + } + + public RecyclerView getRecyclerView() { + return mDialog.getRecyclerView(); + } + + public boolean isPromptCheckBoxChecked() { + return mDialog.isPromptCheckBoxChecked(); + } + + public void setPromptCheckBoxChecked(boolean checked) { + mDialog.setPromptCheckBoxChecked(checked); + } + + public void onClick(View v) { + mDialog.onClick(v); + } + + public MDButton getActionButton(@NonNull DialogAction which) { + return mDialog.getActionButton(which); + } + + public View getView() { + return mDialog.getView(); + } + + @Nullable + public EditText getInputEditText() { + return mDialog.getInputEditText(); + } + + public TextView getTitleView() { + return mDialog.getTitleView(); + } + + public ImageView getIconView() { + return mDialog.getIconView(); + } + + @Nullable + public TextView getContentView() { + return mDialog.getContentView(); + } + + @Nullable + public View getCustomView() { + return mDialog.getCustomView(); + } + + @UiThread + public void setActionButton(@NonNull DialogAction which, CharSequence title) { + mDialog.setActionButton(which, title); + } + + public void setActionButton(DialogAction which, int titleRes) { + mDialog.setActionButton(which, titleRes); + } + + public boolean hasActionButtons() { + return mDialog.hasActionButtons(); + } + + public int numberOfActionButtons() { + return mDialog.numberOfActionButtons(); + } + + @UiThread + public void setTitle(@NonNull CharSequence newTitle) { + if (Looper.myLooper() == Looper.getMainLooper()) { + mDialog.setTitle(newTitle); + } else { + mUiHandler.post(() -> mDialog.setTitle(newTitle)); + } + } + + @UiThread + public void setTitle(int newTitleRes) { + if (Looper.myLooper() == Looper.getMainLooper()) { + mDialog.setTitle(newTitleRes); + } else { + mUiHandler.post(() -> mDialog.setTitle(newTitleRes)); + } + } + + @UiThread + public void setTitle(int newTitleRes, @Nullable Object... formatArgs) { + if (Looper.myLooper() == Looper.getMainLooper()) { + mDialog.setTitle(newTitleRes, formatArgs); + } else { + mUiHandler.post(() -> mDialog.setTitle(newTitleRes, formatArgs)); + } + } + + @UiThread + public void setIcon(int resId) { + if (Looper.myLooper() == Looper.getMainLooper()) { + mDialog.setIcon(resId); + } else { + mUiHandler.post(() -> mDialog.setIcon(resId)); + } + } + + @UiThread + public void setIcon(Drawable d) { + if (Looper.myLooper() == Looper.getMainLooper()) { + mDialog.setIcon(d); + } else { + mUiHandler.post(() -> mDialog.setIcon(d)); + } + } + + @UiThread + public void setIconAttribute(int attrId) { + if (Looper.myLooper() == Looper.getMainLooper()) { + mDialog.setIconAttribute(attrId); + } else { + mUiHandler.post(() -> mDialog.setIconAttribute(attrId)); + } + } + + @UiThread + public void setContent(CharSequence newContent) { + if (Looper.myLooper() == Looper.getMainLooper()) { + mDialog.setContent(newContent); + } else { + mUiHandler.post(() -> mDialog.setContent(newContent)); + } + } + + @UiThread + public void setContent(int newContentRes) { + if (Looper.myLooper() == Looper.getMainLooper()) { + mDialog.setContent(newContentRes); + } else { + mUiHandler.post(() -> mDialog.setContent(newContentRes)); + } + } + + @UiThread + public void setContent(int newContentRes, @Nullable Object... formatArgs) { + if (Looper.myLooper() == Looper.getMainLooper()) { + mDialog.setContent(newContentRes, formatArgs); + } else { + mUiHandler.post(() -> mDialog.setContent(newContentRes, formatArgs)); + } + } + + @Deprecated + public void setMessage(CharSequence message) { + Message msg = Message.obtain(); + msg.obj = message; + mDialog.setDismissMessage(msg); + } + + @Nullable + public ArrayList getItems() { + return mDialog.getItems(); + } + + @UiThread + public void setItems(CharSequence... items) { + if (Looper.myLooper() == Looper.getMainLooper()) { + mDialog.setItems(items); + } else { + mUiHandler.post(() -> mDialog.setItems(items)); + } + } + + @UiThread + public void notifyItemInserted(int index) { + mDialog.notifyItemInserted(index); + } + + @UiThread + public void notifyItemChanged(int index) { + mDialog.notifyItemChanged(index); + } + + @UiThread + public void notifyItemsChanged() { + mDialog.notifyItemsChanged(); + } + + public int getCurrentProgress() { + return mDialog.getCurrentProgress(); + } + + public ProgressBar getProgressBar() { + return mDialog.getProgressBar(); + } + + public void incrementProgress(int by) { + if (Looper.myLooper() == Looper.getMainLooper()) { + mDialog.incrementProgress(by); + } else { + mUiHandler.post(() -> mDialog.incrementProgress(by)); + } + } + + public void setProgress(int progress) { + if (Looper.myLooper() == Looper.getMainLooper()) { + mDialog.setProgress(progress); + } else { + mUiHandler.post(() -> mDialog.setProgress(progress)); + } + } + + public void setMaxProgress(int max) { + mDialog.setMaxProgress(max); + } + + public boolean isIndeterminateProgress() { + return mDialog.isIndeterminateProgress(); + } + + public int getMaxProgress() { + return mDialog.getMaxProgress(); + } + + public void setProgressPercentFormat(NumberFormat format) { + mDialog.setProgressPercentFormat(format); + } + + public void setProgressNumberFormat(String format) { + mDialog.setProgressNumberFormat(format); + } + + public boolean isCancelled() { + return mDialog.isCancelled(); + } + + public int getSelectedIndex() { + return mDialog.getSelectedIndex(); + } + + @Nullable + public Integer[] getSelectedIndices() { + return mDialog.getSelectedIndices(); + } + + @UiThread + public void setSelectedIndex(int index) { + mDialog.setSelectedIndex(index); + } + + @UiThread + public void setSelectedIndices(@NonNull Integer[] indices) { + mDialog.setSelectedIndices(indices); + } + + public void clearSelectedIndices() { + mDialog.clearSelectedIndices(); + } + + public void clearSelectedIndices(boolean sendCallback) { + mDialog.clearSelectedIndices(sendCallback); + } + + public void selectAllIndices() { + mDialog.selectAllIndices(); + } + + public void selectAllIndices(boolean sendCallback) { + mDialog.selectAllIndices(sendCallback); + } + + public void onShow(DialogInterface dialog) { + mDialog.onShow(dialog); + } + + public void dismiss() { + mDialog.dismiss(); + } + + public View findViewById(int id) { + return mDialog.findViewById(id); + } + + public void setOnShowListener(DialogInterface.OnShowListener listener) { + mDialog.setOnShowListener(listener); + } + + @Deprecated + public void setContentView(int layoutResID) throws IllegalAccessError { + mDialog.setContentView(layoutResID); + } + + @Deprecated + public void setContentView(@NonNull View view) throws IllegalAccessError { + mDialog.setContentView(view); + } + + @Deprecated + public void setContentView(@NonNull View view, ViewGroup.LayoutParams params) throws IllegalAccessError { + mDialog.setContentView(view, params); + } + + public Context getContext() { + return mDialog.getContext(); + } + + public ActionBar getActionBar() { + return mDialog.getActionBar(); + } + + public void setOwnerActivity(@NonNull Activity activity) { + mDialog.setOwnerActivity(activity); + } + + public Activity getOwnerActivity() { + return mDialog.getOwnerActivity(); + } + + public boolean isShowing() { + return mDialog.isShowing(); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public void create() { + mDialog.create(); + } + + public void hide() { + mDialog.hide(); + } + + public Bundle onSaveInstanceState() { + return mDialog.onSaveInstanceState(); + } + + public void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { + mDialog.onRestoreInstanceState(savedInstanceState); + } + + public Window getWindow() { + return mDialog.getWindow(); + } + + public View getCurrentFocus() { + return mDialog.getCurrentFocus(); + } + + public void addContentView(@NonNull View view, @Nullable ViewGroup.LayoutParams params) { + mDialog.addContentView(view, params); + } + + public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) { + return mDialog.onKeyDown(keyCode, event); + } + + public boolean onKeyLongPress(int keyCode, @NonNull KeyEvent event) { + return mDialog.onKeyLongPress(keyCode, event); + } + + public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) { + return mDialog.onKeyUp(keyCode, event); + } + + public boolean onKeyMultiple(int keyCode, int repeatCount, @NonNull KeyEvent event) { + return mDialog.onKeyMultiple(keyCode, repeatCount, event); + } + + public void onBackPressed() { + mDialog.onBackPressed(); + } + + public boolean onKeyShortcut(int keyCode, @NonNull KeyEvent event) { + return mDialog.onKeyShortcut(keyCode, event); + } + + public boolean onTouchEvent(@NonNull MotionEvent event) { + return mDialog.onTouchEvent(event); + } + + public boolean onTrackballEvent(@NonNull MotionEvent event) { + return mDialog.onTrackballEvent(event); + } + + public boolean onGenericMotionEvent(@NonNull MotionEvent event) { + return mDialog.onGenericMotionEvent(event); + } + + public void onWindowAttributesChanged(WindowManager.LayoutParams params) { + mDialog.onWindowAttributesChanged(params); + } + + public void onContentChanged() { + mDialog.onContentChanged(); + } + + public void onWindowFocusChanged(boolean hasFocus) { + mDialog.onWindowFocusChanged(hasFocus); + } + + public void onAttachedToWindow() { + mDialog.onAttachedToWindow(); + } + + public void onDetachedFromWindow() { + mDialog.onDetachedFromWindow(); + } + + public boolean dispatchKeyEvent(@NonNull KeyEvent event) { + return mDialog.dispatchKeyEvent(event); + } + + public boolean dispatchKeyShortcutEvent(@NonNull KeyEvent event) { + return mDialog.dispatchKeyShortcutEvent(event); + } + + public boolean dispatchTouchEvent(@NonNull MotionEvent ev) { + return mDialog.dispatchTouchEvent(ev); + } + + public boolean dispatchTrackballEvent(@NonNull MotionEvent ev) { + return mDialog.dispatchTrackballEvent(ev); + } + + public boolean dispatchGenericMotionEvent(@NonNull MotionEvent ev) { + return mDialog.dispatchGenericMotionEvent(ev); + } + + public boolean dispatchPopulateAccessibilityEvent(@NonNull AccessibilityEvent event) { + return mDialog.dispatchPopulateAccessibilityEvent(event); + } + + public View onCreatePanelView(int featureId) { + return mDialog.onCreatePanelView(featureId); + } + + public boolean onCreatePanelMenu(int featureId, @NonNull Menu menu) { + return mDialog.onCreatePanelMenu(featureId, menu); + } + + public boolean onPreparePanel(int featureId, View view, Menu menu) { + return mDialog.onPreparePanel(featureId, view, menu); + } + + public boolean onMenuOpened(int featureId, Menu menu) { + return mDialog.onMenuOpened(featureId, menu); + } + + public boolean onMenuItemSelected(int featureId, MenuItem item) { + return mDialog.onMenuItemSelected(featureId, item); + } + + public void onPanelClosed(int featureId, Menu menu) { + mDialog.onPanelClosed(featureId, menu); + } + + public boolean onCreateOptionsMenu(@NonNull Menu menu) { + return mDialog.onCreateOptionsMenu(menu); + } + + public boolean onPrepareOptionsMenu(@NonNull Menu menu) { + return mDialog.onPrepareOptionsMenu(menu); + } + + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + return mDialog.onOptionsItemSelected(item); + } + + public void onOptionsMenuClosed(@NonNull Menu menu) { + mDialog.onOptionsMenuClosed(menu); + } + + public void openOptionsMenu() { + mDialog.openOptionsMenu(); + } + + public void closeOptionsMenu() { + mDialog.closeOptionsMenu(); + } + + public void invalidateOptionsMenu() { + mDialog.invalidateOptionsMenu(); + } + + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + mDialog.onCreateContextMenu(menu, v, menuInfo); + } + + public void registerForContextMenu(@NonNull View view) { + mDialog.registerForContextMenu(view); + } + + public void unregisterForContextMenu(@NonNull View view) { + mDialog.unregisterForContextMenu(view); + } + + public void openContextMenu(@NonNull View view) { + mDialog.openContextMenu(view); + } + + public boolean onContextItemSelected(@NonNull MenuItem item) { + return mDialog.onContextItemSelected(item); + } + + public void onContextMenuClosed(@NonNull Menu menu) { + mDialog.onContextMenuClosed(menu); + } + + public boolean onSearchRequested(@NonNull SearchEvent searchEvent) { + return mDialog.onSearchRequested(searchEvent); + } + + public boolean onSearchRequested() { + return mDialog.onSearchRequested(); + } + + public SearchEvent getSearchEvent() { + return mDialog.getSearchEvent(); + } + + public ActionMode onWindowStartingActionMode(ActionMode.Callback callback) { + return mDialog.onWindowStartingActionMode(callback); + } + + public ActionMode onWindowStartingActionMode(ActionMode.Callback callback, int type) { + return mDialog.onWindowStartingActionMode(callback, type); + } + + public void onActionModeStarted(ActionMode mode) { + mDialog.onActionModeStarted(mode); + } + + public void onActionModeFinished(ActionMode mode) { + mDialog.onActionModeFinished(mode); + } + + public void takeKeyEvents(boolean get) { + mDialog.takeKeyEvents(get); + } + + public boolean requestWindowFeature(int featureId) { + return mDialog.requestWindowFeature(featureId); + } + + public void setFeatureDrawableResource(int featureId, int resId) { + mDialog.setFeatureDrawableResource(featureId, resId); + } + + public void setFeatureDrawableUri(int featureId, @Nullable Uri uri) { + mDialog.setFeatureDrawableUri(featureId, uri); + } + + public void setFeatureDrawable(int featureId, @Nullable Drawable drawable) { + mDialog.setFeatureDrawable(featureId, drawable); + } + + public void setFeatureDrawableAlpha(int featureId, int alpha) { + mDialog.setFeatureDrawableAlpha(featureId, alpha); + } + + public LayoutInflater getLayoutInflater() { + return mDialog.getLayoutInflater(); + } + + public void setCancelable(boolean flag) { + mDialog.setCancelable(flag); + } + + public void setCanceledOnTouchOutside(boolean cancel) { + mDialog.setCanceledOnTouchOutside(cancel); + } + + public void cancel() { + mDialog.cancel(); + } + + public void setOnCancelListener(@Nullable DialogInterface.OnCancelListener listener) { + mDialog.setOnCancelListener(listener); + } + + public void setCancelMessage(@Nullable Message msg) { + mDialog.setCancelMessage(msg); + } + + public void setOnDismissListener(@Nullable DialogInterface.OnDismissListener listener) { + mDialog.setOnDismissListener(listener); + } + + public void setDismissMessage(@Nullable Message msg) { + mDialog.setDismissMessage(msg); + } + + public void setVolumeControlStream(int streamType) { + mDialog.setVolumeControlStream(streamType); + } + + public int getVolumeControlStream() { + return mDialog.getVolumeControlStream(); + } + + public void setOnKeyListener(@Nullable DialogInterface.OnKeyListener onKeyListener) { + mDialog.setOnKeyListener(onKeyListener); + } + + public void onProvideKeyboardShortcuts(List data, @Nullable Menu menu, int deviceId) { + mDialog.onProvideKeyboardShortcuts(data, menu, deviceId); + } + + public JsDialog once(String eventName, Object listener) { + mEmitter.once(eventName, listener); + return this; + } + + public JsDialog on(String eventName, Object listener) { + mEmitter.on(eventName, listener); + return this; + } + + public JsDialog addListener(String eventName, Object listener) { + mEmitter.addListener(eventName, listener); + return this; + } + + public boolean emit(String eventName, Object... args) { + return mEmitter.emit(eventName, args); + } + + public String[] eventNames() { + return mEmitter.eventNames(); + } + + public int listenerCount(String eventName) { + return mEmitter.listenerCount(eventName); + } + + public Object[] listeners(String eventName) { + return mEmitter.listeners(eventName); + } + + public JsDialog prependListener(String eventName, Object listener) { + mEmitter.prependListener(eventName, listener); + return this; + } + + public JsDialog prependOnceListener(String eventName, Object listener) { + mEmitter.prependOnceListener(eventName, listener); + return this; + } + + public JsDialog removeAllListeners() { + mEmitter.removeAllListeners(); + return this; + } + + public JsDialog removeAllListeners(String eventName) { + mEmitter.removeAllListeners(eventName); + return this; + } + + public JsDialog removeListener(String eventName, Object listener) { + mEmitter.removeListener(eventName, listener); + return this; + } + + public JsDialog setMaxListeners(int n) { + mEmitter.setMaxListeners(n); + return this; + } + + public int getMaxListeners() { + return mEmitter.getMaxListeners(); + } + + public static int defaultMaxListeners() { + return EventEmitter.defaultMaxListeners(); + } + + +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/dialog/JsDialogBuilder.java b/app/src/main/java/com/stardust/autojs/core/ui/dialog/JsDialogBuilder.java new file mode 100644 index 00000000..5dc2ff13 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/dialog/JsDialogBuilder.java @@ -0,0 +1,153 @@ +package com.stardust.autojs.core.ui.dialog; + +import android.content.Context; + + +import android.widget.EditText; + +import com.afollestad.materialdialogs.DialogAction; +import com.afollestad.materialdialogs.GravityEnum; +import com.afollestad.materialdialogs.MaterialDialog; +import com.afollestad.materialdialogs.StackingBehavior; +import com.afollestad.materialdialogs.Theme; +import com.stardust.autojs.core.eventloop.EventEmitter; +import com.stardust.autojs.core.looper.Loopers; +import com.stardust.autojs.core.looper.Timer; +import com.stardust.autojs.runtime.ScriptRuntime; +import com.stardust.util.UiHandler; + +/** + * Created by Stardust on 2018/4/17. + */ + +public class JsDialogBuilder extends MaterialDialog.Builder { + + private final EventEmitter mEmitter; + private final UiHandler mUiHandler; + private final Timer mTimer; + private final Loopers mLoopers; + private JsDialog mDialog; + private volatile int mWaitId = -1; + + + public JsDialogBuilder(Context context, ScriptRuntime runtime) { + super(context); + mTimer = runtime.timers.getTimerForCurrentThread(); + mLoopers = runtime.loopers; + mEmitter = new EventEmitter(runtime.bridges); + mUiHandler = runtime.uiHandler; + setUpEvents(); + } + + private void setUpEvents() { + showListener(dialog -> emit("show", dialog)); + onAny((dialog, which) -> { + switch (which) { + case NEUTRAL: + emit("neutral", dialog); + emit("any", "neutral", dialog); + break; + case NEGATIVE: + emit("negative", dialog); + emit("any", "negative", dialog); + break; + case POSITIVE: + EditText editText = dialog.getInputEditText(); + if (editText != null) { + emit("input", editText.getText().toString()); + } + emit("positive", dialog); + emit("any", "positive", dialog); + break; + } + }); + dismissListener(dialog -> { + mTimer.postDelayed(() -> mLoopers.doNotWaitWhenIdle(mWaitId), 0); + emit("dismiss", dialog); + }); + cancelListener(dialog -> emit("cancel", dialog)); + } + + public void onShowCalled() { + mTimer.postDelayed(() -> mWaitId = mLoopers.waitWhenIdle(), 0); + } + + public JsDialog getDialog() { + return mDialog; + } + + public JsDialog buildDialog() { + mDialog = new JsDialog(this, mEmitter, mUiHandler); + return mDialog; + } + + public JsDialogBuilder once(String eventName, Object listener) { + mEmitter.once(eventName, listener); + return this; + } + + public JsDialogBuilder on(String eventName, Object listener) { + mEmitter.on(eventName, listener); + return this; + } + + public JsDialogBuilder addListener(String eventName, Object listener) { + mEmitter.addListener(eventName, listener); + return this; + } + + public boolean emit(String eventName, Object... args) { + return mEmitter.emit(eventName, args); + } + + public String[] eventNames() { + return mEmitter.eventNames(); + } + + public int listenerCount(String eventName) { + return mEmitter.listenerCount(eventName); + } + + public Object[] listeners(String eventName) { + return mEmitter.listeners(eventName); + } + + public JsDialogBuilder prependListener(String eventName, Object listener) { + mEmitter.prependListener(eventName, listener); + return this; + } + + public JsDialogBuilder prependOnceListener(String eventName, Object listener) { + mEmitter.prependOnceListener(eventName, listener); + return this; + } + + public JsDialogBuilder removeAllListeners() { + mEmitter.removeAllListeners(); + return this; + } + + public JsDialogBuilder removeAllListeners(String eventName) { + mEmitter.removeAllListeners(eventName); + return this; + } + + public JsDialogBuilder removeListener(String eventName, Object listener) { + mEmitter.removeListener(eventName, listener); + return this; + } + + public JsDialogBuilder setMaxListeners(int n) { + mEmitter.setMaxListeners(n); + return this; + } + + public int getMaxListeners() { + return mEmitter.getMaxListeners(); + } + + public static int defaultMaxListeners() { + return EventEmitter.defaultMaxListeners(); + } + +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/inflater/DynamicLayoutInflater.java b/app/src/main/java/com/stardust/autojs/core/ui/inflater/DynamicLayoutInflater.java new file mode 100644 index 00000000..37c0b4fb --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/inflater/DynamicLayoutInflater.java @@ -0,0 +1,361 @@ +package com.stardust.autojs.core.ui.inflater; + +import android.content.Context; +import android.os.Build; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import android.util.AttributeSet; +import android.util.Log; +import android.view.InflateException; +import android.view.View; +import android.view.ViewGroup; +import android.widget.DatePicker; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.RadioGroup; +import android.widget.TextView; +import android.widget.TimePicker; + +import com.google.android.material.appbar.AppBarLayout; +import com.stardust.autojs.core.ui.inflater.inflaters.AppBarInflater; +import com.stardust.autojs.core.ui.inflater.inflaters.BaseViewInflater; +import com.stardust.autojs.core.ui.inflater.inflaters.DatePickerInflater; +import com.stardust.autojs.core.ui.inflater.inflaters.FrameLayoutInflater; +import com.stardust.autojs.core.ui.inflater.inflaters.ImageViewInflater; +import com.stardust.autojs.core.ui.inflater.inflaters.LinearLayoutInflater; +import com.stardust.autojs.core.ui.inflater.inflaters.ProgressBarInflater; +import com.stardust.autojs.core.ui.inflater.inflaters.RadioGroupInflater; +import com.stardust.autojs.core.ui.inflater.inflaters.SpinnerInflater; +import com.stardust.autojs.core.ui.inflater.inflaters.TabLayoutInflater; +import com.stardust.autojs.core.ui.inflater.inflaters.TextViewInflater; +import com.stardust.autojs.core.ui.inflater.inflaters.TimePickerInflater; +import com.stardust.autojs.core.ui.inflater.inflaters.ToolbarInflater; +import com.stardust.autojs.core.ui.inflater.inflaters.ViewGroupInflater; +import com.stardust.autojs.core.ui.inflater.util.Res; +import com.stardust.autojs.core.ui.widget.JsSpinner; +import com.stardust.autojs.core.ui.widget.JsTabLayout; +import com.stardust.autojs.core.ui.widget.JsToolbar; +import com.stardust.autojs.core.ui.xml.XmlConverter; + +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.io.ByteArrayInputStream; +import java.util.HashMap; +import java.util.Map; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + + +public class DynamicLayoutInflater { + + public static final int FLAG_DEFAULT = 0; + public static final int FLAG_IGNORES_DYNAMIC_ATTRS = 1; + public static final int FLAG_JUST_DYNAMIC_ATTRS = 2; + + private static final String LOG_TAG = "DynamicLayoutInflater"; + + private Map> mViewAttrSetters = new HashMap<>(); + private Map> mViewCreators = new HashMap<>(); + private Context mContext; + private ResourceParser mResourceParser; + @NonNull + private LayoutInflaterDelegate mLayoutInflaterDelegate = LayoutInflaterDelegate.NO_OP; + private int mInflateFlags; + + public DynamicLayoutInflater(ResourceParser resourceParser) { + mResourceParser = resourceParser; + registerViewAttrSetters(); + } + + @SuppressWarnings("IncompleteCopyConstructor") + public DynamicLayoutInflater(DynamicLayoutInflater inflater) { + this.mResourceParser = inflater.mResourceParser; + this.mContext = inflater.mContext; + this.mViewAttrSetters = new HashMap<>(inflater.mViewAttrSetters); + this.mViewCreators = new HashMap<>(inflater.mViewCreators); + } + + public int getInflateFlags() { + return mInflateFlags; + } + + public void setInflateFlags(int inflateFlags) { + mInflateFlags = inflateFlags; + } + + public ResourceParser getResourceParser() { + return mResourceParser; + } + + public Context getContext() { + return mContext; + } + + public void setContext(Context context) { + mContext = context; + } + + @NonNull + public LayoutInflaterDelegate getLayoutInflaterDelegate() { + return mLayoutInflaterDelegate; + } + + public void setLayoutInflaterDelegate(@Nullable LayoutInflaterDelegate layoutInflaterDelegate) { + if (layoutInflaterDelegate == null) + layoutInflaterDelegate = LayoutInflaterDelegate.NO_OP; + mLayoutInflaterDelegate = layoutInflaterDelegate; + } + + protected void registerViewAttrSetters() { + registerViewAttrSetter(TextView.class.getName(), new TextViewInflater<>(mResourceParser)); + registerViewAttrSetter(EditText.class.getName(), new TextViewInflater<>(mResourceParser)); + registerViewAttrSetter(ImageView.class.getName(), new ImageViewInflater<>(mResourceParser)); + registerViewAttrSetter(LinearLayout.class.getName(), new LinearLayoutInflater<>(mResourceParser)); + registerViewAttrSetter(FrameLayout.class.getName(), new FrameLayoutInflater<>(mResourceParser)); + registerViewAttrSetter(View.class.getName(), new BaseViewInflater<>(mResourceParser)); + registerViewAttrSetter(JsToolbar.class.getName(), new ToolbarInflater<>(mResourceParser)); + registerViewAttrSetter(DatePicker.class.getName(), new DatePickerInflater(mResourceParser)); + registerViewAttrSetter(RadioGroup.class.getName(), new RadioGroupInflater<>(mResourceParser)); + registerViewAttrSetter(ProgressBar.class.getName(), new ProgressBarInflater<>(mResourceParser)); + registerViewAttrSetter(JsSpinner.class.getName(), new SpinnerInflater(mResourceParser)); + registerViewAttrSetter(TimePicker.class.getName(), new TimePickerInflater(mResourceParser)); + registerViewAttrSetter(AppBarLayout.class.getName(), new AppBarInflater<>(mResourceParser)); + registerViewAttrSetter(JsTabLayout.class.getName(), new TabLayoutInflater<>(mResourceParser)); + } + + public void registerViewAttrSetter(String fullName, ViewInflater inflater) { + mViewAttrSetters.put(fullName, inflater); + ViewCreator creator = inflater.getCreator(); + if (creator != null) { + mViewCreators.put(fullName, creator); + } + } + + public View inflate(String xml) { + return inflate(xml, null); + } + + public View inflate(String xml, @Nullable ViewGroup parent) { + return inflate(xml, parent, parent != null); + } + + public View inflate(String xml, @Nullable ViewGroup parent, boolean attachToParent) { + InflateContext context = newInflateContext(); + return inflate(context, xml, parent, attachToParent); + } + + public View inflate(InflateContext context, String xml, @Nullable ViewGroup parent, boolean attachToParent) { + View view = mLayoutInflaterDelegate.beforeInflation(context, xml, parent); + if (view != null) + return view; + xml = convertXml(context, xml); + return mLayoutInflaterDelegate.afterInflation(context, doInflation(context, xml, parent, attachToParent), xml, parent); + } + + public InflateContext newInflateContext(){ + return new InflateContext(); + } + + + protected View doInflation(InflateContext context, String xml, @Nullable ViewGroup parent, boolean attachToParent) { + try { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + DocumentBuilder db = dbf.newDocumentBuilder(); + Document document = db.parse(new ByteArrayInputStream(xml.getBytes())); + return inflate(context, document.getDocumentElement(), parent, attachToParent); + } catch (Exception e) { + throw new InflateException(e); + } + } + + protected String convertXml(InflateContext context, String xml) { + String str = mLayoutInflaterDelegate.beforeConvertXml(context, xml); + if (str != null) + return str; + try { + return mLayoutInflaterDelegate.afterConvertXml(context, XmlConverter.convertToAndroidLayout(xml)); + } catch (Exception e) { + throw new InflateException(e); + } + } + + public View inflate(InflateContext context, Node node, @Nullable ViewGroup parent, boolean attachToParent) { + View view = doInflation(context, node, parent, attachToParent); + if (view instanceof ShouldCallOnFinishInflate) { + ((ShouldCallOnFinishInflate) view).onFinishDynamicInflate(); + } + return view; + } + + protected View doInflation(InflateContext context, Node node, @Nullable ViewGroup parent, boolean attachToParent) { + View view = mLayoutInflaterDelegate.beforeInflateView(context, node, parent, attachToParent); + if (view != null) + return view; + HashMap attrs = getAttributesMap(node); + view = doCreateView(context, node, node.getNodeName(), parent, attrs); + if (parent != null) { + parent.addView(view); // have to add to parent to generate layout params + if (!attachToParent) { + parent.removeView(view); + } + } + ViewInflater inflater = applyAttributes(context, view, attrs, parent); + if (view instanceof ViewGroup && node.hasChildNodes()) { + inflateChildren(context, inflater, node, (ViewGroup) view); + if (inflater instanceof ViewGroupInflater) { + applyPendingAttributesOfChildren(context, (ViewGroupInflater) inflater, (ViewGroup) view); + } + } + return mLayoutInflaterDelegate.afterInflateView(context, view, node, parent, attachToParent); + } + + @SuppressWarnings("unchecked") + protected void applyPendingAttributesOfChildren(InflateContext context, ViewGroupInflater inflater, ViewGroup view) { + if (mLayoutInflaterDelegate.beforeApplyPendingAttributesOfChildren(context, inflater, view)) { + return; + } + inflater.applyPendingAttributesOfChildren(view); + mLayoutInflaterDelegate.afterApplyPendingAttributesOfChildren(context, inflater, view); + + } + + + @SuppressWarnings("unchecked") + public ViewInflater applyAttributes(InflateContext context, View view, HashMap attrs, @Nullable ViewGroup parent) { + ViewInflater inflater = (ViewInflater) getViewInflater(view); + if (mLayoutInflaterDelegate.beforeApplyAttributes(context, view, inflater, attrs, parent)) { + return inflater; + } + applyAttributes(context, view, inflater, attrs, parent); + mLayoutInflaterDelegate.afterApplyAttributes(context, view, inflater, attrs, parent); + return inflater; + } + + @Nullable + public ViewInflater getViewInflater(View view) { + ViewInflater setter = mViewAttrSetters.get(view.getClass().getName()); + Class c = view.getClass(); + while (setter == null && c != View.class) { + c = c.getSuperclass(); + setter = mViewAttrSetters.get(c.getName()); + } + return setter; + } + + protected void inflateChildren(InflateContext context, ViewInflater inflater, Node node, ViewGroup parent) { + if (mLayoutInflaterDelegate.beforeInflateChildren(context, inflater, node, parent)) { + return; + } + if (inflater.inflateChildren(this, node, parent)) { + return; + } + inflateChildren(context, node, parent); + mLayoutInflaterDelegate.afterInflateChildren(context, inflater, node, parent); + } + + public void inflateChildren(InflateContext context, Node node, ViewGroup parent) { + NodeList nodeList = node.getChildNodes(); + for (int i = 0; i < nodeList.getLength(); i++) { + Node currentNode = nodeList.item(i); + if (currentNode.getNodeType() != Node.ELEMENT_NODE) continue; + inflate(context, currentNode, parent, true); + } + } + + protected View doCreateView(InflateContext context, Node node, String viewName, ViewGroup parent, HashMap attrs) { + View view = mLayoutInflaterDelegate.beforeCreateView(context, node, viewName, parent, attrs); + if (view != null) + return view; + return mLayoutInflaterDelegate.afterCreateView(context, createViewForName(viewName, attrs), node, viewName, parent, attrs); + } + + public View createViewForName(String name, HashMap attrs) { + try { + if (name.equals("View")) { + return new View(mContext); + } + if (!name.contains(".")) { + name = "android.widget." + name; + } + ViewCreator creator = mViewCreators.get(name); + if (creator != null) { + return creator.create(mContext, attrs); + } + Class clazz = Class.forName(name); + String style = attrs.get("style"); + if (style == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + return (View) clazz.getConstructor(Context.class).newInstance(mContext); + } else { + int styleRes = Res.parseStyle(mContext, style); + return (View) clazz.getConstructor(Context.class, AttributeSet.class, int.class, int.class) + .newInstance(mContext, null, 0, styleRes); + } + } catch (Exception e) { + throw new InflateException(e); + } + } + + + public HashMap getAttributesMap(Node currentNode) { + NamedNodeMap attributeMap = currentNode.getAttributes(); + int attributeCount = attributeMap.getLength(); + HashMap attributes = new HashMap<>(attributeCount); + for (int j = 0; j < attributeCount; j++) { + Node attr = attributeMap.item(j); + String nodeName = attr.getNodeName(); + attributes.put(nodeName, attr.getNodeValue()); + } + return attributes; + } + + @SuppressWarnings("unchecked") + protected void applyAttributes(InflateContext context, View view, ViewInflater setter, Map attrs, @Nullable ViewGroup parent) { + if (setter != null) { + for (Map.Entry entry : attrs.entrySet()) { + String[] attr = entry.getKey().split(":"); + if (attr.length == 1) { + applyAttribute(context, setter, view, null, attr[0], entry.getValue(), parent, attrs); + } else if (attr.length == 2) { + applyAttribute(context, setter, view, attr[0], attr[1], entry.getValue(), parent, attrs); + } else { + throw new InflateException("illegal attr name: " + entry.getKey()); + } + } + setter.applyPendingAttributes(view, parent); + } else { + Log.e(LOG_TAG, "cannot set attributes for view: " + view.getClass()); + } + + } + + protected void applyAttribute(InflateContext context, ViewInflater inflater, View view, String ns, String attrName, String value, ViewGroup parent, Map attrs) { + if (mLayoutInflaterDelegate.beforeApplyAttribute(context, inflater, view, ns, attrName, value, parent, attrs)) { + return; + } + boolean isDynamic = isDynamicValue(value); + if ((isDynamic && mInflateFlags == FLAG_IGNORES_DYNAMIC_ATTRS) + || (!isDynamic && mInflateFlags == FLAG_JUST_DYNAMIC_ATTRS)) { + return; + } + inflater.setAttr(view, ns, attrName, value, parent, attrs); + mLayoutInflaterDelegate.afterApplyAttribute(context, inflater, view, ns, attrName, value, parent, attrs); + + } + + public boolean isDynamicValue(String value) { + int i = value.indexOf("{{"); + if (i < 0) + return false; + return value.indexOf("}}", i + 1) >= 0; + } + + +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/inflater/ImageLoader.java b/app/src/main/java/com/stardust/autojs/core/ui/inflater/ImageLoader.java new file mode 100644 index 00000000..4dfb8597 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/inflater/ImageLoader.java @@ -0,0 +1,33 @@ +package com.stardust.autojs.core.ui.inflater; + +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.view.View; +import android.widget.ImageView; + +/** + * Created by Stardust on 2017/11/3. + */ + +public interface ImageLoader { + + interface BitmapCallback { + void onLoaded(Bitmap bitmap); + } + + interface DrawableCallback { + void onLoaded(Drawable drawable); + } + + void loadInto(ImageView view, Uri uri); + + void loadIntoBackground(View view, Uri uri); + + Drawable load(View view, Uri uri); + + void load(View view, Uri uri, DrawableCallback callback); + + void load(View view, Uri uri, BitmapCallback callback); + +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/inflater/InflateContext.java b/app/src/main/java/com/stardust/autojs/core/ui/inflater/InflateContext.java new file mode 100644 index 00000000..c4f03115 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/inflater/InflateContext.java @@ -0,0 +1,32 @@ +package com.stardust.autojs.core.ui.inflater; + +import java.util.HashMap; + +public class InflateContext { + + private HashMap mProperties; + + public void put(String key, Object value) { + if (mProperties == null) { + mProperties = new HashMap<>(); + } + mProperties.put(key, value); + } + + public Object get(String key) { + if(mProperties == null) + return null; + return mProperties.get(key); + } + + + public Object remove(String key){ + if(mProperties == null) + return null; + return mProperties.remove(key); + } + + public boolean has(String key) { + return mProperties.containsKey(key); + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/inflater/LayoutInflaterDelegate.java b/app/src/main/java/com/stardust/autojs/core/ui/inflater/LayoutInflaterDelegate.java new file mode 100644 index 00000000..b1a5133e --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/inflater/LayoutInflaterDelegate.java @@ -0,0 +1,136 @@ +package com.stardust.autojs.core.ui.inflater; + +import android.view.View; +import android.view.ViewGroup; + +import com.stardust.autojs.core.ui.inflater.inflaters.ViewGroupInflater; + +import org.w3c.dom.Node; + +import java.util.HashMap; +import java.util.Map; + +/** + * Created by Stardust on 2018/3/29. + */ + +public interface LayoutInflaterDelegate { + + LayoutInflaterDelegate NO_OP = new NoOp(); + + View beforeInflation(InflateContext inflateContext, String xml, ViewGroup parent); + + View afterInflation(InflateContext inflateContext, View doInflation, String xml, ViewGroup parent); + + String beforeConvertXml(InflateContext inflateContext, String xml); + + String afterConvertXml(InflateContext inflateContext, String xml); + + View beforeInflateView(InflateContext inflateContext, Node node, ViewGroup parent, boolean attachToParent); + + View afterInflateView(InflateContext inflateContext, View view, Node node, ViewGroup parent, boolean attachToParent); + + View beforeCreateView(InflateContext inflateContext, Node node, String viewName, ViewGroup parent, HashMap attrs); + + View afterCreateView(InflateContext inflateContext, View view, Node node, String viewName, ViewGroup parent, HashMap attrs); + + boolean beforeApplyAttributes(InflateContext inflateContext, View view, ViewInflater inflater, HashMap attrs, ViewGroup parent); + + void afterApplyAttributes(InflateContext inflateContext, View view, ViewInflater inflater, HashMap attrs, ViewGroup parent); + + boolean beforeInflateChildren(InflateContext inflateContext, ViewInflater inflater, Node node, ViewGroup parent); + + void afterInflateChildren(InflateContext inflateContext, ViewInflater inflater, Node node, ViewGroup parent); + + void afterApplyPendingAttributesOfChildren(InflateContext inflateContext, ViewGroupInflater inflater, ViewGroup view); + + boolean beforeApplyPendingAttributesOfChildren(InflateContext inflateContext, ViewGroupInflater inflater, ViewGroup view); + + boolean beforeApplyAttribute(InflateContext inflateContext, ViewInflater inflater, View view, String ns, String attrName, String value, ViewGroup parent, Map attrs); + + + void afterApplyAttribute(InflateContext inflateContext, ViewInflater inflater, View view, String ns, String attrName, String value, ViewGroup parent, Map attrs); + + + class NoOp implements LayoutInflaterDelegate { + @Override + public String beforeConvertXml(InflateContext inflateContext, String xml) { + return null; + } + + @Override + public String afterConvertXml(InflateContext inflateContext, String xml) { + return xml; + } + + @Override + public View afterInflation(InflateContext inflateContext, View result, String xml, ViewGroup parent) { + return result; + } + + @Override + public View beforeInflation(InflateContext inflateContext, String xml, ViewGroup parent) { + return null; + } + + @Override + public View beforeInflateView(InflateContext inflateContext, Node node, ViewGroup parent, boolean attachToParent) { + return null; + } + + @Override + public View afterInflateView(InflateContext inflateContext, View view, Node node, ViewGroup parent, boolean attachToParent) { + return view; + } + + @Override + public View beforeCreateView(InflateContext inflateContext, Node node, String viewName, ViewGroup parent, HashMap attrs) { + return null; + } + + @Override + public View afterCreateView(InflateContext inflateContext, View view, Node node, String viewName, ViewGroup parent, HashMap attrs) { + return view; + } + + @Override + public boolean beforeApplyAttributes(InflateContext inflateContext, View view, ViewInflater inflater, HashMap attrs, ViewGroup parent) { + return false; + } + + @Override + public void afterApplyAttributes(InflateContext inflateContext, View view, ViewInflater inflater, HashMap attrs, ViewGroup parent) { + + } + + @Override + public boolean beforeInflateChildren(InflateContext inflateContext, ViewInflater inflater, Node node, ViewGroup parent) { + return false; + } + + @Override + public void afterInflateChildren(InflateContext inflateContext, ViewInflater inflater, Node node, ViewGroup parent) { + + } + + @Override + public void afterApplyPendingAttributesOfChildren(InflateContext inflateContext, ViewGroupInflater inflater, ViewGroup view) { + + } + + @Override + public boolean beforeApplyPendingAttributesOfChildren(InflateContext inflateContext, ViewGroupInflater inflater, ViewGroup view) { + return false; + } + + @Override + public boolean beforeApplyAttribute(InflateContext inflateContext, ViewInflater inflater, View view, String ns, String attrName, String value, ViewGroup parent, Map attrs) { + return false; + } + + @Override + public void afterApplyAttribute(InflateContext inflateContext, ViewInflater inflater, View view, String ns, String attrName, String value, ViewGroup parent, Map attrs) { + + } + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/inflater/ResourceParser.java b/app/src/main/java/com/stardust/autojs/core/ui/inflater/ResourceParser.java new file mode 100644 index 00000000..1d444169 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/inflater/ResourceParser.java @@ -0,0 +1,21 @@ +package com.stardust.autojs.core.ui.inflater; + +import com.stardust.autojs.core.ui.inflater.util.Drawables; + +/** + * Created by Stardust on 2018/1/24. + */ + +public class ResourceParser { + + + private final Drawables mDrawables; + + public ResourceParser(Drawables drawables) { + mDrawables = drawables; + } + + public Drawables getDrawables() { + return mDrawables; + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/inflater/ShouldCallOnFinishInflate.java b/app/src/main/java/com/stardust/autojs/core/ui/inflater/ShouldCallOnFinishInflate.java new file mode 100644 index 00000000..1f698c35 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/inflater/ShouldCallOnFinishInflate.java @@ -0,0 +1,6 @@ +package com.stardust.autojs.core.ui.inflater; + +public interface ShouldCallOnFinishInflate { + + void onFinishDynamicInflate(); +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/inflater/ViewCreator.java b/app/src/main/java/com/stardust/autojs/core/ui/inflater/ViewCreator.java new file mode 100644 index 00000000..2e7e9c88 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/inflater/ViewCreator.java @@ -0,0 +1,16 @@ +package com.stardust.autojs.core.ui.inflater; + +import android.content.Context; +import android.view.View; + +import java.util.Map; + +/** + * Created by Stardust on 2017/11/29. + */ + +public interface ViewCreator { + + V create(Context context, Map attrs); + +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/inflater/ViewInflater.java b/app/src/main/java/com/stardust/autojs/core/ui/inflater/ViewInflater.java new file mode 100644 index 00000000..e7abd6c8 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/inflater/ViewInflater.java @@ -0,0 +1,29 @@ +package com.stardust.autojs.core.ui.inflater; + +import androidx.annotation.Nullable; +import android.view.View; +import android.view.ViewGroup; + + +import org.w3c.dom.Node; + +import java.util.Map; + +/** + * Created by Stardust on 2017/11/3. + */ + + +public interface ViewInflater { + + boolean setAttr(V view, String attrName, String value, ViewGroup parent, Map attrs); + + boolean setAttr(V view, String ns, String attrName, String value, ViewGroup parent, Map attrs); + + void applyPendingAttributes(V view, ViewGroup parent); + + boolean inflateChildren(DynamicLayoutInflater inflater, Node node, V parent); + + @Nullable + ViewCreator getCreator(); +} \ No newline at end of file diff --git a/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/AppBarInflater.java b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/AppBarInflater.java new file mode 100644 index 00000000..7e96b463 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/AppBarInflater.java @@ -0,0 +1,42 @@ +package com.stardust.autojs.core.ui.inflater.inflaters; + +import androidx.annotation.Nullable; +import android.view.View; +import android.view.ViewGroup; + +import com.google.android.material.appbar.AppBarLayout; +import org.autojs.autojs6.R; +import com.stardust.autojs.core.ui.inflater.ResourceParser; +import com.stardust.autojs.core.ui.inflater.ViewCreator; +import com.stardust.autojs.core.ui.inflater.util.Dimensions; + +import java.util.Map; + +public class AppBarInflater extends BaseViewInflater { + + public AppBarInflater(ResourceParser resourceParser) { + super(resourceParser); + } + + @Override + public boolean setAttr(V view, String attr, String value, ViewGroup parent, Map attrs) { + switch (attr) { + case "elevation": + view.setTargetElevation(Dimensions.parseToPixel(value, view)); + break; + case "expanded": + view.setExpanded(Boolean.parseBoolean(value)); + break; + default: + return super.setAttr(view, attr, value, parent, attrs); + } + return true; + } + + @Nullable + @Override + @SuppressWarnings("unchecked") + public ViewCreator getCreator() { + return (ViewCreator) (context, attrs) -> (V) View.inflate(context, R.layout.js_appbar, null); + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/BaseViewInflater.java b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/BaseViewInflater.java new file mode 100644 index 00000000..a4a24cdc --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/BaseViewInflater.java @@ -0,0 +1,653 @@ +package com.stardust.autojs.core.ui.inflater.inflaters; + +import android.content.res.ColorStateList; +import android.graphics.PorterDuff; +import android.os.Build; +import androidx.annotation.Nullable; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CompoundButton; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; + + +import com.stardust.autojs.core.ui.ViewExtras; +import com.stardust.autojs.core.ui.attribute.ViewAttributes; +import com.stardust.autojs.core.ui.inflater.DynamicLayoutInflater; +import com.stardust.autojs.core.ui.inflater.ResourceParser; +import com.stardust.autojs.core.ui.inflater.ViewInflater; +import com.stardust.autojs.core.ui.inflater.ViewCreator; +import com.stardust.autojs.core.ui.inflater.util.Colors; +import com.stardust.autojs.core.ui.inflater.util.Dimensions; +import com.stardust.autojs.core.ui.inflater.util.Drawables; +import com.stardust.autojs.core.ui.inflater.util.Gravities; +import com.stardust.autojs.core.ui.inflater.util.Ids; +import com.stardust.autojs.core.ui.inflater.util.Strings; +import com.stardust.autojs.core.ui.inflater.util.ValueMapper; + +import org.w3c.dom.Node; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Map; + +/** + * Created by Stardust on 2017/11/3. + */ + +public class BaseViewInflater implements ViewInflater { + + private static final String LOG_TAG = "BaseViewInflater"; + + public static final ValueMapper TINT_MODES = new ValueMapper("tintMode") + .map("add", PorterDuff.Mode.ADD) + .map("multiply", PorterDuff.Mode.MULTIPLY) + .map("screen", PorterDuff.Mode.SCREEN) + .map("src_atop", PorterDuff.Mode.SRC_ATOP) + .map("src_in", PorterDuff.Mode.SRC_IN) + .map("src_over", PorterDuff.Mode.SRC_OVER); + + public static final ValueMapper DRAWABLE_CACHE_QUALITIES = new ValueMapper("drawingCacheQuality") + .map("auto", View.DRAWING_CACHE_QUALITY_AUTO) + .map("high", View.DRAWING_CACHE_QUALITY_HIGH) + .map("low", View.DRAWING_CACHE_QUALITY_LOW); + + public static final ValueMapper IMPORTANT_FOR_ACCESSIBILITY = new ValueMapper("importantForAccessibility") + .map("auto", 0) //View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) + .map("no", 2) //View.IMPORTANT_FOR_ACCESSIBILITY_NO) + .map("noHideDescendants", 4) //View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) + .map("yes", 1); //View.IMPORTANT_FOR_ACCESSIBILITY_YES); + public static final ValueMapper LAYOUT_DIRECTIONS = new ValueMapper("layoutDirection") + .map("inherit", 2) + .map("locale", 3) + .map("ltr", 0) + .map("rtl", 1); + public static final ValueMapper SCROLLBARS_STYLES = new ValueMapper("scrollbarStyle") + .map("insideInset", View.SCROLLBARS_INSIDE_INSET) + .map("insideOverlay", View.SCROLLBARS_INSIDE_OVERLAY) + .map("outsideInset", View.SCROLLBARS_OUTSIDE_INSET) + .map("outsideOverlay", View.SCROLLBARS_OUTSIDE_OVERLAY); + public static final ValueMapper SCROLL_INDICATORS = new ValueMapper("scrollIndicators") + .map("bottom", 2) //View.SCROLL_INDICATOR_BOTTOM) + .map("end", 20) //View.SCROLL_INDICATOR_END) + .map("left", 4) //View.SCROLL_INDICATOR_LEFT) + .map("none", 0) + .map("right", 8) //View.SCROLL_INDICATOR_RIGHT) + .map("start", 10) //View.SCROLL_INDICATOR_START) + .map("top", 1); //View.SCROLL_INDICATOR_TOP) + public static final ValueMapper VISIBILITY = new ValueMapper("visibility") + .map("visible", View.VISIBLE) + .map("invisible", View.INVISIBLE) + .map("gone", View.GONE); + public static final ValueMapper TEXT_DIRECTIONS = new ValueMapper("textDirection") + .map("anyRtl", 2) + .map("firstStrong", 1) + .map("firstStrongLtr", 6) + .map("firstStrongRtl", 7) + .map("inherit", 0) + .map("locale", 5) + .map("ltr", 3) + .map("rtl", 4); + public static final ValueMapper TEXT_ALIGNMENTS = new ValueMapper("textAlignment") + .map("center", 4) + .map("gravity", 1) + .map("inherit", 0) + .map("textEnd", 3) + .map("textStart", 2) + .map("viewEnd", 6) + .map("viewStart", 5); + + private final ResourceParser mResourceParser; + + public BaseViewInflater(ResourceParser resourceParser) { + mResourceParser = resourceParser; + } + + public Drawables getDrawables() { + return mResourceParser.getDrawables(); + } + + public ResourceParser getResourceParser() { + return mResourceParser; + } + + @Override + public boolean setAttr(V view, String attr, String value, ViewGroup parent, Map attrs) { + ViewAttributes viewAttributes = ViewExtras.getViewAttributes(view, getResourceParser()); + ViewAttributes.Attribute attribute = viewAttributes.get(attr); + if (attribute != null) { + Log.d(LOG_TAG, "setAttr use ViewAttributes: attr = " + attr); + attribute.set(value); + return true; + } + Log.d(LOG_TAG, "setAttr cannot use ViewAttributes: attr = " + attr); + Integer layoutRule = null; + boolean layoutTarget = false; + ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); + switch (attr) { + case "id": + view.setId(Ids.parse(value)); + break; + case "gravity": + return setGravity(view, value); + case "width": + case "layout_width": + switch (value) { + case "wrap_content": + layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT; + break; + case "fill_parent": + case "match_parent": + layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; + break; + default: + layoutParams.width = Dimensions.parseToPixel(value, view, parent, true); + break; + } + break; + case "height": + case "layout_height": + switch (value) { + case "wrap_content": + layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; + break; + case "fill_parent": + case "match_parent": + layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT; + break; + default: + layoutParams.height = Dimensions.parseToPixel(value, view, parent, false); + break; + } + break; + case "layout_gravity": + if (parent instanceof LinearLayout) { + ((LinearLayout.LayoutParams) layoutParams).gravity = Gravities.parse(value); + } else if (parent instanceof FrameLayout) { + ((FrameLayout.LayoutParams) layoutParams).gravity = Gravities.parse(value); + } else { + return setLayoutGravity(parent, view, Gravities.parse(value)); + } + break; + case "layout_weight": + if (parent instanceof LinearLayout) { + ((LinearLayout.LayoutParams) layoutParams).weight = Float.parseFloat(value); + } + break; + case "layout_below": + layoutRule = RelativeLayout.BELOW; + layoutTarget = true; + break; + case "layout_above": + layoutRule = RelativeLayout.ABOVE; + layoutTarget = true; + break; + case "layout_toLeftOf": + layoutRule = RelativeLayout.LEFT_OF; + layoutTarget = true; + break; + case "layout_toRightOf": + layoutRule = RelativeLayout.RIGHT_OF; + layoutTarget = true; + break; + case "layout_alignBottom": + layoutRule = RelativeLayout.ALIGN_BOTTOM; + layoutTarget = true; + break; + case "layout_alignTop": + layoutRule = RelativeLayout.ALIGN_TOP; + layoutTarget = true; + break; + case "layout_alignLeft": + case "layout_alignStart": + layoutRule = RelativeLayout.ALIGN_LEFT; + layoutTarget = true; + break; + case "layout_alignRight": + case "layout_alignEnd": + layoutRule = RelativeLayout.ALIGN_RIGHT; + layoutTarget = true; + break; + case "layout_alignParentBottom": + layoutRule = RelativeLayout.ALIGN_PARENT_BOTTOM; + break; + case "layout_alignParentTop": + layoutRule = RelativeLayout.ALIGN_PARENT_TOP; + break; + case "layout_alignParentLeft": + case "layout_alignParentStart": + layoutRule = RelativeLayout.ALIGN_PARENT_LEFT; + break; + case "layout_alignParentRight": + case "layout_alignParentEnd": + layoutRule = RelativeLayout.ALIGN_PARENT_RIGHT; + break; + case "layout_centerHorizontal": + layoutRule = RelativeLayout.CENTER_HORIZONTAL; + break; + case "layout_centerVertical": + layoutRule = RelativeLayout.CENTER_VERTICAL; + break; + case "layout_centerInParent": + layoutRule = RelativeLayout.CENTER_IN_PARENT; + break; + case "layout_margin": + if (view.getLayoutParams() instanceof ViewGroup.MarginLayoutParams) { + ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) view.getLayoutParams(); + int margin = Dimensions.parseToIntPixel(value, view); + params.bottomMargin = params.leftMargin = params.topMargin = params.rightMargin = margin; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + params.setMarginStart(margin); + params.setMarginEnd(margin); + } + } + break; + case "layout_marginLeft": + if (view.getLayoutParams() instanceof ViewGroup.MarginLayoutParams) { + ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) view.getLayoutParams(); + params.leftMargin = Dimensions.parseToIntPixel(value, view); + } + break; + case "layout_marginTop": + if (view.getLayoutParams() instanceof ViewGroup.MarginLayoutParams) { + ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) view.getLayoutParams(); + params.topMargin = Dimensions.parseToIntPixel(value, view); + } + break; + case "layout_marginRight": + if (view.getLayoutParams() instanceof ViewGroup.MarginLayoutParams) { + ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) view.getLayoutParams(); + params.rightMargin = Dimensions.parseToIntPixel(value, view); + } + break; + case "layout_marginBottom": + if (view.getLayoutParams() instanceof ViewGroup.MarginLayoutParams) { + ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) view.getLayoutParams(); + params.bottomMargin = Dimensions.parseToIntPixel(value, view); + } + break; + case "padding": + int p = Dimensions.parseToIntPixel(value, view); + view.setPadding(p, p, p, p); + break; + case "paddingLeft": + view.setPadding(Dimensions.parseToIntPixel(value, view), view.getPaddingTop(), view.getPaddingRight(), view.getPaddingBottom()); + break; + case "paddingTop": + view.setPadding(view.getPaddingLeft(), Dimensions.parseToIntPixel(value, view), view.getPaddingRight(), view.getPaddingBottom()); + break; + case "paddingRight": + view.setPadding(view.getPaddingLeft(), view.getPaddingTop(), Dimensions.parseToIntPixel(value, view), view.getPaddingBottom()); + break; + case "paddingBottom": + view.setPadding(view.getPaddingLeft(), view.getPaddingTop(), view.getPaddingRight(), Dimensions.parseToIntPixel(value, view)); + break; + case "bg": + case "background": + getDrawables().setupWithViewBackground(view, value); + break; + case "accessibilityLiveRegion": + case "accessibilityTraversalAfter": + case "accessibilityTraversalBefore": + Exceptions.unsupports(view, attr, value); + case "alpha": + view.setAlpha(Float.valueOf(value)); + break; + case "autofillHints": + case "autofilledHighlight": + Exceptions.unsupports(view, attr, value); + break; + case "backgroundTint": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + view.setBackgroundTintList(ColorStateList.valueOf(Colors.parse(view, value))); + } + break; + case "backgroundTintMode": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + view.setBackgroundTintMode(TINT_MODES.get(value)); + } + break; + case "checked": + if (view instanceof CompoundButton) { + ((CompoundButton) view).setChecked(Boolean.parseBoolean(value)); + } + break; + case "clickable": + view.setClickable(Boolean.valueOf(value)); + break; + case "contentDescription": + view.setContentDescription(Strings.parse(view, value)); + break; + case "contextClickable": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + view.setContextClickable(Boolean.valueOf(value)); + } + break; + case "defaultFocusHighlightEnabled": + Exceptions.unsupports(view, attr, value); + break; + case "drawingCacheQuality": + view.setDrawingCacheQuality(DRAWABLE_CACHE_QUALITIES.get(value)); + break; + case "duplicateParentState": + view.setDuplicateParentStateEnabled(Boolean.valueOf(value)); + break; + case "elevation": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + view.setElevation(Dimensions.parseToIntPixel(value, view)); + } + break; + case "fadeScrollbars": + view.setScrollbarFadingEnabled(Boolean.valueOf(value)); + break; + case "fadingEdgeLength": + view.setFadingEdgeLength(Dimensions.parseToIntPixel(value, view)); + break; + case "filterTouchesWhenObscured": + view.setFilterTouchesWhenObscured(Boolean.valueOf(value)); + break; + case "fitsSystemWindows": + view.setFitsSystemWindows(Boolean.valueOf(value)); + break; + case "focusable": + view.setFocusable(Boolean.valueOf(value)); + break; + case "focusableInTouchMode": + view.setFocusableInTouchMode(Boolean.valueOf(value)); + break; + case "focusedByDefault": + Exceptions.unsupports(view, attr, value); + break; + case "forceHasOverlappingRendering": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + view.forceHasOverlappingRendering(Boolean.valueOf(value)); + } + break; + case "foreground": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + view.setForeground(getDrawables().parse(view, value)); + } + break; + case "foregroundGravity": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + view.setForegroundGravity(Gravities.parse(value)); + } + break; + case "foregroundTint": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + view.setForegroundTintList(ColorStateList.valueOf(Colors.parse(view, value))); + } + break; + case "foregroundTintMode": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + view.setForegroundTintMode(TINT_MODES.get(value)); + } + break; + case "hapticFeedbackEnabled": + view.setHapticFeedbackEnabled(Boolean.valueOf(value)); + break; + case "importantForAccessibility": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + view.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY.get(value)); + } + break; + case "importantForAutofill": + Exceptions.unsupports(view, attr, value); + break; + case "isScrollContainer": + view.setScrollContainer(Boolean.valueOf(value)); + break; + case "keepScreenOn": + view.setKeepScreenOn(Boolean.valueOf(value)); + break; + case "keyboardNavigationCluster": + Exceptions.unsupports(view, attr, value); + break; + case "layerType": + Exceptions.unsupports(view, attr, value); + break; + case "layoutDirection": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + view.setLayoutDirection(LAYOUT_DIRECTIONS.get(value)); + } + break; + case "longClickable": + view.setLongClickable(Boolean.valueOf(value)); + break; + case "minHeight": + view.setMinimumHeight(Dimensions.parseToIntPixel(value, view)); + break; + case "minWidth": + view.setMinimumWidth(Dimensions.parseToIntPixel(value, view)); + break; + case "nextClusterForward": + Exceptions.unsupports(view, attr, value); + break; + case "nextFocusDown": + Exceptions.unsupports(view, attr, value); + break; + case "nextFocusForward": + Exceptions.unsupports(view, attr, value); + break; + case "nextFocusLeft": + Exceptions.unsupports(view, attr, value); + break; + case "nextFocusRight": + Exceptions.unsupports(view, attr, value); + break; + case "nextFocusUp": + Exceptions.unsupports(view, attr, value); + break; + case "onClick": + Exceptions.unsupports(view, attr, value); + break; + case "paddingEnd": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + view.setPaddingRelative(view.getPaddingStart(), view.getPaddingTop(), + Dimensions.parseToIntPixel(value, view), view.getPaddingBottom()); + } else { + view.setPadding(view.getPaddingLeft(), view.getPaddingTop(), + Dimensions.parseToIntPixel(value, view), view.getPaddingBottom()); + } + break; + case "paddingHorizontal": + case "paddingVertical": + Exceptions.unsupports(view, attr, value); + break; + case "paddingStart": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + view.setPaddingRelative(Dimensions.parseToIntPixel(value, view), view.getPaddingTop(), + view.getPaddingEnd(), view.getPaddingBottom()); + } else { + view.setPadding(Dimensions.parseToIntPixel(value, view), view.getPaddingTop(), + view.getPaddingRight(), view.getPaddingBottom()); + } + break; + case "requiresFadingEdge": + for (String str : value.split("\\|")) { + if (str.equals("horizontal")) { + view.setHorizontalFadingEdgeEnabled(true); + } else if (str.equals("vertical")) { + view.setVerticalFadingEdgeEnabled(true); + } + } + break; + case "rotation": + view.setRotation(Float.parseFloat(value)); + break; + case "rotationX": + view.setRotationX(Float.parseFloat(value)); + break; + case "rotationY": + view.setRotationY(Float.parseFloat(value)); + break; + case "saveEnabled": + view.setSaveEnabled(Boolean.valueOf(value)); + break; + case "scaleX": + view.setScaleX(Float.parseFloat(value)); + break; + case "scaleY": + view.setScaleY(Float.parseFloat(value)); + break; + case "scrollIndicators": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + view.setScrollIndicators(SCROLL_INDICATORS.get(value)); + } + break; + case "scrollX": + view.setScrollX(Dimensions.parseToIntPixel(value, view)); + break; + case "scrollY": + view.setScrollY(Dimensions.parseToIntPixel(value, view)); + break; + case "scrollbarAlwaysDrawHorizontalTrack": + case "scrollbarAlwaysDrawVerticalTrack": + Exceptions.unsupports(view, attr, value); + break; + case "scrollbarDefaultDelayBeforeFade": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + view.setScrollBarDefaultDelayBeforeFade(Integer.valueOf(value)); + } + break; + case "scrollbarFadeDuration": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + view.setScrollBarFadeDuration(Integer.valueOf(value)); + } + break; + case "scrollbarSize": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + view.setScrollBarSize(Dimensions.parseToIntPixel(value, view)); + } + break; + case "scrollbarStyle": + view.setScrollBarStyle(SCROLLBARS_STYLES.get(value)); + break; + case "scrollbarThumbHorizontal": + case "scrollbarThumbVertical": + case "scrollbarTrackHorizontal": + case "scrollbarTrackVertical": + Exceptions.unsupports(view, attr, value); + case "scrollbars": + for (String str : value.split("|")) { + if (str.equals("horizontal")) { + view.setHorizontalScrollBarEnabled(true); + } else if (str.equals("vertical")) { + view.setVerticalScrollBarEnabled(true); + } + } + break; + case "soundEffectsEnabled": + view.setSoundEffectsEnabled(Boolean.valueOf(value)); + break; + case "stateListAnimator": + Exceptions.unsupports(view, attr, value); + case "tag": + view.setTag(Strings.parse(view, value)); + break; + case "textAlignment": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + view.setTextAlignment(TEXT_ALIGNMENTS.get(value)); + } + break; + case "textDirection": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + view.setTextDirection(TEXT_DIRECTIONS.get(value)); + } + break; + case "theme": + //Exceptions.unsupports(view, attr, value); + + break; + case "tooltipText": + Exceptions.unsupports(view, attr, value); + break; + case "transformPivotX": + view.setPivotX(Dimensions.parseToPixel(value, view)); + break; + case "transformPivotY": + view.setPivotY(Dimensions.parseToPixel(value, view)); + break; + case "transitionName": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + view.setTransitionName(Strings.parse(view, value)); + } + break; + case "translationX": + view.setTranslationX(Dimensions.parseToPixel(value, view)); + break; + case "translationY": + view.setTranslationY(Dimensions.parseToPixel(value, view)); + break; + case "translationZ": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + view.setTranslationZ(Dimensions.parseToPixel(value, view)); + } + break; + case "visibility": + view.setVisibility(VISIBILITY.get(value)); + break; + default: + return false; + + } + if (layoutRule != null && parent instanceof RelativeLayout) { + if (layoutTarget) { + int anchor = Ids.parse(value); + ((RelativeLayout.LayoutParams) layoutParams).addRule(layoutRule, anchor); + } else if (value.equals("true")) { + ((RelativeLayout.LayoutParams) layoutParams).addRule(layoutRule); + } + } + return true; + } + + public boolean setLayoutGravity(ViewGroup parent, V view, int gravity) { + try { + ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); + Field field = layoutParams.getClass().getField("gravity"); + field.set(layoutParams, gravity); + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + @Override + public boolean setAttr(V view, String ns, String attrName, String value, ViewGroup parent, Map attrs) { + if (ns == null || ns.equals("android")) { + return setAttr(view, attrName, value, parent, attrs); + } + return false; + } + + private boolean setGravity(V view, String g) { + try { + Method setGravity = view.getClass().getMethod("setGravity", int.class); + setGravity.invoke(view, Gravities.parse(g)); + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + @Override + public void applyPendingAttributes(V view, ViewGroup parent) { + + } + + @Override + public boolean inflateChildren(DynamicLayoutInflater inflater, Node node, V parent) { + return false; + } + + @Nullable + @Override + public ViewCreator getCreator() { + return null; + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/CanvasViewInflater.java b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/CanvasViewInflater.java new file mode 100644 index 00000000..1c5309b7 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/CanvasViewInflater.java @@ -0,0 +1,28 @@ +package com.stardust.autojs.core.ui.inflater.inflaters; + +import androidx.annotation.Nullable; + +import com.stardust.autojs.core.graphics.ScriptCanvasView; +import com.stardust.autojs.core.ui.inflater.ResourceParser; +import com.stardust.autojs.core.ui.inflater.ViewCreator; +import com.stardust.autojs.runtime.ScriptRuntime; + +/** + * Created by Stardust on 2018/3/16. + */ + +public class CanvasViewInflater extends BaseViewInflater { + + private ScriptRuntime mScriptRuntime; + + public CanvasViewInflater(ResourceParser resourceParser, ScriptRuntime runtime) { + super(resourceParser); + mScriptRuntime = runtime; + } + + @Nullable + @Override + public ViewCreator getCreator() { + return (context, attrs) -> new ScriptCanvasView(context, mScriptRuntime); + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/DatePickerInflater.java b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/DatePickerInflater.java new file mode 100644 index 00000000..760fd4d1 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/DatePickerInflater.java @@ -0,0 +1,98 @@ +package com.stardust.autojs.core.ui.inflater.inflaters; + +import android.os.Build; +import androidx.annotation.Nullable; +import android.view.InflateException; +import android.view.View; +import android.view.ViewGroup; +import android.widget.DatePicker; + + +import org.autojs.autojs6.R; +import com.stardust.autojs.core.ui.inflater.ResourceParser; +import com.stardust.autojs.core.ui.inflater.ViewCreator; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Locale; +import java.util.Map; + +/** + * Created by Stardust on 2017/11/29. + */ + +public class DatePickerInflater extends BaseViewInflater { + + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("mm/dd/yyyy", Locale.getDefault()); + + public DatePickerInflater(ResourceParser resourceParser) { + super(resourceParser); + } + + @Override + public boolean setAttr(DatePicker view, String attr, String value, ViewGroup parent, Map attrs) { + switch (attr) { + case "calendarTextColor": + Exceptions.unsupports(view, attr, value); + break; + case "calendarViewShown": + view.setCalendarViewShown(Boolean.parseBoolean(attr)); + break; + case "dayOfWeekBackground": + case "dayOfWeekTextAppearance": + case "endYear": + Exceptions.unsupports(view, attr, value); + break; + case "firstDayOfWeek": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + view.setFirstDayOfWeek(Integer.parseInt(value)); + } + break; + case "headerBackground": + case "headerDayOfMonthTextAppearance": + case "headerMonthTextAppearance": + case "headerYearTextAppearance": + Exceptions.unsupports(view, attr, value); + break; + case "maxDate": + try { + view.setMaxDate(DATE_FORMAT.parse(value).getTime()); + } catch (ParseException e) { + throw new InflateException(e); + } + break; + case "minDate": + try { + view.setMinDate(DATE_FORMAT.parse(value).getTime()); + } catch (ParseException e) { + throw new InflateException(e); + } + break; + case "spinnersShown": + view.setSpinnersShown(Boolean.parseBoolean(value)); + break; + case "startYear": + case "yearListItemTextAppearance": + case "yearListSelectorColor": + Exceptions.unsupports(view, attr, value); + break; + default: + return super.setAttr(view, attr, value, parent, attrs); + } + return true; + } + + @Nullable + @Override + public ViewCreator getCreator() { + return (context, attrs) -> { + String datePickerMode = attrs.remove("android:datePickerMode"); + if (datePickerMode == null || !datePickerMode.equals("spinner")) { + return new DatePicker(context); + } + DatePicker datePicker = (DatePicker) View.inflate(context, R.layout.date_picker_spinner, null); + datePicker.setCalendarViewShown(false); + return datePicker; + }; + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/Exceptions.java b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/Exceptions.java new file mode 100644 index 00000000..29635bf6 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/Exceptions.java @@ -0,0 +1,36 @@ +package com.stardust.autojs.core.ui.inflater.inflaters; + +import android.view.View; + +/** + * Created by Stardust on 2017/11/4. + */ + +public class Exceptions { + + public static final RuntimeException NO_EXCEPTION = new RuntimeException(); + + + public interface ExceptionHandler { + boolean handleUnsupportedException(UnsupportedOperationException e, View v, String attrName, String value); + } + + private static ExceptionHandler sExceptionHandler; + + public static void unsupports(View v, String name, String value) { + UnsupportedOperationException e = new UnsupportedOperationException(String.format("Attr %s:%s=\"%s\" is not supported", + v.getClass().getSimpleName(), name, value)); + if (sExceptionHandler == null || !sExceptionHandler.handleUnsupportedException(e, v, name, value)) { + throw e; + } + + } + + public static ExceptionHandler getExceptionHandler() { + return sExceptionHandler; + } + + public static void setExceptionHandler(ExceptionHandler exceptionHandler) { + sExceptionHandler = exceptionHandler; + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/FrameLayoutInflater.java b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/FrameLayoutInflater.java new file mode 100644 index 00000000..bfc678db --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/FrameLayoutInflater.java @@ -0,0 +1,45 @@ +package com.stardust.autojs.core.ui.inflater.inflaters; + +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import com.stardust.autojs.core.ui.inflater.ResourceParser; +import com.stardust.autojs.core.ui.inflater.util.Gravities; + +import java.util.Map; + +/** + * Created by Stardust on 2017/11/29. + */ + +public class FrameLayoutInflater extends ViewGroupInflater { + + private Integer mGravity; + + public FrameLayoutInflater(ResourceParser resourceParser) { + super(resourceParser); + } + + @Override + public boolean setAttr(V view, String attr, String value, ViewGroup parent, Map attrs) { + if (attr.equals("gravity")) { + mGravity = Gravities.parse(value); + return true; + } + return super.setAttr(view, attr, value, parent, attrs); + } + + @Override + public void applyPendingAttributesOfChildren(V view) { + if (mGravity == null) + return; + for (int i = 0; i < view.getChildCount(); i++) { + View child = view.getChildAt(i); + FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) child.getLayoutParams(); + params.gravity = mGravity; + child.setLayoutParams(params); + } + mGravity = null; + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/ImageViewInflater.java b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/ImageViewInflater.java new file mode 100644 index 00000000..1de52e9c --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/ImageViewInflater.java @@ -0,0 +1,108 @@ +package com.stardust.autojs.core.ui.inflater.inflaters; + +import android.os.Build; +import android.view.InflateException; +import android.view.ViewGroup; +import android.widget.ImageView; + +import com.stardust.autojs.core.ui.inflater.ResourceParser; +import com.stardust.autojs.core.ui.inflater.util.Colors; +import com.stardust.autojs.core.ui.inflater.util.Dimensions; + +import java.util.Map; + +/** + * Created by Stardust on 2017/11/3. + */ + +public class ImageViewInflater extends BaseViewInflater { + + public ImageViewInflater(ResourceParser resourceParser) { + super(resourceParser); + } + + @Override + public boolean setAttr(V view, String attr, String value, ViewGroup parent, Map attrs) { + if (super.setAttr(view, attr, value, parent, attrs)) { + return true; + } + switch (attr) { + case "adjustViewBounds": + view.setAdjustViewBounds(Boolean.valueOf(value)); + break; + case "baseline": + view.setBaseline(Dimensions.parseToIntPixel(value, view)); + break; + case "baselineAlignBottom": + view.setBaselineAlignBottom(Boolean.valueOf(value)); + break; + case "cropToPadding": + view.setCropToPadding(Boolean.valueOf(value)); + break; + case "maxHeight": + view.setMaxHeight(Dimensions.parseToIntPixel(value, view)); + break; + case "maxWidth": + view.setMaxWidth(Dimensions.parseToIntPixel(value, view)); + break; + case "path": + getDrawables().setupWithImage(view, wrapAsPath(value)); + break; + case "scaleType": + view.setScaleType(parseScaleType(value)); + break; + case "src": + getDrawables().setupWithImage(view, value); + break; + case "tint": + view.setColorFilter(Colors.parse(view, value)); + break; + case "tintMode": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + view.setImageTintMode(TINT_MODES.get(value)); + } + break; + case "url": + getDrawables().setupWithImage(view, wrapAsUrl(value)); + break; + default: + return false; + } + return true; + } + private String wrapAsPath(String value) { + if (!value.startsWith("file://")) { + return "file://" + value; + } + return value; + } + + private String wrapAsUrl(String value) { + if (!value.startsWith("http://") && !value.startsWith("https://")) { + return "http://" + value; + } + return value; + } + + private ImageView.ScaleType parseScaleType(String value) { + switch (value.toLowerCase()) { + case "center": + return ImageView.ScaleType.CENTER; + case "center_crop": + return ImageView.ScaleType.CENTER_CROP; + case "center_inside": + return ImageView.ScaleType.CENTER_INSIDE; + case "fit_center": + return ImageView.ScaleType.FIT_CENTER; + case "fit_end": + return ImageView.ScaleType.FIT_END; + case "fit_start": + return ImageView.ScaleType.FIT_START; + case "fit_xy": + return ImageView.ScaleType.FIT_XY; + case "matrix": + return ImageView.ScaleType.MATRIX; + } + throw new InflateException("unknown scale type: " + value); + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/JsGridViewInflater.java b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/JsGridViewInflater.java new file mode 100644 index 00000000..6136d65e --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/JsGridViewInflater.java @@ -0,0 +1,46 @@ +package com.stardust.autojs.core.ui.inflater.inflaters; + +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.GridLayoutManager; + +import android.view.ViewGroup; + +import com.stardust.autojs.core.ui.inflater.ResourceParser; +import com.stardust.autojs.core.ui.inflater.ViewCreator; +import com.stardust.autojs.core.ui.widget.JsGridView; +import com.stardust.autojs.core.ui.widget.JsListView; +import com.stardust.autojs.runtime.ScriptRuntime; + +import java.util.Map; + +/** + * Created by Stardust on 2018/3/30. + */ + +public class JsGridViewInflater extends JsListViewInflater { + + public JsGridViewInflater(ResourceParser resourceParser, ScriptRuntime runtime) { + super(resourceParser, runtime); + } + + @Override + public boolean setAttr(V view, String attr, String value, ViewGroup parent, Map attrs) { + switch (attr) { + case "orientation": + ((GridLayoutManager) view.getLayoutManager()).setOrientation(LinearLayoutInflater.ORIENTATIONS.get(value)); + return true; + case "spanCount": + ((GridLayoutManager) view.getLayoutManager()).setSpanCount(Integer.parseInt(value)); + return true; + default: + return super.setAttr(view, attr, value, parent, attrs); + } + } + + + @Nullable + @Override + public ViewCreator getCreator() { + return (context, attrs) -> new JsGridView(context, getRuntime()); + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/JsImageViewInflater.java b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/JsImageViewInflater.java new file mode 100644 index 00000000..9e0a4e80 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/JsImageViewInflater.java @@ -0,0 +1,84 @@ +package com.stardust.autojs.core.ui.inflater.inflaters; + +import androidx.annotation.Nullable; +import android.view.ViewGroup; +import android.widget.ImageView.ScaleType; + +import com.makeramen.roundedimageview.Corner; +import com.stardust.autojs.core.ui.inflater.ResourceParser; +import com.stardust.autojs.core.ui.inflater.ViewCreator; +import com.stardust.autojs.core.ui.inflater.util.Colors; +import com.stardust.autojs.core.ui.inflater.util.Dimensions; +import com.stardust.autojs.core.ui.inflater.util.ValueMapper; +import com.stardust.autojs.core.ui.widget.JsImageView; + + +import java.util.Map; + +/** + * Created by Stardust on 2017/11/30. + */ + +public class JsImageViewInflater extends ImageViewInflater { + + + protected static final ValueMapper SCALE_TYPES = new ValueMapper("scaleType") + .map("center", ScaleType.CENTER) + .map("centerCrop", ScaleType.CENTER_CROP) + .map("centerInside", ScaleType.CENTER_INSIDE) + .map("fitCenter", ScaleType.FIT_CENTER) + .map("fitEnd", ScaleType.FIT_END) + .map("fitStart", ScaleType.FIT_START) + .map("fitXY", ScaleType.FIT_XY) + .map("matrix", ScaleType.MATRIX); + + public JsImageViewInflater(ResourceParser resourceParser) { + super(resourceParser); + } + + @Override + public boolean setAttr(JsImageView view, String attr, String value, ViewGroup parent, Map attrs) { + switch (attr) { + case "radius": + view.setCornerRadius(Dimensions.parseToPixel(value, view)); + break; + case "radiusTopLeft": + view.setCornerRadius(Dimensions.parseToPixel(value, view), view.getCornerRadius(Corner.TOP_RIGHT), view.getCornerRadius(Corner.BOTTOM_LEFT), view.getCornerRadius(Corner.BOTTOM_RIGHT)); + break; + case "radiusTopRight": + view.setCornerRadius(view.getCornerRadius(Corner.TOP_LEFT), Dimensions.parseToPixel(value, view), view.getCornerRadius(Corner.BOTTOM_LEFT), view.getCornerRadius(Corner.BOTTOM_RIGHT)); + break; + case "radiusBottomLeft": + view.setCornerRadius(view.getCornerRadius(Corner.TOP_LEFT), view.getCornerRadius(Corner.TOP_RIGHT), Dimensions.parseToPixel(value, view), view.getCornerRadius(Corner.BOTTOM_RIGHT)); + break; + case "radiusBottomRight": + view.setCornerRadius(view.getCornerRadius(Corner.TOP_LEFT), view.getCornerRadius(Corner.TOP_RIGHT), view.getCornerRadius(Corner.BOTTOM_LEFT), Dimensions.parseToPixel(value, view)); + break; + case "borderWidth": + view.setBorderWidth(Dimensions.parseToPixel(value, view)); + break; + case "borderColor": + view.setBorderColor(Colors.parse(view, value)); + break; + case "circle": + view.setCircle(true); + break; + case "scaleType": + view.setScaleType(SCALE_TYPES.get(value)); + break; + default: + return super.setAttr(view, attr, value, parent, attrs); + } + return true; + } + + @Nullable + @Override + public ViewCreator getCreator() { + return (context, attrs) -> { + JsImageView imageView = new JsImageView(context); + imageView.setDrawables(getDrawables()); + return imageView; + }; + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/JsListViewInflater.java b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/JsListViewInflater.java new file mode 100644 index 00000000..fab6ee1b --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/JsListViewInflater.java @@ -0,0 +1,68 @@ +package com.stardust.autojs.core.ui.inflater.inflaters; + +import androidx.annotation.Nullable; +import android.view.ViewGroup; + +import com.stardust.autojs.core.ui.inflater.DynamicLayoutInflater; +import com.stardust.autojs.core.ui.inflater.ResourceParser; +import com.stardust.autojs.core.ui.inflater.ViewCreator; +import com.stardust.autojs.core.ui.widget.JsListView; +import com.stardust.autojs.runtime.ScriptRuntime; +import com.stardust.autojs.workground.WrapContentLinearLayoutManager; + +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.util.Map; + +/** + * Created by Stardust on 2018/3/28. + */ + +public class JsListViewInflater extends BaseViewInflater { + + private final ScriptRuntime mRuntime; + + public JsListViewInflater(ResourceParser resourceParser, ScriptRuntime runtime) { + super(resourceParser); + mRuntime = runtime; + } + + public ScriptRuntime getRuntime() { + return mRuntime; + } + + @Override + public boolean setAttr(V view, String attr, String value, ViewGroup parent, Map attrs) { + switch (attr) { + case "orientation": + view.setLayoutManager(new WrapContentLinearLayoutManager(view.getContext(), LinearLayoutInflater.ORIENTATIONS.get(value), false)); + return true; + default: + return super.setAttr(view, attr, value, parent, attrs); + } + } + + @Override + public boolean setAttr(V view, String ns, String attrName, String value, ViewGroup parent, Map attrs) { + return super.setAttr(view, ns, attrName, value, parent, attrs); + } + + @Override + public boolean inflateChildren(DynamicLayoutInflater inflater, Node node, JsListView parent) { + NodeList nodeList = node.getChildNodes(); + for (int i = 0; i < nodeList.getLength(); i++) { + Node child = nodeList.item(i); + if (child.getNodeType() != Node.ELEMENT_NODE) continue; + parent.setItemTemplate(inflater, child); + return true; + } + return false; + } + + @Nullable + @Override + public ViewCreator getCreator() { + return (context, attrs) -> new JsListView(context, mRuntime); + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/LinearLayoutInflater.java b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/LinearLayoutInflater.java new file mode 100644 index 00000000..fd150e45 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/LinearLayoutInflater.java @@ -0,0 +1,64 @@ +package com.stardust.autojs.core.ui.inflater.inflaters; + +import android.view.ViewGroup; +import android.widget.LinearLayout; + +import com.stardust.autojs.core.ui.inflater.ResourceParser; +import com.stardust.autojs.core.ui.inflater.util.Gravities; +import com.stardust.autojs.core.ui.inflater.util.ValueMapper; + +import java.util.Map; + +/** + * Created by Stardust on 2017/11/4. + */ + +public class LinearLayoutInflater extends ViewGroupInflater { + + static final ValueMapper ORIENTATIONS = new ValueMapper("orientation") + .map("vertical", LinearLayout.VERTICAL) + .map("horizontal", LinearLayout.HORIZONTAL); + + static final ValueMapper SHOW_DIVIDERS = new ValueMapper("showDividers") + .map("beginning", LinearLayout.SHOW_DIVIDER_BEGINNING) + .map("middle", LinearLayout.SHOW_DIVIDER_MIDDLE) + .map("end", LinearLayout.SHOW_DIVIDER_END) + .map("none", LinearLayout.SHOW_DIVIDER_NONE); + + public LinearLayoutInflater(ResourceParser resourceParser) { + super(resourceParser); + } + + @Override + public boolean setAttr(V view, String attr, String value, ViewGroup parent, Map attrs) { + switch (attr) { + case "baselineAligned": + view.setBaselineAligned(Boolean.valueOf(value)); + break; + case "baselineAlignedChildIndex": + view.setBaselineAlignedChildIndex(Integer.valueOf(value)); + break; + case "divider": + view.setDividerDrawable(getDrawables().parse(view, value)); + break; + case "gravity": + view.setGravity(Gravities.parse(value)); + break; + case "measureWithLargestChild": + view.setMeasureWithLargestChildEnabled(Boolean.valueOf(value)); + break; + case "orientation": + view.setOrientation(ORIENTATIONS.get(value)); + break; + case "showDividers": + view.setShowDividers(SHOW_DIVIDERS.split(value)); + break; + case "weightSum": + view.setWeightSum(Float.valueOf(value)); + break; + default: + return super.setAttr(view, attr, value, parent, attrs); + } + return true; + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/MapAttrbuteSet.java b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/MapAttrbuteSet.java new file mode 100644 index 00000000..e2b4d90d --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/MapAttrbuteSet.java @@ -0,0 +1,138 @@ +package com.stardust.autojs.core.ui.inflater.inflaters; + +import android.util.AttributeSet; + +import java.util.ArrayList; +import java.util.Map; + +/** + * Created by Stardust on 2017/11/5. + */ + +public class MapAttrbuteSet implements AttributeSet { + private Map mMap; + private ArrayList mKeys; + private ArrayList mValues; + + public MapAttrbuteSet(Map map) { + mMap = map; + mKeys = new ArrayList<>(map.size()); + mValues = new ArrayList<>(map.size()); + for (Map.Entry entry : map.entrySet()) { + mKeys.add(entry.getKey()); + mValues.add(entry.getValue()); + } + } + + @Override + public int getAttributeCount() { + return mKeys.size(); + } + + @Override + public String getAttributeName(int index) { + return mKeys.get(index); + } + + @Override + public String getAttributeValue(int index) { + return mValues.get(index); + } + + @Override + public String getAttributeValue(String namespace, String name) { + return mMap.get(name); + } + + @Override + public String getPositionDescription() { + return null; + } + + @Override + public int getAttributeNameResource(int index) { + return 0; + } + + @Override + public int getAttributeListValue(String namespace, String attribute, String[] options, int defaultValue) { + return 0; + } + + @Override + public boolean getAttributeBooleanValue(String namespace, String attribute, boolean defaultValue) { + String v = mMap.get(attribute); + return v == null ? defaultValue : Boolean.valueOf(v); + + } + + @Override + public int getAttributeResourceValue(String namespace, String attribute, int defaultValue) { + return 0; + } + + @Override + public int getAttributeIntValue(String namespace, String attribute, int defaultValue) { + return 0; + } + + @Override + public int getAttributeUnsignedIntValue(String namespace, String attribute, int defaultValue) { + return 0; + } + + @Override + public float getAttributeFloatValue(String namespace, String attribute, float defaultValue) { + return 0; + } + + @Override + public int getAttributeListValue(int index, String[] options, int defaultValue) { + return 0; + } + + @Override + public boolean getAttributeBooleanValue(int index, boolean defaultValue) { + return false; + } + + @Override + public int getAttributeResourceValue(int index, int defaultValue) { + return 0; + } + + @Override + public int getAttributeIntValue(int index, int defaultValue) { + return 0; + } + + @Override + public int getAttributeUnsignedIntValue(int index, int defaultValue) { + return 0; + } + + @Override + public float getAttributeFloatValue(int index, float defaultValue) { + return 0; + } + + @Override + public String getIdAttribute() { + return null; + } + + @Override + public String getClassAttribute() { + return null; + } + + @Override + public int getIdAttributeResourceValue(int defaultValue) { + return 0; + } + + @Override + public int getStyleAttribute() { + return 0; + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/ProgressBarInflater.java b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/ProgressBarInflater.java new file mode 100644 index 00000000..a9cc93c1 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/ProgressBarInflater.java @@ -0,0 +1,123 @@ +package com.stardust.autojs.core.ui.inflater.inflaters; + +import android.content.res.ColorStateList; +import android.os.Build; +import android.view.ViewGroup; +import android.widget.ProgressBar; + +import com.stardust.autojs.core.ui.inflater.ResourceParser; +import com.stardust.autojs.core.ui.inflater.util.Colors; +import com.stardust.autojs.core.ui.inflater.util.Dimensions; + +import java.util.Map; + +/** + * Created by Stardust on 2017/11/29. + */ + +public class ProgressBarInflater extends BaseViewInflater { + + public ProgressBarInflater(ResourceParser resourceParser) { + super(resourceParser); + } + + @Override + public boolean setAttr(V view, String attr, String value, ViewGroup parent, Map attrs) { + switch (attr) { + case "animationResolution": + Exceptions.unsupports(view, attr, value); + break; + case "indeterminate": + view.setIndeterminate(Boolean.parseBoolean(value)); + break; + case "indeterminateBehavior": + Exceptions.unsupports(view, attr, value); + break; + case "indeterminateDrawable": + view.setIndeterminateDrawable(getDrawables().parse(view, value)); + break; + case "indeterminateDuration": + Exceptions.unsupports(view, attr, value); + break; + case "indeterminateOnly": + Exceptions.unsupports(view, attr, value); + break; + case "indeterminateTint": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + view.setIndeterminateTintList(ColorStateList.valueOf(Colors.parse(view, value))); + } + break; + case "indeterminateTintMode": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + view.setIndeterminateTintMode(TINT_MODES.get(value)); + } + break; + case "interpolato": + Exceptions.unsupports(view, attr, value); + break; + case "max": + view.setMax(Integer.parseInt(value)); + break; + case "maxHeight": + Exceptions.unsupports(view, attr, value); + break; + case "maxWidth": + Exceptions.unsupports(view, attr, value); + break; + case "min": + Exceptions.unsupports(view, attr, value); + break; + case "minHeigh": + view.setMinimumHeight(Dimensions.parseToIntPixel(value, view)); + break; + case "minWidth": + view.setMinimumWidth(Dimensions.parseToIntPixel(value, view)); + break; + case "mirrorForRtl": + Exceptions.unsupports(view, attr, value); + break; + case "progress": + view.setProgress(Integer.parseInt(value)); + break; + case "progressBackgroundTint": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + view.setProgressBackgroundTintList(ColorStateList.valueOf(Colors.parse(view, value))); + } + break; + case "progressBackgroundTintMode": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + view.setProgressBackgroundTintMode(TINT_MODES.get(value)); + } + break; + case "progressDrawable": + view.setProgressDrawable(getDrawables().parse(view, value)); + break; + case "progressTint": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + view.setProgressTintList(ColorStateList.valueOf(Colors.parse(view, value))); + } + break; + case "progressTintMode": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + view.setProgressTintMode(TINT_MODES.get(value)); + } + break; + case "secondaryProgress": + view.setSecondaryProgress(Integer.parseInt(value)); + break; + case "secondaryProgressTint": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + view.setSecondaryProgressTintList(ColorStateList.valueOf(Colors.parse(view, value))); + } + break; + case "secondaryProgressTintMode": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + view.setSecondaryProgressTintMode(TINT_MODES.get(value)); + } + break; + default: + return super.setAttr(view, attr, value, parent, attrs); + } + return true; + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/RadioGroupInflater.java b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/RadioGroupInflater.java new file mode 100644 index 00000000..3ecb5e45 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/RadioGroupInflater.java @@ -0,0 +1,40 @@ +package com.stardust.autojs.core.ui.inflater.inflaters; + +import android.view.ViewGroup; +import android.widget.RadioGroup; + +import com.stardust.autojs.core.ui.inflater.ResourceParser; +import com.stardust.autojs.core.ui.inflater.util.Ids; + +import java.util.Map; + +/** + * Created by Stardust on 2017/11/29. + */ + +public class RadioGroupInflater extends LinearLayoutInflater { + + private Integer mCheckedButton; + + public RadioGroupInflater(ResourceParser resourceParser) { + super(resourceParser); + } + + @Override + public boolean setAttr(V view, String attr, String value, ViewGroup parent, Map attrs) { + if (attr.equals("checkedButton")) { + mCheckedButton = Ids.parse(value); + return true; + } else { + return super.setAttr(view, attr, value, parent, attrs); + } + } + + @Override + public void applyPendingAttributesOfChildren(V view) { + if (mCheckedButton != null) { + view.check(mCheckedButton); + mCheckedButton = null; + } + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/SpinnerInflater.java b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/SpinnerInflater.java new file mode 100644 index 00000000..bf6c486f --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/SpinnerInflater.java @@ -0,0 +1,95 @@ +package com.stardust.autojs.core.ui.inflater.inflaters; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import android.view.ViewGroup; +import android.widget.Spinner; + +import com.stardust.autojs.core.ui.inflater.ResourceParser; +import com.stardust.autojs.core.ui.inflater.ViewCreator; +import com.stardust.autojs.core.ui.inflater.util.Colors; +import com.stardust.autojs.core.ui.inflater.util.Dimensions; +import com.stardust.autojs.core.ui.inflater.util.Strings; +import com.stardust.autojs.core.ui.inflater.util.ValueMapper; +import com.stardust.autojs.core.ui.widget.JsSpinner; + +import java.util.Map; + +/** + * Created by Stardust on 2017/11/29. + */ + +public class SpinnerInflater extends BaseViewInflater { + + protected static final ValueMapper SPINNER_MODES = new ValueMapper("spinnerMode") + .map("dialog", Spinner.MODE_DIALOG) + .map("dropdown", Spinner.MODE_DROPDOWN); + + public SpinnerInflater(ResourceParser resourceParser) { + super(resourceParser); + } + + @Override + public boolean setAttr(JsSpinner view, String attr, String value, ViewGroup parent, Map attrs) { + switch (attr) { + case "dropDownHorizontalOffset": + view.setDropDownHorizontalOffset(Dimensions.parseToIntPixel(value, view)); + break; + case "dropDownSelector": + Exceptions.unsupports(view, attr, value); + break; + case "dropDownVerticalOffset": + view.setDropDownVerticalOffset(Dimensions.parseToIntPixel(value, view)); + break; + case "dropDownWidth": + view.setDropDownWidth(Dimensions.parseToIntPixel(value, view)); + break; + case "popupBackground": + view.setPopupBackgroundDrawable(getDrawables().parse(view, value)); + break; + case "prompt": + view.setPrompt(Strings.parse(view, value)); + break; + case "entries": + view.setAdapter(view.new Adapter(view.getContext(), + android.R.layout.simple_spinner_dropdown_item, value.split("[|]"))); + break; + case "textStyle": + view.setTextStyle(TextViewInflater.TEXT_STYLES.split(value)); + break; + case "textColor": + view.setTextColor(Colors.parse(view.getContext(), value)); + break; + case "textSize": + view.setTextSize(Dimensions.parseToPixel(value, view)); + break; + case "entryTextStyle": + view.setEntryTextStyle(TextViewInflater.TEXT_STYLES.split(value)); + break; + case "entryTextColor": + view.setEntryTextColor(Colors.parse(view.getContext(), value)); + break; + case "entryTextSize": + view.setEntryTextSize(Dimensions.parseToPixel(value, view)); + break; + default: + return super.setAttr(view, attr, value, parent, attrs); + } + return true; + } + + @Nullable + @Override + public ViewCreator getCreator() { + return (context, attrs) -> { + String mode = attrs.remove("android:spinnerMode"); + if (mode == null) { + return new JsSpinner(context); + } + return new JsSpinner(context, SPINNER_MODES.get(mode)); + }; + } + + +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/TabLayoutInflater.java b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/TabLayoutInflater.java new file mode 100644 index 00000000..600f2ad0 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/TabLayoutInflater.java @@ -0,0 +1,68 @@ +package com.stardust.autojs.core.ui.inflater.inflaters; + +import android.content.res.ColorStateList; +import android.graphics.Color; +import androidx.annotation.Nullable; +import android.view.View; +import android.view.ViewGroup; + +import com.google.android.material.tabs.TabLayout; +import org.autojs.autojs6.R; +import com.stardust.autojs.core.ui.inflater.ResourceParser; +import com.stardust.autojs.core.ui.inflater.ViewCreator; +import com.stardust.autojs.core.ui.inflater.util.Colors; +import com.stardust.autojs.core.ui.inflater.util.Dimensions; +import com.stardust.autojs.core.ui.inflater.util.Gravities; +import com.stardust.autojs.core.ui.inflater.util.ValueMapper; + +import java.util.Map; + +public class TabLayoutInflater extends BaseViewInflater { + + private static final ValueMapper TAB_MODES = new ValueMapper("tabMode") + .map("fixed", TabLayout.MODE_FIXED) + .map("scrollable", TabLayout.MODE_SCROLLABLE); + + public TabLayoutInflater(ResourceParser resourceParser) { + super(resourceParser); + } + + @Override + public boolean setAttr(V view, String attr, String value, ViewGroup parent, Map attrs) { + switch (attr) { + case "tabGravity": + view.setTabGravity(Gravities.parse(value)); + break; + case "tabIndicatorColor": + view.setSelectedTabIndicatorColor(Colors.parse(view, value)); + break; + case "tabIndicatorHeight": + view.setSelectedTabIndicatorHeight(Dimensions.parseToIntPixel(value, view)); + break; + case "tabMode": + view.setTabMode(TAB_MODES.get(value)); + break; + case "tabSelectedTextColor": + ColorStateList colors = view.getTabTextColors(); + view.setTabTextColors(colors == null ? Color.WHITE : colors.getDefaultColor(), + Colors.parse(view, value)); + break; + case "tabTextColor": + colors = view.getTabTextColors(); + view.setTabTextColors(Colors.parse(view, value), + colors == null ? Color.WHITE : colors.getDefaultColor()); + break; + default: + return super.setAttr(view, attr, value, parent, attrs); + + } + return true; + } + + @Nullable + @Override + @SuppressWarnings("unchecked") + public ViewCreator getCreator() { + return (ViewCreator) (context, attrs) -> (V) View.inflate(context, R.layout.js_tablayout, null); + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/TextViewInflater.java b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/TextViewInflater.java new file mode 100644 index 00000000..fc3128fb --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/TextViewInflater.java @@ -0,0 +1,428 @@ +package com.stardust.autojs.core.ui.inflater.inflaters; + +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.text.InputFilter; +import android.text.InputType; +import android.text.TextUtils; +import android.text.method.DigitsKeyListener; +import android.text.method.TextKeyListener; +import android.text.util.Linkify; +import android.util.TypedValue; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.widget.TextView; + +import com.stardust.autojs.core.ui.inflater.ResourceParser; +import com.stardust.autojs.core.ui.inflater.util.Colors; +import com.stardust.autojs.core.ui.inflater.util.Dimensions; +import com.stardust.autojs.core.ui.inflater.util.Gravities; +import com.stardust.autojs.core.ui.inflater.util.Res; +import com.stardust.autojs.core.ui.inflater.util.Strings; +import com.stardust.autojs.core.ui.inflater.util.ValueMapper; + +import java.util.Map; + +/** + * Created by Stardust on 2017/11/3. + */ + +public class TextViewInflater extends BaseViewInflater { + + private static final int LEFT = 0; + private static final int TOP = 1; + private static final int RIGHT = 2; + private static final int BOTTOM = 3; + + + private static final ValueMapper AUTO_LINK_MASKS = new ValueMapper("autoLink") + .map("all", Linkify.ALL) + .map("email", Linkify.EMAIL_ADDRESSES) + .map("map", Linkify.MAP_ADDRESSES) + .map("none", 0) + .map("phone", Linkify.PHONE_NUMBERS) + .map("web", Linkify.WEB_URLS); + private static final ValueMapper ELLIPSIZE = new ValueMapper("ellipsize") + .map("end", TextUtils.TruncateAt.END) + .map("marquee", TextUtils.TruncateAt.MARQUEE) + .map("none", null) + .map("start", TextUtils.TruncateAt.START) + .map("middle", TextUtils.TruncateAt.MIDDLE); + private static final ValueMapper HYPHENATION_FREQUENCY = new ValueMapper("hyphenationFrequency") + .map("full", 2) + .map("none", 0) + .map("normal", 1); + + // TODO: 2017/11/4 IME FLAG + private static final ValueMapper IME_OPTIONS = new ValueMapper("imeOptions") + .map("actionDone", EditorInfo.IME_ACTION_DONE) + .map("actionGo", EditorInfo.IME_ACTION_DONE) + .map("actionNext", EditorInfo.IME_ACTION_DONE) + .map("actionNone", EditorInfo.IME_ACTION_DONE) + .map("actionPrevious", EditorInfo.IME_ACTION_DONE) + .map("actionSearch", EditorInfo.IME_ACTION_DONE) + .map("actionSend", EditorInfo.IME_ACTION_DONE) + .map("actionUnspecified", EditorInfo.IME_ACTION_DONE); + + private static final ValueMapper INPUT_TYPES = new ValueMapper("inputType") + .map("date", 0x14) + .map("datetime", 0x4) + .map("none", 0x0) + .map("number", 0x2) + .map("numberDecimal", 0x2002) + .map("numberPassword", 0x12) + .map("numberSigned", 0x1002) + .map("phone", 0x3) + .map("text", 0x1) + .map("textAutoComplete", 0x10001) + .map("textAutoCorrect", 0x8001) + .map("textCapCharacters", 0x1001) + .map("textCapSentences", 0x4001) + .map("textCapWords", 0x2001) + .map("textEmailAddress", 0x21) + .map("textEmailSubject", 0x31) + .map("textFilter", 0xb1) + .map("textImeMultiLine", 0x40001) + .map("textLongMessage", 0x51) + .map("textMultiLine", 0x20001) + .map("textNoSuggestions", 0x80001) + .map("textPassword", 0x81) + .map("textPersonName", 0x61) + .map("textPhonetic", 0xc1) + .map("textPostalAddress", 0x71) + .map("textShortMessage", 0x41) + .map("textUri", 0x11) + .map("textVisiblePassword", 0x91) + .map("textWebEditText", 0xa1) + .map("textWebEmailAddress", 0xd1) + .map("textWebPassword", 0xe1) + .map("time", 0x24); + + private static final ValueMapper INPUT_TYPE_NUMERIC = new ValueMapper("numeric") + .map("decimal", InputType.TYPE_NUMBER_FLAG_DECIMAL) + .map("number", InputType.TYPE_CLASS_NUMBER) + .map("signed", InputType.TYPE_NUMBER_FLAG_SIGNED); + + static final ValueMapper TEXT_STYLES = new ValueMapper("textStyle") + .map("bold", Typeface.BOLD) + .map("italic", Typeface.ITALIC) + .map("normal", Typeface.NORMAL); + + private static final ValueMapper CAPITALIZE = new ValueMapper("capitalize") + .map("characters", TextKeyListener.Capitalize.CHARACTERS) + .map("none", TextKeyListener.Capitalize.NONE) + .map("sentences", TextKeyListener.Capitalize.SENTENCES) + .map("words", TextKeyListener.Capitalize.WORDS); + + + private boolean mAutoText; + private TextKeyListener.Capitalize mCapitalize; + private Drawable mDrawableBottom; + private Drawable mDrawableRight; + private Drawable mDrawableTop; + private Drawable mDrawableLeft; + private Integer mLineSpacingExtra; + private Integer mLineSpacingMultiplier; + private String mFontFamily; + private Integer mTextStyle; + private String mTypeface; + + public TextViewInflater(ResourceParser resourceParser) { + super(resourceParser); + } + + @Override + public boolean setAttr(V view, String attrName, String value, ViewGroup parent, Map attrs) { + if (super.setAttr(view, attrName, value, parent, attrs)) { + return true; + } + switch (attrName) { + case "autoLink": + view.setAutoLinkMask(AUTO_LINK_MASKS.get(value)); + break; + case "autoText": + mAutoText = Boolean.valueOf(value); + break; + case "capitalize": + mCapitalize = CAPITALIZE.get(value); + break; + case "cursorVisible": + view.setCursorVisible(Boolean.valueOf(value)); + break; + case "digit": + if (value.equals("true")) { + view.setKeyListener(DigitsKeyListener.getInstance()); + } else if (!value.equals("false")) { + view.setKeyListener(DigitsKeyListener.getInstance(value)); + } + break; + case "drawableBottom": + mDrawableBottom = getDrawables().parse(view, value); + break; + case "drawableTop": + mDrawableTop = getDrawables().parse(view, value); + break; + case "drawableLeft": + mDrawableLeft = getDrawables().parse(view, value); + break; + case "drawableRight": + mDrawableRight = getDrawables().parse(view, value); + break; + case "drawablePadding": + view.setCompoundDrawablePadding(Dimensions.parseToIntPixel(value, view)); + break; + case "drawableStart": + case "drawableEnd": + case "editable": + case "editorExtras": + Exceptions.unsupports(view, attrName, value); + case "elegantTextHeight": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + view.setElegantTextHeight(Boolean.valueOf(value)); + } + break; + case "ellipsize": + TextUtils.TruncateAt e = ELLIPSIZE.get(value); + if (e != null) { + view.setEllipsize(e); + } + break; + case "ems": + view.setEms(Integer.valueOf(value)); + break; + case "fontFamily": + mFontFamily = value; + break; + case "fontFeatureSettings": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + view.setFontFeatureSettings(value); + } + break; + case "freezesText": + view.setFreezesText(Boolean.valueOf(value)); + break; + case "gravity": + view.setGravity(Gravities.parse(value)); + break; + case "hint": + view.setHint(Strings.parse(view, value)); + break; + case "hyphenationFrequency": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + view.setHyphenationFrequency(HYPHENATION_FREQUENCY.get(value)); + } + break; + case "imeActionId": + view.setImeActionLabel(view.getImeActionLabel(), Integer.valueOf(value)); + break; + case "imeActionLabel": + view.setImeActionLabel(value, view.getImeActionId()); + break; + case "imeOptions": + view.setImeOptions(IME_OPTIONS.split(value)); + break; + case "includeFontPadding": + view.setIncludeFontPadding(Boolean.valueOf(value)); + break; + case "inputMethod": + Exceptions.unsupports(view, attrName, value); + case "inputType": + view.setInputType(INPUT_TYPES.split(value)); + break; + case "letterSpacing": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + view.setLetterSpacing(Float.valueOf(value)); + } + break; + case "lineSpacingExtra": + view.setLineSpacing(Dimensions.parseToIntPixel(value, view), view.getLineSpacingMultiplier()); + break; + case "lineSpacingMultiplier": + view.setLineSpacing(view.getLineSpacingExtra(), Dimensions.parseToIntPixel(value, view)); + break; + case "lines": + view.setLines(Integer.valueOf(value)); + break; + case "linksClickable": + view.setLinksClickable(Boolean.valueOf(value)); + break; + case "marqueeRepeatLimit": + view.setMarqueeRepeatLimit(value.equals("marquee_forever") ? Integer.MAX_VALUE : Integer.valueOf(value)); + break; + case "maxEms": + view.setMaxEms(Integer.valueOf(value)); + break; + case "maxHeight": + view.setMaxHeight(Integer.valueOf(value)); + break; + case "maxLength": + view.setFilters(new InputFilter[]{new InputFilter.LengthFilter(Integer.valueOf(value))}); + break; + case "maxLines": + view.setMaxLines(Integer.valueOf(value)); + break; + case "maxWidth": + view.setMaxWidth(Integer.valueOf(value)); + break; + case "minEms": + view.setMinEms(Integer.valueOf(value)); + break; + case "minHeight": + view.setMinHeight(Dimensions.parseToIntPixel(value, view)); + break; + case "minLines": + view.setMinLines(Integer.valueOf(value)); + break; + case "minWidth": + view.setMinWidth(Dimensions.parseToIntPixel(value, view)); + break; + case "numeric": + view.setInputType(INPUT_TYPE_NUMERIC.split(value) | InputType.TYPE_CLASS_NUMBER); + break; + case "password": + if (value.equals("true")) { + view.setInputType(view.getInputType() | InputType.TYPE_TEXT_VARIATION_PASSWORD); + } + break; + case "phoneNumber": + if (value.equals("true")) { + view.setInputType(view.getInputType() | InputType.TYPE_TEXT_VARIATION_PHONETIC); + } + break; + case "privateImeOptions": + view.setPrivateImeOptions(Strings.parse(view, value)); + break; + case "scrollHorizontally": + view.setHorizontallyScrolling(Boolean.valueOf(value)); + break; + case "selectAllOnFocus": + view.setSelectAllOnFocus(Boolean.valueOf(value)); + break; + case "shadowColor": + view.setShadowLayer(view.getShadowRadius(), view.getShadowDx(), view.getShadowDy(), Colors.parse(view, value)); + break; + case "shadowDx": + view.setShadowLayer(view.getShadowRadius(), Dimensions.parseToPixel(value, view), view.getShadowDy(), view.getShadowColor()); + break; + case "shadowDy": + view.setShadowLayer(view.getShadowRadius(), view.getShadowDx(), Dimensions.parseToPixel(value, view), view.getShadowColor()); + break; + case "shadowRadius": + view.setShadowLayer(Dimensions.parseToPixel(value, view), view.getShadowDx(), view.getShadowDy(), view.getShadowColor()); + break; + case "singleLine": + view.setSingleLine(Boolean.valueOf(value)); + break; + case "textAllCaps": + view.setAllCaps(Boolean.valueOf(value)); + break; + case "textAppearance": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + view.setTextAppearance(Res.parseStyle(view, value)); + } + break; + case "textColorHighlight": + view.setHighlightColor(Colors.parse(view, value)); + break; + case "textColorHint": + view.setHintTextColor(Colors.parse(view, value)); + break; + case "textColorLink": + view.setLinkTextColor(Colors.parse(view, value)); + break; + case "textIsSelectable": + view.setTextIsSelectable(Boolean.valueOf(value)); + break; + case "textScaleX": + view.setTextScaleX(Dimensions.parseToPixel(value, view)); + break; + case "textStyle": + mTextStyle = TEXT_STYLES.split(value); + break; + case "typeface": + mTypeface = value; + break; + case "text": + view.setText(Strings.parse(view, value)); + break; + case "color": + case "textColor": + view.setTextColor(Colors.parse(view.getContext(), value)); + break; + case "size": + case "textSize": + view.setTextSize(TypedValue.COMPLEX_UNIT_PX, Dimensions.parseToPixel(value, view)); + break; + default: + return false; + } + return true; + } + + @Override + public void applyPendingAttributes(V view, ViewGroup parent) { + setTypeface(view); + setLineSpacing(view); + setDrawables(view); + setKeyListener(view); + } + + private void setKeyListener(V view) { + if (mCapitalize != null) { + view.setKeyListener(TextKeyListener.getInstance(mAutoText, mCapitalize)); + } + mCapitalize = null; + mAutoText = false; + } + + private void setDrawable(V view, int d) { + Drawable[] drawables = view.getCompoundDrawables(); + view.setCompoundDrawables( + mDrawableLeft != null ? mDrawableLeft : drawables[0], + mDrawableTop != null ? mDrawableTop : drawables[1], + mDrawableRight != null ? mDrawableRight : drawables[2], + mDrawableBottom != null ? mDrawableBottom : drawables[3] + ); + mDrawableLeft = mDrawableBottom = mDrawableRight = mDrawableTop = null; + } + + + private void setDrawables(V view) { + Drawable[] drawables = view.getCompoundDrawables(); + view.setCompoundDrawables( + mDrawableLeft != null ? mDrawableLeft : drawables[0], + mDrawableTop != null ? mDrawableTop : drawables[1], + mDrawableRight != null ? mDrawableRight : drawables[2], + mDrawableBottom != null ? mDrawableBottom : drawables[3] + ); + mDrawableLeft = mDrawableBottom = mDrawableRight = mDrawableTop = null; + } + + private void setLineSpacing(V view) { + if (mLineSpacingExtra != null) { + view.setLineSpacing(mLineSpacingExtra, mLineSpacingMultiplier == null ? 1 : mLineSpacingMultiplier); + } else if (mLineSpacingMultiplier != null) { + view.setLineSpacing(0, mLineSpacingMultiplier); + } + mLineSpacingMultiplier = mLineSpacingExtra = null; + } + + private void setTypeface(V view) { + if (mFontFamily != null) { + //ignore typeface as android does + mTypeface = mFontFamily; + } + if (mTypeface != null) { + if (mTextStyle != null) { + view.setTypeface(Typeface.create(mTypeface, mTextStyle)); + } else { + view.setTypeface(Typeface.create(mTypeface, view.getTypeface().getStyle())); + } + } else if (mTextStyle != null) { + view.setTypeface(view.getTypeface(), mTextStyle); + } + mTypeface = mFontFamily = null; + mTextStyle = null; + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/TimePickerInflater.java b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/TimePickerInflater.java new file mode 100644 index 00000000..1aba5acf --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/TimePickerInflater.java @@ -0,0 +1,32 @@ +package com.stardust.autojs.core.ui.inflater.inflaters; + +import androidx.annotation.Nullable; +import android.view.View; +import android.widget.TimePicker; + +import org.autojs.autojs6.R; +import com.stardust.autojs.core.ui.inflater.ResourceParser; +import com.stardust.autojs.core.ui.inflater.ViewCreator; + +/** + * Created by Stardust on 2017/11/29. + */ + +public class TimePickerInflater extends BaseViewInflater { + + public TimePickerInflater(ResourceParser resourceParser) { + super(resourceParser); + } + + @Nullable + @Override + public ViewCreator getCreator() { + return (context, attrs) -> { + String datePickerMode = attrs.remove("android:timePickerMode"); + if (datePickerMode == null || !datePickerMode.equals("spinner")) { + return new TimePicker(context); + } + return (TimePicker) View.inflate(context, R.layout.time_picker_spinner, null); + }; + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/ToolbarInflater.java b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/ToolbarInflater.java new file mode 100644 index 00000000..cd757713 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/ToolbarInflater.java @@ -0,0 +1,89 @@ +package com.stardust.autojs.core.ui.inflater.inflaters; + +import androidx.annotation.Nullable; +import androidx.appcompat.widget.Toolbar; +import android.view.View; +import android.view.ViewGroup; + +import org.autojs.autojs6.R; +import com.stardust.autojs.core.ui.inflater.ResourceParser; +import com.stardust.autojs.core.ui.inflater.ViewCreator; +import com.stardust.autojs.core.ui.inflater.util.Colors; +import com.stardust.autojs.core.ui.inflater.util.Dimensions; +import com.stardust.autojs.core.ui.inflater.util.Strings; +import com.stardust.autojs.core.ui.inflater.util.ValueMapper; + +import java.util.Map; + +/** + * Created by Stardust on 2017/11/5. + */ + +public class ToolbarInflater extends BaseViewInflater { + + + private static final ValueMapper POP_UP_THEMES = new ValueMapper("popupTheme") + .map("dark", androidx.appcompat.R.style.ThemeOverlay_AppCompat_Dark_ActionBar) + .map("light", androidx.appcompat.R.style.ThemeOverlay_AppCompat_ActionBar); + + public ToolbarInflater(ResourceParser resourceParser) { + super(resourceParser); + } + + @Override + public boolean setAttr(V view, String attr, String value, ViewGroup parent, Map attrs) { + switch (attr) { + + case "logo": + view.setLogo(getDrawables().parse(view, value)); + break; + case "logoDescription": + view.setLogoDescription(Strings.parse(view, value)); + break; + case "navigationIcon": + view.setNavigationContentDescription(Strings.parse(view, value)); + break; + case "popupTheme": + view.setPopupTheme(POP_UP_THEMES.get(value)); + break; + case "subtitle": + view.setSubtitle(Strings.parse(view, value)); + break; + case "subtitleTextColor": + view.setSubtitleTextColor(Colors.parse(view, value)); + break; + case "titleTextColor": + view.setTitleTextColor(Colors.parse(view, value)); + break; + case "title": + view.setTitle(Strings.parse(view, value)); + break; + case "titleMargin": + int margin = Dimensions.parseToIntPixel(value, view); + view.setTitleMargin(margin, margin, margin, margin); + break; + case "titleMarginBottom": + view.setTitleMarginBottom(Dimensions.parseToIntPixel(value, view)); + break; + case "titleMarginTop": + view.setTitleMarginTop(Dimensions.parseToIntPixel(value, view)); + break; + case "titleMarginStart": + view.setTitleMarginStart(Dimensions.parseToIntPixel(value, view)); + break; + case "titleMarginEnd": + view.setTitleMarginEnd(Dimensions.parseToIntPixel(value, view)); + break; + default: + return super.setAttr(view, attr, value, parent, attrs); + } + return true; + } + + @Nullable + @Override + @SuppressWarnings("unchecked") + public ViewCreator getCreator() { + return (ViewCreator) (context, attrs) -> (V) View.inflate(context, R.layout.js_toolbar, null); + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/ViewGroupInflater.java b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/ViewGroupInflater.java new file mode 100644 index 00000000..dbec63b1 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/inflater/inflaters/ViewGroupInflater.java @@ -0,0 +1,85 @@ +package com.stardust.autojs.core.ui.inflater.inflaters; + +import android.animation.LayoutTransition; +import android.os.Build; +import android.view.ViewGroup; + +import com.stardust.autojs.core.ui.inflater.ResourceParser; +import com.stardust.autojs.core.ui.inflater.util.ValueMapper; + +import java.util.Map; + +/** + * Created by Stardust on 2017/11/4. + */ + +public class ViewGroupInflater extends BaseViewInflater { + + private static final ValueMapper PERSISTENT_DRAWING_CACHE = new ValueMapper("persistentDrawingCache") + .map("all", ViewGroup.PERSISTENT_ALL_CACHES) + .map("animation", ViewGroup.PERSISTENT_ANIMATION_CACHE) + .map("none", 0) + .map("scrolling", ViewGroup.PERSISTENT_SCROLLING_CACHE); + + private static final ValueMapper LAYOUT_MODES = new ValueMapper("layoutMode") + .map("clipBounds", 0) //ViewGroup.LAYOUT_MODE_CLIP_BOUNDS) + .map("opticalBounds", 1); //ViewGroup.LAYOUT_MODE_OPTICAL_BOUNDS); + + private static final ValueMapper DESCENDANT_FOCUSABILITY = new ValueMapper("descendantFocusability") + .map("afterDescendants", ViewGroup.FOCUS_AFTER_DESCENDANTS) + .map("beforeDescendants", ViewGroup.FOCUS_BEFORE_DESCENDANTS) + .map("blocksDescendants", ViewGroup.FOCUS_BLOCK_DESCENDANTS); + + public ViewGroupInflater(ResourceParser resourceParser) { + super(resourceParser); + } + + + @Override + public boolean setAttr(V view, String attr, String value, ViewGroup parent, Map attrs) { + switch (attr) { + case "addStatesFromChildren": + view.setAddStatesFromChildren(Boolean.valueOf(value)); + break; + case "alwaysDrawnWithCache": + view.setAlwaysDrawnWithCacheEnabled(Boolean.valueOf(value)); + break; + case "animateLayoutChanges": + view.setLayoutTransition(new LayoutTransition()); + break; + case "animationCache": + view.setAnimationCacheEnabled(Boolean.valueOf(value)); + break; + case "clipChildren": + view.setClipChildren(Boolean.valueOf(value)); + break; + case "clipToPadding": + view.setClipToPadding(Boolean.valueOf(value)); + break; + case "descendantFocusability": + view.setDescendantFocusability(DESCENDANT_FOCUSABILITY.get(value)); + break; + case "layoutAnimation": + Exceptions.unsupports(view, attr, value); + break; + case "layoutMode": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + view.setLayoutMode(LAYOUT_MODES.get(value)); + } + break; + case "persistentDrawingCache": + view.setPersistentDrawingCache(PERSISTENT_DRAWING_CACHE.get(value)); + break; + case "splitMotionEvents": + view.setMotionEventSplittingEnabled(Boolean.valueOf(value)); + break; + default: + return super.setAttr(view, attr, value, parent, attrs); + } + return true; + } + + public void applyPendingAttributesOfChildren(V view) { + + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/inflater/util/Colors.java b/app/src/main/java/com/stardust/autojs/core/ui/inflater/util/Colors.java new file mode 100644 index 00000000..eafa4b09 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/inflater/util/Colors.java @@ -0,0 +1,28 @@ +package com.stardust.autojs.core.ui.inflater.util; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Color; +import android.view.View; + +/** + * Created by Stardust on 2017/11/3. + */ + +public class Colors { + + public static int parse(Context context, String color) { + Resources resources = context.getResources(); + if (color.startsWith("@color/")) { + return resources.getColor(resources.getIdentifier(color.substring("@color/".length()), "color", context.getPackageName())); + } + if (color.startsWith("@android:color/")) { + return Color.parseColor(color.substring(15)); + } + return Color.parseColor(color); + } + + public static int parse(View view, String color){ + return parse(view.getContext(), color); + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/inflater/util/Dimensions.java b/app/src/main/java/com/stardust/autojs/core/ui/inflater/util/Dimensions.java new file mode 100644 index 00000000..cc0d4117 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/inflater/util/Dimensions.java @@ -0,0 +1,92 @@ +package com.stardust.autojs.core.ui.inflater.util; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.TypedValue; +import android.view.InflateException; +import android.view.View; +import android.view.ViewGroup; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Created by nick on 8/10/15. + *

+ * Taken from http://stackoverflow.com/questions/8343971/how-to-parse-a-dimension-string-and-convert-it-to-a-dimension-value + */ +public class Dimensions { + + private static final ValueMapper UNITS = new ValueMapper("unit") + .map("px", TypedValue.COMPLEX_UNIT_PX) + .map("dip", TypedValue.COMPLEX_UNIT_DIP) + .map("dp", TypedValue.COMPLEX_UNIT_DIP) + .map("sp", TypedValue.COMPLEX_UNIT_SP) + .map("pt", TypedValue.COMPLEX_UNIT_PT) + .map("in", TypedValue.COMPLEX_UNIT_IN) + .map("mm", TypedValue.COMPLEX_UNIT_MM); + + + private static final Pattern DIMENSION_PATTERN = Pattern.compile("([+-]?[0-9.]+)([a-zA-Z]*)"); + + public static int parseToPixel(String dimension, View view, ViewGroup parent, boolean horizontal) { + if (dimension.endsWith("%") && parent != null) { + float pct = Float.parseFloat(dimension.substring(0, dimension.length() - 1)) / 100.0f; + return (int) (pct * (horizontal ? parent.getMeasuredWidth() : parent.getMeasuredHeight())); + } + return parseToIntPixel(dimension, view.getContext()); + } + + public static float parseToPixel(String dimension, View view) { + return parseToPixel(dimension, view.getContext()); + } + + public static float parseToPixel(View view, String dimension) { + return parseToPixel(dimension, view.getContext()); + } + + public static float parseToPixel(String dimension, Context context) { + if (dimension.startsWith("?")) { + int[] attr = {context.getResources().getIdentifier(dimension.substring(1), "attr", + context.getPackageName())}; + TypedArray ta = context.obtainStyledAttributes(attr); + float d = ta.getDimension(0, 0); + ta.recycle(); + return d; + } + Matcher m = DIMENSION_PATTERN.matcher(dimension); + if (!m.matches()) { + throw new InflateException("dimension cannot be resolved: " + dimension); + } + int unit = m.groupCount() == 2 ? UNITS.get(m.group(2), TypedValue.COMPLEX_UNIT_DIP) : TypedValue.COMPLEX_UNIT_DIP; + float value = Integer.valueOf(m.group(1)); + return TypedValue.applyDimension(unit, value, context.getResources().getDisplayMetrics()); + } + + public static int parseToIntPixel(String value, View view) { + return Math.round(parseToPixel(value, view)); + } + + public static int parseToIntPixel(String value, Context context) { + return Math.round(parseToPixel(value, context)); + } + + public static int[] parseToIntPixelArray(View view, String value) { + String[] split = value.split(" "); + int[] pixels = new int[4]; + for (int i = 0; i < split.length; i++) { + pixels[i] = parseToIntPixel(split[i], view); + } + if (split.length == 1) { + pixels[1] = pixels[2] = pixels[3] = pixels[0]; + } else if (split.length == 2) { + pixels[2] = pixels[0]; + pixels[3] = pixels[1]; + } + return pixels; + } +} + + + + diff --git a/app/src/main/java/com/stardust/autojs/core/ui/inflater/util/Drawables.java b/app/src/main/java/com/stardust/autojs/core/ui/inflater/util/Drawables.java new file mode 100644 index 00000000..3a36d90b --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/inflater/util/Drawables.java @@ -0,0 +1,180 @@ +package com.stardust.autojs.core.ui.inflater.util; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Build; +import android.util.Base64; +import android.view.View; +import android.widget.ImageView; + +import com.stardust.autojs.core.ui.inflater.ImageLoader; + +import java.net.URL; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Created by Stardust on 2017/11/3. + */ + +public class Drawables { + + private static final Pattern DATA_PATTERN = Pattern.compile("data:(\\w+/\\w+);base64,(.+)"); + private static ImageLoader sDefaultImageLoader = new DefaultImageLoader(); + private ImageLoader mImageLoader = sDefaultImageLoader; + + public static void setDefaultImageLoader(ImageLoader defaultImageLoader) { + if (defaultImageLoader == null) + throw new NullPointerException(); + sDefaultImageLoader = defaultImageLoader; + } + + public static ImageLoader getDefaultImageLoader() { + return sDefaultImageLoader; + } + + public Drawable parse(Context context, String value) { + Resources resources = context.getResources(); + if (value.startsWith("@color/") || value.startsWith("@android:color/") || value.startsWith("#")) { + return new ColorDrawable(Colors.parse(context, value)); + } + if (value.startsWith("?")) { + return loadAttrResources(context, value); + } + if (value.startsWith("file://")) { + return decodeImage(value.substring(7)); + } + return loadDrawableResources(context, value); + } + + public Drawable loadDrawableResources(Context context, String value) { + int resId = context.getResources().getIdentifier(value, "drawable", + context.getPackageName()); + if (resId == 0) + throw new Resources.NotFoundException("drawable not found: " + value); + return context.getResources().getDrawable(resId); + } + + public Drawable loadAttrResources(Context context, String value) { + int[] attr = {context.getResources().getIdentifier(value.substring(1), "attr", + context.getPackageName())}; + TypedArray ta = context.obtainStyledAttributes(attr); + Drawable drawable = ta.getDrawable(0 /* index */); + ta.recycle(); + return drawable; + } + + public Drawable decodeImage(String path) { + return new BitmapDrawable(BitmapFactory.decodeFile(path)); + } + + public Drawable parse(View view, String name) { + return parse(view.getContext(), name); + } + + public void loadInto(ImageView view, Uri uri) { + mImageLoader.loadInto(view, uri); + } + + public void loadIntoBackground(View view, Uri uri) { + mImageLoader.loadIntoBackground(view, uri); + } + + public void setupWithImage(V view, String value) { + if (value.startsWith("http://") || value.startsWith("https://")) { + loadInto(view, Uri.parse(value)); + } else if (value.startsWith("data:")) { + loadDataInto(view, value); + } else { + view.setImageDrawable(parse(view, value)); + } + } + + private void loadDataInto(ImageView view, String data) { + Bitmap bitmap = loadBase64Data(data); + view.setImageBitmap(bitmap); + } + + public static Bitmap loadBase64Data(String data) { + Matcher matcher = DATA_PATTERN.matcher(data); + String base64; + if (!matcher.matches() || matcher.groupCount() != 2) { + base64 = data; + } else { + String mimeType = matcher.group(1); + base64 = matcher.group(2); + } + byte[] bytes = Base64.decode(base64, Base64.DEFAULT); + return BitmapFactory.decodeByteArray(bytes, 0, bytes.length); + } + + public void setupWithViewBackground(View view, String value) { + if (value.startsWith("http://") || value.startsWith("https://")) { + loadIntoBackground(view, Uri.parse(value)); + } else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + view.setBackground(parse(view, value)); + } else { + view.setBackgroundDrawable(parse(view, value)); + } + } + } + + public void setImageLoader(ImageLoader imageLoader) { + mImageLoader = imageLoader; + } + + public ImageLoader getImageLoader() { + return mImageLoader; + } + + private static class DefaultImageLoader implements ImageLoader { + + @Override + public void loadInto(final ImageView view, Uri uri) { + load(view, uri, view::setImageDrawable); + } + + @Override + public void loadIntoBackground(final View view, Uri uri) { + load(view, uri, view::setBackground); + } + + @Override + public Drawable load(View view, Uri uri) { + try { + URL url = new URL(uri.toString()); + Bitmap bmp = BitmapFactory.decodeStream(url.openConnection().getInputStream()); + return new BitmapDrawable(view.getResources(), bmp); + } catch (Exception e) { + return null; + } + } + + @Override + public void load(View view, Uri uri, final DrawableCallback callback) { + load(view, uri, (BitmapCallback) bitmap -> callback.onLoaded(new BitmapDrawable(view.getResources(), bitmap))); + } + + @Override + public void load(final View view, final Uri uri, final BitmapCallback callback) { + new Thread(() -> { + try { + URL url = new URL(uri.toString()); + final Bitmap bmp = BitmapFactory.decodeStream(url.openConnection().getInputStream()); + view.post(() -> callback.onLoaded(bmp)); + } catch (Exception e) { + e.printStackTrace(); + } + }).start(); + } + } +} + diff --git a/app/src/main/java/com/stardust/autojs/core/ui/inflater/util/Gravities.java b/app/src/main/java/com/stardust/autojs/core/ui/inflater/util/Gravities.java new file mode 100644 index 00000000..2b7f74cc --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/inflater/util/Gravities.java @@ -0,0 +1,46 @@ +package com.stardust.autojs.core.ui.inflater.util; + +import android.view.Gravity; + +/** + * Created by Stardust on 2017/11/3. + */ + +public class Gravities { + + public static int parse(String g) { + int gravity = Gravity.NO_GRAVITY; + String[] parts = g.toLowerCase().split("[|]"); + for (String part : parts) { + switch (part) { + case "center": + gravity = gravity | Gravity.CENTER; + break; + case "left": + case "textStart": + gravity = gravity | Gravity.LEFT; + break; + case "right": + case "textEnd": + gravity = gravity | Gravity.RIGHT; + break; + case "top": + gravity = gravity | Gravity.TOP; + break; + case "bottom": + gravity = gravity | Gravity.BOTTOM; + break; + case "center_horizontal": + gravity = gravity | Gravity.CENTER_HORIZONTAL; + break; + case "center_vertical": + gravity = gravity | Gravity.CENTER_VERTICAL; + break; + } + } + return gravity; + } + + + +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/inflater/util/Ids.java b/app/src/main/java/com/stardust/autojs/core/ui/inflater/util/Ids.java new file mode 100644 index 00000000..d660e0f3 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/inflater/util/Ids.java @@ -0,0 +1,33 @@ +package com.stardust.autojs.core.ui.inflater.util; + +import java.util.HashMap; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Created by Stardust on 2017/11/3. + */ + +public class Ids { + + private static AtomicInteger maxId = new AtomicInteger(20161209); + private static HashMap ids = new HashMap<>(); + + public static String parseIdName(String idName) { + if (idName.startsWith("@+id/")) { + return idName.substring(5); + } else if (idName.startsWith("@id/")) { + return idName.substring(4); + } + return idName; + } + + public static int parse(String name) { + name = parseIdName(name); + Integer id = ids.get(name); + if (id == null) { + id = maxId.incrementAndGet(); + ids.put(name, id); + } + return id; + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/inflater/util/Res.java b/app/src/main/java/com/stardust/autojs/core/ui/inflater/util/Res.java new file mode 100644 index 00000000..6a8076f8 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/inflater/util/Res.java @@ -0,0 +1,22 @@ +package com.stardust.autojs.core.ui.inflater.util; + +import android.content.Context; +import android.view.View; + +/** + * Created by Stardust on 2017/11/5. + */ + +public class Res { + public static int parseStyle(View view, String value) { + return parseStyle(view.getContext(), value); + } + + public static int parseStyle(Context context, String value) { + // FIXME: 2017/11/5 Can or should it retrieve android.R.style or styleable? + if (value.startsWith("@style/")) { + value = value.substring(7); + } + return context.getResources().getIdentifier(value, "style", context.getPackageName()); + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/inflater/util/Strings.java b/app/src/main/java/com/stardust/autojs/core/ui/inflater/util/Strings.java new file mode 100644 index 00000000..4437e006 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/inflater/util/Strings.java @@ -0,0 +1,26 @@ +package com.stardust.autojs.core.ui.inflater.util; + +import android.content.Context; +import android.content.res.Resources; +import android.view.View; + +/** + * Created by Stardust on 2017/11/4. + */ + +public class Strings { + + + public static String parse(Context context, String str) { + if (str.startsWith("@string/")) { + Resources resources = context.getResources(); + return resources.getString(resources.getIdentifier(str, "string", + context.getPackageName())); + } + return str; + } + + public static String parse(View view, String str) { + return parse(view.getContext(), str); + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/inflater/util/ValueMapper.java b/app/src/main/java/com/stardust/autojs/core/ui/inflater/util/ValueMapper.java new file mode 100644 index 00000000..655ff9a1 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/inflater/util/ValueMapper.java @@ -0,0 +1,61 @@ +package com.stardust.autojs.core.ui.inflater.util; + +import android.view.InflateException; + +import java.util.HashMap; + +/** + * Created by Stardust on 2017/11/3. + */ + +public class ValueMapper { + + private HashMap mHashMap = new HashMap<>(); + private String mAttrName; + + public ValueMapper(String attrName) { + mAttrName = attrName; + } + + public ValueMapper map(String key, V value) { + mHashMap.put(key, value); + return this; + } + + + public ValueMapper mapDefault(String key, V value) { + mHashMap.put(key, value); + mHashMap.put("", value); + return this; + } + + public ValueMapper mapDefault(V value) { + mHashMap.put("", value); + return this; + } + + public V get(String key, V defValue) { + V v = mHashMap.get(key); + if (v == null) { + return defValue; + } + return v; + } + + public V get(String key) { + V v = mHashMap.get(key); + if (v == null) { + throw new InflateException(String.format("unknown value for %s: %s", mAttrName, key)); + } + return v; + } + + public int split(String str){ + int r = 0; + for(String s : str.split("\\|")){ + r |= (Integer) get(s); + } + return r; + } + +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/nativeview/NativeView.java b/app/src/main/java/com/stardust/autojs/core/ui/nativeview/NativeView.java new file mode 100644 index 00000000..acd82243 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/nativeview/NativeView.java @@ -0,0 +1,87 @@ +package com.stardust.autojs.core.ui.nativeview; + +import android.graphics.PorterDuff; +import android.view.View; +import android.widget.Button; + +import com.stardust.autojs.core.ui.JsViewHelper; +import com.stardust.autojs.core.ui.ViewExtras; +import com.stardust.autojs.core.ui.attribute.ViewAttributes; +import com.stardust.autojs.rhino.NativeJavaObjectWithPrototype; + +import org.mozilla.javascript.NativeJavaObject; +import org.mozilla.javascript.NativeObject; +import org.mozilla.javascript.ScriptRuntime; +import org.mozilla.javascript.Scriptable; + +public class NativeView extends NativeJavaObjectWithPrototype { + + private static final String LOG_TAG = "NativeView"; + + public static class ScrollEvent { + public int scrollX; + public int scrollY; + public int oldScrollX; + public int oldScrollY; + + public ScrollEvent(int scrollX, int scrollY, int oldScrollX, int oldScrollY) { + this.scrollX = scrollX; + this.scrollY = scrollY; + this.oldScrollX = oldScrollX; + this.oldScrollY = oldScrollY; + } + } + + public static class LongClickEvent { + public final View view; + + public LongClickEvent(View view) { + this.view = view; + } + } + + + private final ViewAttributes mViewAttributes; + private final View mView; + private final ViewPrototype mViewPrototype; + + public NativeView(Scriptable scope, View view, Class staticType, com.stardust.autojs.runtime.ScriptRuntime runtime) { + super(scope, view, staticType); + mViewAttributes = ViewExtras.getViewAttributes(view, runtime.ui.getResourceParser()); + mView = view; + mViewPrototype = new ViewPrototype(mView, mViewAttributes, scope, runtime); + prototype = new NativeJavaObjectWithPrototype(scope, mViewPrototype, mViewPrototype.getClass()); + prototype.setPrototype(new NativeObject()); + } + + @Override + public boolean has(String name, Scriptable start) { + if (mViewAttributes.contains(name)) { + return true; + } + return super.has(name, start); + } + + @Override + public Object get(String name, Scriptable start) { + if (super.has(name, start)) { + return super.get(name, start); + } else { + View view = JsViewHelper.findViewByStringId(mView, name); + if (view != null) { + return view; + } + } + return Scriptable.NOT_FOUND; + } + + public ViewPrototype getViewPrototype() { + return mViewPrototype; + } + + @Override + public View unwrap() { + return mView; + } + +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/nativeview/ViewPrototype.java b/app/src/main/java/com/stardust/autojs/core/ui/nativeview/ViewPrototype.java new file mode 100644 index 00000000..fbd102b6 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/nativeview/ViewPrototype.java @@ -0,0 +1,253 @@ +package com.stardust.autojs.core.ui.nativeview; + +import android.annotation.SuppressLint; +import android.os.Build; +import android.os.Looper; +import androidx.core.view.ViewCompat; +import android.view.MotionEvent; +import android.view.View; +import android.widget.CompoundButton; + +import com.stardust.autojs.core.eventloop.EventEmitter; +import com.stardust.autojs.core.ui.BaseEvent; +import com.stardust.autojs.core.ui.attribute.ViewAttributes; +import com.stardust.autojs.core.ui.widget.JsListView; +import com.stardust.autojs.runtime.ScriptRuntime; + +import org.mozilla.javascript.Scriptable; +import org.mozilla.javascript.Undefined; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public class ViewPrototype { + + private final EventEmitter mEventEmitter; + private final View mView; + private final Set mRegisteredEvents = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private final Scriptable mScope; + private final ViewAttributes mViewAttributes; + private Object mWidget; + + public ViewPrototype(View view, ViewAttributes viewAttributes, Scriptable scope, ScriptRuntime runtime) { + mView = view; + mViewAttributes = viewAttributes; + mEventEmitter = runtime.events.emitter(); + mScope = scope; + } + + public ViewAttributes getViewAttributes() { + return mViewAttributes; + } + + public Object attr(String name) { + ViewAttributes.Attribute attribute = mViewAttributes.get(name); + if (attribute != null) { + return attribute.get(); + } + return Undefined.SCRIPTABLE_UNDEFINED; + } + + public void attr(String name, Object value) { + ViewAttributes.Attribute attribute = mViewAttributes.get(name); + if (attribute != null) { + attribute.set(org.mozilla.javascript.ScriptRuntime.toString(value)); + } + } + + public void setWidget(Object widget) { + mWidget = widget; + } + + public Object getWidget() { + return mWidget; + } + + public void click() { + mView.performClick(); + } + + public void longClick() { + mView.performLongClick(); + } + + public void click(Object listener) { + on("click", listener); + } + + public void longClick(Object listener) { + on("long_click", listener); + } + + public EventEmitter once(String eventName, Object listener) { + registerEventIfNeeded(eventName); + return mEventEmitter.once(eventName, listener); + } + + public EventEmitter on(String eventName, Object listener) { + registerEventIfNeeded(eventName); + return mEventEmitter.on(eventName, listener); + } + + public EventEmitter addListener(String eventName, Object listener) { + registerEventIfNeeded(eventName); + return mEventEmitter.addListener(eventName, listener); + } + + private void registerEventIfNeeded(String eventName) { + if (mRegisteredEvents.contains(eventName)) { + return; + } + if (Looper.getMainLooper() == Looper.myLooper()) { + if (registerEvent(eventName)) { + mRegisteredEvents.add(eventName); + } + } else { + mView.post(() -> { + if (mRegisteredEvents.contains(eventName)) { + return; + } + if (registerEvent(eventName)) { + mRegisteredEvents.add(eventName); + } + }); + } + + } + + @SuppressLint("ClickableViewAccessibility") + private boolean registerEvent(String eventName) { + switch (eventName) { + case "touch_down": + case "touch_up": + case "touch": { + mView.setOnTouchListener((v, event) -> { + BaseEvent e = new BaseEvent(mScope, event, event.getClass()); + //Log.d(LOG_TAG, "this = " + NativeView.this + ", emitter = " + mEventEmitter + ", view = " + mView); + if (event.getAction() == MotionEvent.ACTION_DOWN) { + emit("touch_down", e, v); + } else if (event.getAction() == MotionEvent.ACTION_UP) { + emit("touch_up", e, v); + } else if (event.getAction() == MotionEvent.ACTION_MOVE) { + emit("touch_move", e, v); + } + emit("touch", e, v); + return e.isConsumed(); + }); + } + return true; + case "click": { + mView.setOnClickListener(v -> emit("click", v)); + } + return true; + case "long_click": { + mView.setOnLongClickListener(v -> { + BaseEvent e = new BaseEvent(mScope, new NativeView.LongClickEvent(v)); + emit("long_click", e, v); + return e.isConsumed(); + }); + } + return true; + case "key": { + mView.setOnKeyListener((v, keyCode, event) -> { + BaseEvent e = new BaseEvent(mScope, event, event.getClass()); + if (event.getAction() == MotionEvent.ACTION_DOWN) { + emit("key_down", keyCode, e, v); + } else if (event.getAction() == MotionEvent.ACTION_UP) { + emit("key_up", keyCode, e, v); + } + emit("key", keyCode, e, v); + return e.isConsumed(); + }); + } + return true; + case "scroll_change": { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + mView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> { + BaseEvent e = new BaseEvent(mScope, new NativeView.ScrollEvent(scrollX, scrollY, oldScrollX, oldScrollY)); + emit("scroll_change", e, v); + }); + return true; + } + } + break; + case "check": { + if (mView instanceof CompoundButton) { + ((CompoundButton) mView).setOnCheckedChangeListener((buttonView, isChecked) -> + emit("check", isChecked, buttonView)); + } + } + case "item_click": + case "item_long_click": { + if (mView instanceof JsListView) { + ((JsListView) mView).setOnItemTouchListener(new JsListView.OnItemTouchListener() { + @Override + public void onItemClick(JsListView listView, View itemView, Object item, int pos) { + emit("item_click", item, pos, itemView, listView); + } + + @Override + public boolean onItemLongClick(JsListView listView, View itemView, Object item, int pos) { + BaseEvent e = new BaseEvent(mScope, new NativeView.LongClickEvent(itemView)); + emit("item_long_click", e, item, pos, itemView, listView); + return e.isConsumed(); + } + }); + return true; + } + } + break; + } + return false; + } + + public boolean emit(String eventName, Object... args) { + return mEventEmitter.emit(eventName, args); + } + + public String[] eventNames() { + return mEventEmitter.eventNames(); + } + + public int listenerCount(String eventName) { + return mEventEmitter.listenerCount(eventName); + } + + public Object[] listeners(String eventName) { + return mEventEmitter.listeners(eventName); + } + + public EventEmitter prependListener(String eventName, Object listener) { + return mEventEmitter.prependListener(eventName, listener); + } + + public EventEmitter prependOnceListener(String eventName, Object listener) { + return mEventEmitter.prependOnceListener(eventName, listener); + } + + public EventEmitter removeAllListeners() { + return mEventEmitter.removeAllListeners(); + } + + public EventEmitter removeAllListeners(String eventName) { + return mEventEmitter.removeAllListeners(eventName); + } + + public EventEmitter removeListener(String eventName, Object listener) { + return mEventEmitter.removeListener(eventName, listener); + } + + public EventEmitter setMaxListeners(int n) { + return mEventEmitter.setMaxListeners(n); + } + + public int getMaxListeners() { + return mEventEmitter.getMaxListeners(); + } + + public static int defaultMaxListeners() { + return EventEmitter.defaultMaxListeners(); + } + +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/widget/JsButton.java b/app/src/main/java/com/stardust/autojs/core/ui/widget/JsButton.java new file mode 100644 index 00000000..126eec6d --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/widget/JsButton.java @@ -0,0 +1,40 @@ +package com.stardust.autojs.core.ui.widget; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Build; +import androidx.annotation.RequiresApi; +import android.util.AttributeSet; +import android.widget.Button; + +/** + * Created by Stardust on 2017/5/15. + */ + +@SuppressLint("AppCompatCustomView") +public class JsButton extends Button { + public JsButton(Context context) { + super(context); + } + + public JsButton(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public JsButton(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public JsButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public String text() { + return getText().toString(); + } + + public void text(CharSequence text) { + setText(text); + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/widget/JsEditText.java b/app/src/main/java/com/stardust/autojs/core/ui/widget/JsEditText.java new file mode 100644 index 00000000..9c050659 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/widget/JsEditText.java @@ -0,0 +1,42 @@ +package com.stardust.autojs.core.ui.widget; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Build; +import androidx.annotation.RequiresApi; +import android.util.AttributeSet; +import android.widget.EditText; + +/** + * Created by Stardust on 2017/5/15. + */ + +@SuppressLint("AppCompatCustomView") +public class JsEditText extends EditText { + public JsEditText(Context context) { + super(context); + } + + public JsEditText(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public JsEditText(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public JsEditText(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public String text() { + return getText().toString(); + } + + public void text(CharSequence text) { + setText(text); + } + +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/widget/JsFrameLayout.java b/app/src/main/java/com/stardust/autojs/core/ui/widget/JsFrameLayout.java new file mode 100644 index 00000000..3cad32db --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/widget/JsFrameLayout.java @@ -0,0 +1,41 @@ +package com.stardust.autojs.core.ui.widget; + +import android.content.Context; +import android.os.Build; +import androidx.annotation.AttrRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.StyleRes; +import android.util.AttributeSet; +import android.view.View; +import android.widget.FrameLayout; + +import com.stardust.autojs.core.ui.JsViewHelper; + +/** + * Created by Stardust on 2017/5/14. + */ + +public class JsFrameLayout extends FrameLayout { + public JsFrameLayout(@NonNull Context context) { + super(context); + } + + public JsFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public JsFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public JsFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public View id(String id) { + return JsViewHelper.findViewByStringId(this, id); + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/widget/JsGridView.java b/app/src/main/java/com/stardust/autojs/core/ui/widget/JsGridView.java new file mode 100644 index 00000000..d4e5ebd4 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/widget/JsGridView.java @@ -0,0 +1,34 @@ +package com.stardust.autojs.core.ui.widget; + +import android.content.Context; + +import com.stardust.autojs.runtime.ScriptRuntime; + +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +/** + * Created by Stardust on 2018/3/30. + */ + +public class JsGridView extends JsListView { + + public JsGridView(Context context, ScriptRuntime scriptRuntime) { + super(context, scriptRuntime); + } + + @Override + protected void init() { + super.init(); + setLayoutManager(new GridLayoutManager(getContext(), 1){ + @Override + public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { + try { + super.onLayoutChildren(recycler, state); + } catch (IndexOutOfBoundsException e) { + getScriptRuntime().console.error(e); + } + } + }); + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/widget/JsImageView.java b/app/src/main/java/com/stardust/autojs/core/ui/widget/JsImageView.java new file mode 100644 index 00000000..97b56860 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/widget/JsImageView.java @@ -0,0 +1,70 @@ +package com.stardust.autojs.core.ui.widget; + +import android.content.Context; +import androidx.core.widget.ImageViewCompat; +import androidx.appcompat.widget.AppCompatImageView; +import android.util.AttributeSet; + +import com.makeramen.roundedimageview.RoundedImageView; + +import com.stardust.autojs.core.image.ImageWrapper; +import com.stardust.autojs.core.ui.inflater.util.Drawables; + +/** + * Created by Stardust on 2017/11/30. + */ + +public class JsImageView extends RoundedImageView { + + private boolean mCircle; + private Drawables mDrawables; + + public JsImageView(Context context) { + super(context); + } + + + public JsImageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public JsImageView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public void setCircle(boolean circle) { + mCircle = circle; + if (circle && getWidth() != 0) { + setCornerRadius(Math.min(getWidth(), getHeight()) / 2); + } + } + + public boolean isCircle() { + return mCircle; + } + + public void setSource(String uri) { + getDrawables().setupWithImage(this, uri); + } + + public void setSource(ImageWrapper image) { + setImageBitmap(image.getBitmap()); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + setMeasuredDimension(getMeasuredWidth(), getMeasuredHeight()); + if (mCircle) { + setCornerRadius(getMeasuredWidth() / 2); + } + } + + public void setDrawables(Drawables drawables) { + mDrawables = drawables; + } + + public Drawables getDrawables() { + return mDrawables; + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/widget/JsLinearLayout.java b/app/src/main/java/com/stardust/autojs/core/ui/widget/JsLinearLayout.java new file mode 100644 index 00000000..1ec5ec79 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/widget/JsLinearLayout.java @@ -0,0 +1,41 @@ +package com.stardust.autojs.core.ui.widget; + +import android.content.Context; +import android.os.Build; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import android.util.AttributeSet; +import android.view.View; +import android.widget.LinearLayout; + +import com.stardust.autojs.core.ui.JsViewHelper; + +/** + * Created by Stardust on 2017/5/14. + */ + +public class JsLinearLayout extends LinearLayout { + + + public JsLinearLayout(Context context) { + super(context); + } + + public JsLinearLayout(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public JsLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public JsLinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public View id(String id) { + return JsViewHelper.findViewByStringId(this, id); + } + +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/widget/JsListView.java b/app/src/main/java/com/stardust/autojs/core/ui/widget/JsListView.java new file mode 100644 index 00000000..0e849373 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/widget/JsListView.java @@ -0,0 +1,188 @@ +package com.stardust.autojs.core.ui.widget; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; + +import com.stardust.autojs.core.ui.ViewExtras; +import com.stardust.autojs.core.ui.inflater.DynamicLayoutInflater; +import com.stardust.autojs.core.ui.nativeview.NativeView; +import com.stardust.autojs.core.ui.nativeview.ViewPrototype; +import com.stardust.autojs.runtime.ScriptRuntime; + +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import com.stardust.autojs.workground.WrapContentLinearLayoutManager; + +import androidx.recyclerview.widget.RecyclerView; + +/** + * Created by Stardust on 2018/3/28. + */ + +public class JsListView extends RecyclerView { + + public interface DataSourceAdapter { + + int getItemCount(Object dataSource); + + Object getItem(Object dataSource, int i); + + void setDataSource(Object dataSource); + } + + public interface OnItemTouchListener { + void onItemClick(JsListView listView, View itemView, Object item, int pos); + + boolean onItemLongClick(JsListView listView, View itemView, Object item, int pos); + } + + private Node mItemTemplate; + private DynamicLayoutInflater mDynamicLayoutInflater; + private ScriptRuntime mScriptRuntime; + private Object mDataSource; + private DataSourceAdapter mDataSourceAdapter; + private OnItemTouchListener mOnItemTouchListener; + + public JsListView(Context context, ScriptRuntime scriptRuntime) { + super(context); + mScriptRuntime = scriptRuntime; + init(); + } + + protected void init() { + setAdapter(new Adapter()); + setLayoutManager(new WrapContentLinearLayoutManager(getContext())); + } + + protected ScriptRuntime getScriptRuntime() { + return mScriptRuntime; + } + + public void setOnItemTouchListener(OnItemTouchListener onItemTouchListener) { + mOnItemTouchListener = onItemTouchListener; + } + + public void setDataSourceAdapter(DataSourceAdapter dataSourceAdapter) { + mDataSourceAdapter = dataSourceAdapter; + getAdapter().notifyDataSetChanged(); + } + + public Object getDataSource() { + return mDataSource; + } + + public void setDataSource(Object dataSource) { + mDataSource = dataSource; + if (mDataSourceAdapter != null) + mDataSourceAdapter.setDataSource(dataSource); + getAdapter().notifyDataSetChanged(); + } + + public void setItemTemplate(DynamicLayoutInflater inflater, Node itemTemplate) { + mDynamicLayoutInflater = inflater; + mItemTemplate = itemTemplate; + } + + + public static class ItemHolder { + private final ViewHolder mViewHolder; + + ItemHolder(ViewHolder viewHolder) { + mViewHolder = viewHolder; + } + + public int getPosition() { + return mViewHolder.getAdapterPosition(); + } + + public Object getItem() { + return mViewHolder.item; + } + } + + public class ViewHolder extends RecyclerView.ViewHolder { + + Object item = null; + + public ViewHolder(View itemView) { + super(itemView); + itemView.setOnClickListener(v -> { + if (mOnItemTouchListener != null) { + int pos = getAdapterPosition(); + mOnItemTouchListener.onItemClick(JsListView.this, itemView, mDataSourceAdapter.getItem(mDataSource, pos), pos); + } + }); + itemView.setOnLongClickListener(v -> { + if (mOnItemTouchListener == null) + return false; + int pos = getAdapterPosition(); + return mOnItemTouchListener.onItemLongClick(JsListView.this, itemView, mDataSourceAdapter.getItem(mDataSource, pos), pos); + }); + NativeView nativeView = ViewExtras.getNativeView(JsListView.this); + if (nativeView != null) { + ViewPrototype prototype = nativeView.getViewPrototype(); + prototype.emit("item_bind", itemView, new ItemHolder(this)); + } + } + + + } + + private class Adapter extends RecyclerView.Adapter { + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + try { + mDynamicLayoutInflater.setInflateFlags(DynamicLayoutInflater.FLAG_IGNORES_DYNAMIC_ATTRS); + return new ViewHolder(mDynamicLayoutInflater.inflate(mDynamicLayoutInflater.newInflateContext(), mItemTemplate, parent, false)); + } catch (Exception e) { + mScriptRuntime.exit(e); + return new ViewHolder(new View(parent.getContext())); + } finally { + mDynamicLayoutInflater.setInflateFlags(DynamicLayoutInflater.FLAG_DEFAULT); + } + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + try { + Object oldCtx = mScriptRuntime.ui.getBindingContext(); + Object item = mDataSourceAdapter.getItem(mDataSource, position); + holder.item = item; + mScriptRuntime.ui.setBindingContext(item); + mDynamicLayoutInflater.setInflateFlags(DynamicLayoutInflater.FLAG_JUST_DYNAMIC_ATTRS); + applyDynamicAttrs(mItemTemplate, holder.itemView, JsListView.this); + mScriptRuntime.ui.setBindingContext(oldCtx); + } catch (Exception e) { + mScriptRuntime.exit(e); + } finally { + mDynamicLayoutInflater.setInflateFlags(DynamicLayoutInflater.FLAG_DEFAULT); + } + } + + private void applyDynamicAttrs(Node node, View itemView, ViewGroup parent) { + mDynamicLayoutInflater.applyAttributes(mDynamicLayoutInflater.newInflateContext(), itemView, mDynamicLayoutInflater.getAttributesMap(node), parent); + if (!(itemView instanceof ViewGroup)) + return; + ViewGroup viewGroup = (ViewGroup) itemView; + NodeList nodeList = node.getChildNodes(); + int j = 0; + for (int i = 0; i < nodeList.getLength(); i++) { + Node child = nodeList.item(i); + if (child.getNodeType() != Node.ELEMENT_NODE) continue; + applyDynamicAttrs(child, viewGroup.getChildAt(j), viewGroup); + j++; + } + } + + @Override + public int getItemCount() { + return mDataSource == null ? 0 + : mDataSourceAdapter == null ? 0 + : mDataSourceAdapter.getItemCount(mDataSource); + } + } + +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/widget/JsRelativeLayout.java b/app/src/main/java/com/stardust/autojs/core/ui/widget/JsRelativeLayout.java new file mode 100644 index 00000000..703c3f6c --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/widget/JsRelativeLayout.java @@ -0,0 +1,37 @@ +package com.stardust.autojs.core.ui.widget; + +import android.content.Context; +import android.os.Build; +import androidx.annotation.RequiresApi; +import android.util.AttributeSet; +import android.view.View; +import android.widget.RelativeLayout; + +import com.stardust.autojs.core.ui.JsViewHelper; + +/** + * Created by Stardust on 2017/5/14. + */ + +public class JsRelativeLayout extends RelativeLayout { + public JsRelativeLayout(Context context) { + super(context); + } + + public JsRelativeLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public JsRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public JsRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public View id(String id) { + return JsViewHelper.findViewByStringId(this, id); + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/widget/JsSpinner.java b/app/src/main/java/com/stardust/autojs/core/ui/widget/JsSpinner.java new file mode 100644 index 00000000..ea77c62a --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/widget/JsSpinner.java @@ -0,0 +1,162 @@ +package com.stardust.autojs.core.ui.widget; + +import android.content.Context; +import android.content.res.Resources; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; + +public class JsSpinner extends androidx.appcompat.widget.AppCompatSpinner { + + private float mTextSize = -1; + private int mTextStyle = -1; + private int mTextColor = 0; + + private float mEntryTextSize = -1; + private int mEntryTextStyle = -1; + private int mEntryTextColor = 0; + + public JsSpinner(Context context) { + super(context); + } + + public JsSpinner(Context context, int mode) { + super(context, mode); + } + + public JsSpinner(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public JsSpinner(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public JsSpinner(Context context, AttributeSet attrs, int defStyleAttr, int mode) { + super(context, attrs, defStyleAttr, mode); + } + + public JsSpinner(Context context, AttributeSet attrs, int defStyleAttr, int mode, Resources.Theme popupTheme) { + super(context, attrs, defStyleAttr, mode, popupTheme); + } + + + public float getTextSize() { + return mTextSize; + } + + public float getEntryTextSize() { + return mEntryTextSize; + } + + public void setEntryTextSize(float entryTextSize) { + mEntryTextSize = entryTextSize; + } + + public int getEntryTextStyle() { + return mEntryTextStyle; + } + + public void setEntryTextStyle(int entryTextStyle) { + mEntryTextStyle = entryTextStyle; + } + + public int getEntryTextColor() { + return mEntryTextColor; + } + + public void setEntryTextColor(int entryTextColor) { + mEntryTextColor = entryTextColor; + } + + public void setTextSize(float textSize) { + mTextSize = textSize; + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (child instanceof TextView) { + ((TextView) child).setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize); + } + } + } + + + public int getTextStyle() { + return mTextStyle; + } + + public void setTextStyle(int textStyle) { + mTextStyle = textStyle; + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (child instanceof TextView) { + ((TextView) child).setTypeface(((TextView) child).getTypeface(), mTextStyle); + } + } + } + + public int getTextColor() { + return mTextColor; + } + + public void setTextColor(int textColor) { + mTextColor = textColor; + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (child instanceof TextView) { + ((TextView) child).setTextColor(mTextColor); + } + } + } + + + public class Adapter extends ArrayAdapter { + + public Adapter(@NonNull Context context, int resource, @NonNull String[] objects) { + super(context, resource, objects); + } + + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + View view = super.getView(position, convertView, parent); + if (!(view instanceof TextView)) { + return view; + } + TextView textView = (TextView) view; + if (mTextColor != 0) { + textView.setTextColor(mTextColor); + } + if (mTextSize != -1) { + textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize); + } + if (mTextStyle != -1) { + textView.setTypeface(textView.getTypeface(), mTextStyle); + } + return textView; + } + + @Override + public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + View view = super.getDropDownView(position, convertView, parent); + if (!(view instanceof TextView)) { + return view; + } + TextView textView = (TextView) view; + if (mEntryTextColor != 0) { + textView.setTextColor(mEntryTextColor); + } + if (mEntryTextSize != -1) { + textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mEntryTextSize); + } + if (mEntryTextStyle != -1) { + textView.setTypeface(textView.getTypeface(), mEntryTextStyle); + } + return textView; + } + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/widget/JsTabLayout.java b/app/src/main/java/com/stardust/autojs/core/ui/widget/JsTabLayout.java new file mode 100644 index 00000000..88f7aef3 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/widget/JsTabLayout.java @@ -0,0 +1,21 @@ +package com.stardust.autojs.core.ui.widget; + +import android.content.Context; +import android.util.AttributeSet; + +import com.google.android.material.tabs.TabLayout; + +public class JsTabLayout extends TabLayout { + + public JsTabLayout(Context context) { + super(context); + } + + public JsTabLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public JsTabLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/widget/JsTextView.java b/app/src/main/java/com/stardust/autojs/core/ui/widget/JsTextView.java new file mode 100644 index 00000000..33ee02ec --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/widget/JsTextView.java @@ -0,0 +1,36 @@ +package com.stardust.autojs.core.ui.widget; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.util.AttributeSet; + +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatTextView; + +/** + * Created by Stardust on 2017/5/15. + */ + +@SuppressLint("AppCompatCustomView") +public class JsTextView extends AppCompatTextView { + + public JsTextView(Context context) { + super(context); + } + + public JsTextView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public JsTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public String text() { + return getText().toString(); + } + + public void text(CharSequence text) { + setText(text); + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/widget/JsToolbar.java b/app/src/main/java/com/stardust/autojs/core/ui/widget/JsToolbar.java new file mode 100644 index 00000000..ac55da28 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/widget/JsToolbar.java @@ -0,0 +1,47 @@ +package com.stardust.autojs.core.ui.widget; + +import android.app.Activity; +import android.content.Context; +import android.content.ContextWrapper; +import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBarDrawerToggle; +import androidx.appcompat.widget.Toolbar; +import androidx.drawerlayout.widget.DrawerLayout; + +import android.util.AttributeSet; + +import org.autojs.autojs6.R; + +public class JsToolbar extends Toolbar { + + public JsToolbar(Context context) { + super(context); + } + + public JsToolbar(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public JsToolbar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public void setupWithDrawer(DrawerLayout drawerLayout) { + ActionBarDrawerToggle drawerToggle = new ActionBarDrawerToggle(getActivity(), drawerLayout, this, R.string.text_drawer_open, + R.string.text_drawer_close); + drawerToggle.syncState(); + drawerLayout.addDrawerListener(drawerToggle); + } + + private Activity getActivity() { + Context context = getContext(); + while (!(context instanceof Activity)) { + if (context instanceof ContextWrapper) { + context = ((ContextWrapper) context).getBaseContext(); + } else { + return null; + } + } + return (Activity) context; + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/widget/JsViewPager.java b/app/src/main/java/com/stardust/autojs/core/ui/widget/JsViewPager.java new file mode 100644 index 00000000..04fa1793 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/widget/JsViewPager.java @@ -0,0 +1,67 @@ +package com.stardust.autojs.core.ui.widget; + +import android.content.Context; + +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; + +import com.stardust.autojs.core.ui.inflater.ShouldCallOnFinishInflate; + +import androidx.viewpager.widget.PagerAdapter; +import androidx.viewpager.widget.ViewPager; + +public class JsViewPager extends ViewPager implements ShouldCallOnFinishInflate { + + private String[] mTitles; + + public JsViewPager(Context context) { + super(context); + } + + + public JsViewPager(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void onFinishDynamicInflate() { + setAdapter(); + } + + public void setTitles(String[] titles) { + mTitles = titles; + } + + private void setAdapter() { + setOffscreenPageLimit(getChildCount()); + setAdapter(new PagerAdapter() { + + @Override + public Object instantiateItem(ViewGroup container, int position) { + return getChildAt(position); + } + + @Override + public CharSequence getPageTitle(int position) { + return mTitles != null && position < mTitles.length ? + mTitles[position] : + super.getPageTitle(position); + } + + @Override + public int getCount() { + return getChildCount(); + } + + @Override + public boolean isViewFromObject(View view, Object object) { + return view == object; + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + + } + }); + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/widget/JsWebView.java b/app/src/main/java/com/stardust/autojs/core/ui/widget/JsWebView.java new file mode 100644 index 00000000..f629fa04 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/widget/JsWebView.java @@ -0,0 +1,52 @@ +package com.stardust.autojs.core.ui.widget; + +import android.content.Context; +import android.os.Build; +import androidx.annotation.RequiresApi; +import android.util.AttributeSet; +import android.webkit.WebChromeClient; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +/** + * Created by Stardust on 2017/11/29. + */ + +public class JsWebView extends WebView { + + public JsWebView(Context context) { + super(context); + init(); + } + + public JsWebView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public JsWebView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public JsWebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + + private void init() { + WebSettings settings = getSettings(); + settings.setUseWideViewPort(true); + settings.setBuiltInZoomControls(true); + settings.setLoadWithOverviewMode(true); + settings.setJavaScriptEnabled(true); + settings.setJavaScriptCanOpenWindowsAutomatically(true); + settings.setDomStorageEnabled(true); + settings.setDisplayZoomControls(false); + setWebViewClient(new WebViewClient()); + setWebChromeClient(new WebChromeClient()); + } + +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/xml/AttributeHandler.java b/app/src/main/java/com/stardust/autojs/core/ui/xml/AttributeHandler.java new file mode 100644 index 00000000..1fc0f34e --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/xml/AttributeHandler.java @@ -0,0 +1,188 @@ +package com.stardust.autojs.core.ui.xml; + +import android.util.Log; + +import org.w3c.dom.Node; + +import java.util.HashMap; +import java.util.Map; + +/** + * Created by Stardust on 2017/5/14. + */ + +public interface AttributeHandler { + + boolean handle(String nodeName, Node attr, StringBuilder layoutXml); + + class AttrNameRouter implements AttributeHandler { + + private Map mHandlerMap = new HashMap<>(); + private AttributeHandler mDefaultHandler; + + @Override + public boolean handle(String nodeName, Node attr, StringBuilder layoutXml) { + AttributeHandler handler = mHandlerMap.get(attr.getNodeName()); + if (handler == null) + handler = mDefaultHandler; + return handler != null && handler.handle(nodeName, attr, layoutXml); + } + + public AttrNameRouter handler(String attrName, AttributeHandler handler) { + mHandlerMap.put(attrName, handler); + return this; + } + + public AttrNameRouter defaultHandler(AttributeHandler defaultHandler) { + mDefaultHandler = defaultHandler; + return this; + } + } + + class MappedAttributeHandler implements AttributeHandler { + + private Map mAttrNameMap = new HashMap<>(); + private Map> mAttrValueMap = new HashMap<>(); + + @Override + public boolean handle(String nodeName, Node attr, StringBuilder layoutXml) { + if (!attr.getNodeName().equals("style")) { + layoutXml.append("android:"); + } + layoutXml.append(mapAttrName(nodeName, attr.getNodeName())) + .append("=\"").append(mapAttrValue(nodeName, attr.getNodeName(), attr.getNodeValue())).append("\"\n"); + return true; + } + + public MappedAttributeHandler mapName(String oldAttrName, String newAttrName) { + mAttrNameMap.put(oldAttrName, newAttrName); + return this; + } + + public MappedAttributeHandler mapValue(String attrName, String oldValue, String newValue) { + Map valueMap = mAttrValueMap.get(attrName); + if (valueMap == null) { + valueMap = new HashMap<>(); + mAttrValueMap.put(attrName, valueMap); + } + valueMap.put(oldValue, newValue); + return this; + } + + + private String mapAttrName(String nodeName, String attrName) { + String mappedAttrName = mAttrNameMap.get(attrName); + if (mappedAttrName == null) + return attrName; + return mappedAttrName; + } + + private String mapAttrValue(String nodeName, String attrName, String value) { + Map valueMap = mAttrValueMap.get(attrName); + if (valueMap == null) + return value; + String mappedValue = valueMap.get(value); + return mappedValue == null ? value : mappedValue; + } + } + + class IdHandler implements AttributeHandler { + + @Override + public boolean handle(String nodeName, Node attr, StringBuilder layoutXml) { + layoutXml.append("android:id=\"@+id/").append(attr.getNodeValue()).append("\"\n"); + return true; + } + } + + class DimenHandler implements AttributeHandler { + + private String mAttrName; + + public DimenHandler(String attrName) { + mAttrName = attrName; + } + + @Override + public boolean handle(String nodeName, Node attr, StringBuilder layoutXml) { + String dimen = convertToAndroidDimen(attr.getNodeValue()); + layoutXml.append("android:").append(mAttrName).append("=\"").append(dimen).append("\"\n"); + return true; + } + + static String convertToAndroidDimen(String dimen) { + if (dimen.equals("*")) { + return "match_parent"; + } + if (dimen.equals("auto")) { + return "wrap_content"; + } + if (Character.isDigit(dimen.charAt(dimen.length() - 1))) { + return dimen + "dp"; + } + return dimen; + } + } + + class OrientationHandler implements AttributeHandler { + + @Override + public boolean handle(String nodeName, Node attr, StringBuilder layoutXml) { + if (attr.getNodeValue().equals("true")) { + layoutXml.append("android:orientation=\"vertical\"\n"); + } else if (attr.getNodeValue().equals("false")) { + layoutXml.append("android:orientation=\"horizontal\"\n"); + } else { + return false; + } + return true; + } + } + + class MarginPaddingHandler implements AttributeHandler { + + private String mAttrName; + + public MarginPaddingHandler(String attrName) { + mAttrName = attrName; + } + + @Override + public boolean handle(String nodeName, Node attr, StringBuilder layoutXml) { + String[] intervals = attr.getNodeValue().split("[ ,]"); + String[] dimens = new String[intervals.length]; + for (int i = 0; i < intervals.length; i++) { + dimens[i] = DimenHandler.convertToAndroidDimen(intervals[i]); + } + String left, top, right, bottom; + switch (dimens.length) { + case 1: + left = top = right = bottom = dimens[0]; + break; + case 2: + top = bottom = dimens[0]; + left = right = dimens[1]; + break; + case 3: + top = dimens[0]; + left = right = dimens[1]; + bottom = dimens[2]; + break; + case 4: + top = dimens[0]; + right = dimens[1]; + bottom = dimens[2]; + left = dimens[3]; + break; + default: + return false; + } + layoutXml.append("android:").append(mAttrName).append("Top=\"").append(top).append("\"\n") + .append("android:").append(mAttrName).append("Right=\"").append(right).append("\"\n") + .append("android:").append(mAttrName).append("Bottom=\"").append(bottom).append("\"\n") + .append("android:").append(mAttrName).append("Left=\"").append(left).append("\"\n"); + return true; + } + + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/xml/NodeHandler.java b/app/src/main/java/com/stardust/autojs/core/ui/xml/NodeHandler.java new file mode 100644 index 00000000..963b986d --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/xml/NodeHandler.java @@ -0,0 +1,91 @@ +package com.stardust.autojs.core.ui.xml; + +import org.w3c.dom.Node; + +import java.util.HashMap; +import java.util.Map; + +/** + * Created by Stardust on 2017/5/15. + */ + +public interface NodeHandler { + + String handleNode(Node node, StringBuilder layoutXml); + + String handleNode(Node node, String namespace, StringBuilder layoutXml); + + abstract class Adapter implements NodeHandler { + + @Override + public String handleNode(Node node, String namespace, StringBuilder layoutXml) { + String name = handleNode(node, layoutXml); + layoutXml.append(namespace); + return name; + } + } + + + class NameRouter extends Adapter { + + private Map mNodeHandlerMap = new HashMap<>(); + private NodeHandler mDefaultHandler; + + @Override + public String handleNode(Node node, StringBuilder layoutXml) { + NodeHandler handler = mNodeHandlerMap.get(node.getNodeName()); + if (handler != null) { + return handler.handleNode(node, layoutXml); + } + if (mDefaultHandler == null) + return null; + return mDefaultHandler.handleNode(node, layoutXml); + } + + public NameRouter defaultHandler(NodeHandler defaultHandler) { + mDefaultHandler = defaultHandler; + return this; + } + + public NameRouter handler(String name, NodeHandler handler) { + mNodeHandlerMap.put(name, handler); + return this; + } + } + + class MapNameHandler extends Adapter { + + private Map mNameMap = new HashMap<>(); + + @Override + public String handleNode(Node node, StringBuilder layoutXml) { + String name = mNameMap.get(node.getNodeName()); + if (name == null) { + name = node.getNodeName(); + } + layoutXml.append("<").append(name).append("\n"); + return name; + } + + public MapNameHandler map(String oldName, String newName) { + mNameMap.put(oldName, newName); + return this; + } + } + + class VerticalHandler extends Adapter { + + private String mLinearLayoutClassName; + + public VerticalHandler(String linearLayoutClassName) { + mLinearLayoutClassName = linearLayoutClassName; + } + + @Override + public String handleNode(Node node, StringBuilder layoutXml) { + layoutXml.append("<").append(mLinearLayoutClassName) + .append("\nandroid:orientation=\"vertical\"\n"); + return mLinearLayoutClassName; + } + } +} diff --git a/app/src/main/java/com/stardust/autojs/core/ui/xml/XmlConverter.java b/app/src/main/java/com/stardust/autojs/core/ui/xml/XmlConverter.java new file mode 100644 index 00000000..a39de109 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/ui/xml/XmlConverter.java @@ -0,0 +1,166 @@ +package com.stardust.autojs.core.ui.xml; + +import androidx.appcompat.widget.Toolbar; +import androidx.cardview.widget.CardView; +import androidx.drawerlayout.widget.DrawerLayout; + +import android.widget.CheckBox; +import android.widget.DatePicker; +import android.widget.ProgressBar; +import android.widget.RadioButton; +import android.widget.RadioGroup; +import android.widget.ScrollView; +import android.widget.SeekBar; +import android.widget.TimePicker; + +import com.google.android.material.appbar.AppBarLayout; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.stardust.autojs.core.graphics.ScriptCanvasView; +import com.stardust.autojs.core.ui.widget.JsButton; +import com.stardust.autojs.core.ui.widget.JsEditText; +import com.stardust.autojs.core.ui.widget.JsFrameLayout; +import com.stardust.autojs.core.ui.widget.JsGridView; +import com.stardust.autojs.core.ui.widget.JsImageView; +import com.stardust.autojs.core.ui.widget.JsLinearLayout; +import com.stardust.autojs.core.ui.widget.JsListView; +import com.stardust.autojs.core.ui.widget.JsRelativeLayout; +import com.stardust.autojs.core.ui.widget.JsSpinner; +import com.stardust.autojs.core.ui.widget.JsTabLayout; +import com.stardust.autojs.core.ui.widget.JsTextView; +import com.stardust.autojs.core.ui.widget.JsToolbar; +import com.stardust.autojs.core.ui.widget.JsViewPager; +import com.stardust.autojs.core.ui.widget.JsWebView; + +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.io.StringReader; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +/** + * Created by Stardust on 2017/5/14. + */ + +public class XmlConverter { + + private static final NodeHandler NODE_HANDLER = new NodeHandler.NameRouter() + .handler("vertical", new NodeHandler.VerticalHandler(JsLinearLayout.class.getName())) + .defaultHandler(new NodeHandler.MapNameHandler() + .map("frame", JsFrameLayout.class.getName()) + .map("linear", JsLinearLayout.class.getName()) + .map("horizontal", JsLinearLayout.class.getName()) + .map("relative", JsRelativeLayout.class.getName()) + .map("button", JsButton.class.getName()) + .map("text", JsTextView.class.getName()) + .map("input", JsEditText.class.getName()) + .map("img", JsImageView.class.getName()) + .map("datepicker", DatePicker.class.getName()) + .map("timepicker", TimePicker.class.getName()) + .map("webview", JsWebView.class.getName()) + .map("progressbar", ProgressBar.class.getName()) + .map("seekbar", SeekBar.class.getName()) + .map("spinner", JsSpinner.class.getName()) + .map("radio", RadioButton.class.getName()) + .map("radiogroup", RadioGroup.class.getName()) + .map("checkbox", CheckBox.class.getName()) + .map("scroll", ScrollView.class.getName()) + .map("toolbar", JsToolbar.class.getName()) + .map("canvas", ScriptCanvasView.class.getName()) + .map("list", JsListView.class.getName()) + .map("grid", JsGridView.class.getName()) + .map("drawer", DrawerLayout.class.getName()) + .map("appbar", AppBarLayout.class.getName()) + .map("tabs", JsTabLayout.class.getName()) + .map("viewpager", JsViewPager.class.getName()) + .map("card", CardView.class.getName()) + .map("fab", FloatingActionButton.class.getName()) + ); + + private static final AttributeHandler ATTRIBUTE_HANDLER = new AttributeHandler.AttrNameRouter() + .handler("w", new AttributeHandler.DimenHandler("width")) + .handler("h", new AttributeHandler.DimenHandler("height")) + .handler("size", new AttributeHandler.DimenHandler("textSize")) + .handler("id", new AttributeHandler.IdHandler()) + .handler("vertical", new AttributeHandler.OrientationHandler()) + .handler("margin", new AttributeHandler.DimenHandler("layout_margin")) + .handler("padding", new AttributeHandler.DimenHandler("padding")) + .handler("marginLeft", new AttributeHandler.DimenHandler("layout_marginLeft")) + .handler("marginRight", new AttributeHandler.DimenHandler("layout_marginRight")) + .handler("marginTop", new AttributeHandler.DimenHandler("layout_marginTop")) + .handler("marginBottom", new AttributeHandler.DimenHandler("layout_marginBottom")) + .handler("paddingLeft", new AttributeHandler.DimenHandler("paddingLeft")) + .handler("paddingRight", new AttributeHandler.DimenHandler("paddingRight")) + .handler("paddingTop", new AttributeHandler.DimenHandler("paddingTop")) + .handler("paddingBottom", new AttributeHandler.DimenHandler("paddingBottom")) + .defaultHandler(new AttributeHandler.MappedAttributeHandler() + .mapName("align", "layout_gravity") + ); + + public static String convertToAndroidLayout(String xml) throws IOException, SAXException, ParserConfigurationException { + return convertToAndroidLayout(new InputSource(new StringReader(xml))); + } + + public static String convertToAndroidLayout(InputSource source) throws ParserConfigurationException, IOException, SAXException { + StringBuilder layoutXml = new StringBuilder(); + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document document = builder.parse(source); + handleNode(document.getFirstChild(), "xmlns:android=\"http://schemas.android.com/apk/res/android\"", layoutXml); + return layoutXml.toString(); + } + + private static void handleNode(Node node, String namespace, StringBuilder layoutXml) { + String nodeName = node.getNodeName(); + String mappedNodeName = NODE_HANDLER.handleNode(node, namespace, layoutXml); + handleText(nodeName, node.getTextContent(), layoutXml); + handleAttributes(nodeName, node.getAttributes(), layoutXml); + layoutXml.append(">\n"); + handleChildren(node.getChildNodes(), layoutXml); + layoutXml.append("\n"); + } + + private static void handleText(String nodeName, String textContent, StringBuilder layoutXml) { + if (textContent == null || textContent.isEmpty()) { + return; + } + if (nodeName.equals("text") || nodeName.equals("button") || nodeName.equals("input")) + layoutXml.append("android:text=\"").append(textContent).append("\"\n"); + } + + private static void handleChildren(NodeList nodes, StringBuilder layoutXml) { + if (nodes == null) + return; + int len = nodes.getLength(); + for (int i = 0; i < len; i++) { + Node node = nodes.item(i); + if (node.getNodeType() != Node.ELEMENT_NODE) + continue; + handleNode(node, "", layoutXml); + } + } + + + private static void handleAttributes(String nodeName, NamedNodeMap attributes, StringBuilder layoutXml) { + if (attributes == null) + return; + int len = attributes.getLength(); + for (int i = 0; i < len; i++) { + Node attr = attributes.item(i); + handleAttribute(nodeName, attr, layoutXml); + } + } + + private static void handleAttribute(String nodeName, Node attr, StringBuilder layoutXml) { + ATTRIBUTE_HANDLER.handle(nodeName, attr, layoutXml); + } + + +} diff --git a/app/src/main/java/com/stardust/autojs/core/util/ProcessShell.java b/app/src/main/java/com/stardust/autojs/core/util/ProcessShell.java new file mode 100644 index 00000000..26755b48 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/util/ProcessShell.java @@ -0,0 +1,239 @@ +package com.stardust.autojs.core.util; + + +import android.util.Log; + +import com.stardust.autojs.runtime.api.AbstractShell; +import com.stardust.autojs.runtime.exception.ScriptInterruptedException; +import com.stardust.autojs.util.ProcessUtils; +import com.stardust.pio.UncheckedIOException; + +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +/** + * Created by Stardust on 2017/1/20. + *

+ * 来自网络~~ + */ + +public class ProcessShell extends AbstractShell { + + private static final String TAG = "ProcessShell"; + + private Process mProcess; + private DataOutputStream mCommandOutputStream; + private BufferedReader mSucceedReader; + private BufferedReader mErrorReader; + + private StringBuilder mSucceedOutput = new StringBuilder(); + private StringBuilder mErrorOutput = new StringBuilder(); + + public ProcessShell() { + + } + + public ProcessShell(boolean root) { + super(root); + } + + @Override + protected void init(String initialCommand) { + try { + mProcess = new ProcessBuilder(initialCommand).redirectErrorStream(true).start(); + mCommandOutputStream = new DataOutputStream(mProcess.getOutputStream()); + mSucceedReader = new BufferedReader(new InputStreamReader(mProcess.getInputStream())); + mErrorReader = new BufferedReader(new InputStreamReader(mProcess.getErrorStream())); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public void exec(String command) { + try { + mCommandOutputStream.writeBytes(command); + if (!command.endsWith(COMMAND_LINE_END)) { + mCommandOutputStream.writeBytes(COMMAND_LINE_END); + } + mCommandOutputStream.flush(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void exit() { + if (mProcess != null) { + Log.d(TAG, "exit: pid = " + ProcessUtils.getProcessPid(mProcess)); + mProcess.destroy(); + mProcess = null; + } + if (mSucceedReader != null) { + try { + mSucceedReader.close(); + } catch (IOException ignored) { + + } + mSucceedReader = null; + } + if (mErrorReader != null) { + try { + mErrorReader.close(); + } catch (IOException ignored) { + + } + mErrorReader = null; + } + + } + + @Override + public void exitAndWaitFor() { + exec(COMMAND_EXIT); + waitFor(); + exit(); + } + + public int waitFor() { + try { + return mProcess.waitFor(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + public ProcessShell readAll() { + return readSucceedOutput().readErrorOutput(); + } + + public ProcessShell readSucceedOutput() { + read(mSucceedReader, mSucceedOutput); + return this; + } + + private void read(BufferedReader reader, StringBuilder sb) { + try { + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public ProcessShell readErrorOutput() { + read(mErrorReader, mErrorOutput); + return this; + } + + public StringBuilder getSucceedOutput() { + return mSucceedOutput; + } + + public StringBuilder getErrorOutput() { + return mErrorOutput; + } + + public Process getProcess() { + return mProcess; + } + + public BufferedReader getSucceedReader() { + return mSucceedReader; + } + + public BufferedReader getErrorReader() { + return mErrorReader; + } + + public static Result exec(String command, boolean isRoot) { + String[] commands = command.split("\n"); + return exec(commands, isRoot); + } + + public static Result exec(String[] commands, boolean isRoot) { + ProcessShell shell = null; + try { + shell = new ProcessShell(isRoot); + for (String command : commands) { + shell.exec(command); + } + shell.exec(COMMAND_EXIT); + Result result = new Result(); + result.code = shell.waitFor(); + shell.readAll(); + result.error = shell.getErrorOutput().toString(); + result.result = shell.getSucceedOutput().toString(); + shell.exit(); + return result; + } finally { + if (shell != null) { + shell.exit(); + } + } + } + + public static Result execCommand(String[] commands, boolean isRoot) { + Result commandResult = new Result(); + if (commands == null || commands.length == 0) + throw new IllegalArgumentException("command is empty"); + Process process = null; + DataOutputStream os = null; + try { + process = Runtime.getRuntime().exec(isRoot ? COMMAND_SU : COMMAND_SH); + os = new DataOutputStream(process.getOutputStream()); + for (String command : commands) { + if (command != null) { + os.write(command.getBytes()); + os.writeBytes(COMMAND_LINE_END); + os.flush(); + } + } + os.writeBytes(COMMAND_EXIT); + os.flush(); + Log.d(TAG, "pid = " + ProcessUtils.getProcessPid(process)); + commandResult.code = process.waitFor(); + commandResult.result = readAll(process.getInputStream()); + commandResult.error = readAll(process.getErrorStream()); + Log.d(TAG, commandResult.toString()); + } catch (Exception e) { + throw new ScriptInterruptedException(e); + } finally { + try { + if (os != null) os.close(); + if (process != null) { + process.getInputStream().close(); + process.getOutputStream().close(); + } + } catch (IOException ignored) { + + } + if (process != null) { + process.destroy(); + } + } + return commandResult; + } + + private static String readAll(InputStream inputStream) throws IOException { + String line; + StringBuilder builder = new StringBuilder(); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + while ((line = reader.readLine()) != null) { + builder.append(line).append('\n'); + } + return builder.toString(); + } + + public static Result execCommand(String command, boolean isRoot) { + String[] commands = command.split("\n"); + return execCommand(commands, isRoot); + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/stardust/autojs/core/util/ScriptPromiseAdapter.kt b/app/src/main/java/com/stardust/autojs/core/util/ScriptPromiseAdapter.kt new file mode 100644 index 00000000..3e116da5 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/util/ScriptPromiseAdapter.kt @@ -0,0 +1,49 @@ +package com.stardust.autojs.core.util + +import com.stardust.autojs.core.internal.Functions + +class ScriptPromiseAdapter { + + interface Callback { + fun call(arg: Any?) + } + + private var mResolveCallback: Callback? = null + private var mRejectCallback: Callback? = null + private var mResult: Any? = UNSET + private var mError: Any? = UNSET + + fun onResolve(callback: Callback): ScriptPromiseAdapter { + mResolveCallback = callback + mResult.let { + if (it !== UNSET) { + callback.call(it) + } + } + return this + } + + fun onReject(callback: Callback): ScriptPromiseAdapter { + mRejectCallback = callback + mError.let { + if (it !== UNSET) { + callback.call(it) + } + } + return this + } + + fun resolve(result: Any?) { + mResult = result + mResolveCallback?.call(result) + } + + fun reject(error: Any?) { + mError = error + mRejectCallback?.call(error) + } + + companion object { + private val UNSET = Object() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/stardust/autojs/core/util/Shell.java b/app/src/main/java/com/stardust/autojs/core/util/Shell.java new file mode 100644 index 00000000..c1a44a2b --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/util/Shell.java @@ -0,0 +1,321 @@ +package com.stardust.autojs.core.util; + +import android.content.Context; +import android.os.Handler; +import android.preference.PreferenceManager; +import android.util.Log; + +import com.stardust.autojs.runtime.ScriptRuntime; +import com.stardust.autojs.runtime.api.AbstractShell; +import com.stardust.autojs.runtime.exception.ScriptInterruptedException; +import com.stardust.pio.UncheckedIOException; + +import java.io.IOException; +import java.util.ArrayList; + +import jackpal.androidterm.ShellTermSession; +import jackpal.androidterm.emulatorview.TermSession; +import jackpal.androidterm.util.TermSettings; + +/** + * Created by Stardust on 2017/4/24. + */ + +public class Shell extends AbstractShell { + + public interface Callback { + + void onOutput(String str); + + void onNewLine(String line); + + void onInitialized(); + + void onInterrupted(InterruptedException e); + } + + public static class SimpleCallback implements Callback { + + @Override + public void onOutput(String str) { + + } + + @Override + public void onNewLine(String str) { + + } + + @Override + public void onInitialized() { + + } + + @Override + public void onInterrupted(InterruptedException e) { + + } + } + + private static final boolean DEBUG = true; + private static final String TAG = "Shell"; + + private volatile TermSession mTermSession; + private final Object mInitLock = new Object(); + private final Object mExitLock = new Object(); + private final Object mCommandOutputLock = new Object(); + private volatile RuntimeException mInitException; + private volatile boolean mInitialized = false; + private volatile boolean mWaitingExit = false; + private volatile String mCommandOutput = null; + private final boolean mShouldReadOutput; + private Callback mCallback; + + public Shell(Context context) { + this(context, false); + } + + public Shell(Context context, boolean root) { + this(context, root, true); + } + + public Shell(Context context, boolean root, boolean shouldReadOutput) { + super(context, root); + mShouldReadOutput = shouldReadOutput; + } + + public Shell() { + this(false); + } + + public Shell(boolean root) { + this(ScriptRuntime.getApplicationContext(), root); + } + + @Override + protected void init(final String initialCommand) { + Handler uiHandler = new Handler(mContext.getMainLooper()); + uiHandler.post(() -> { + TermSettings settings = new TermSettings(mContext.getResources(), PreferenceManager.getDefaultSharedPreferences(mContext)); + try { + mTermSession = new MyShellTermSession(settings, initialCommand); + mTermSession.initializeEmulator(1024, 40); + } catch (IOException e) { + mInitException = new UncheckedIOException(e); + } + }); + } + + public void exec(String command) { + ensureInitialized(); + mTermSession.write(command + "\n"); + } + + public String execAndWaitFor(String command) { + exec(command); + synchronized (mCommandOutputLock) { + try { + mCommandOutputLock.wait(); + return mCommandOutput; + } catch (InterruptedException e) { + throw new ScriptInterruptedException(); + } + } + } + + public void setCallback(Callback callback) { + mCallback = callback; + } + + public boolean isInitialized() { + return mInitialized; + } + + private void ensureInitialized() { + if (mTermSession == null) { + logDebug("ensureInitialized: not init"); + checkInitException(); + waitInitialization(); + if (mTermSession == null) { + checkInitException(); + throw new IllegalStateException(); + } + } else { + logDebug("ensureInitialized: init"); + } + } + + private void logDebug(String log) { + if (DEBUG) { + Log.d(TAG, log); + } + } + + private void checkInitException() { + if (mInitException != null) { + throw mInitException; + } + } + + private void waitInitialization() { + synchronized (mInitLock) { + if (mInitialized) { + return; + } + logDebug("waitInitialization: enter"); + try { + mInitLock.wait(); + logDebug("waitInitialization: exit"); + } catch (InterruptedException e) { + onInterrupted(e); + } + } + } + + private void onInterrupted(InterruptedException e) { + if (mCallback == null) { + exit(); + throw new ScriptInterruptedException(); + } else { + mCallback.onInterrupted(e); + } + } + + @Override + public void exit() { + mTermSession.finish(); + } + + @Override + public void exitAndWaitFor() { + execExitAndWait(); + if (!isRoot()) { + return; + } + execExitAndWait(); + } + + private void execExitAndWait() { + synchronized (mExitLock) { + mWaitingExit = true; + exec("exit"); + try { + mExitLock.wait(); + } catch (InterruptedException e) { + onInterrupted(e); + } + } + } + + public TermSession getTermSession() { + return mTermSession; + } + + private class MyShellTermSession extends ShellTermSession { + + private StringBuilder mStringBuffer = new StringBuilder(); + private ArrayList mCommandOutputs = new ArrayList<>(); + + public MyShellTermSession(TermSettings settings, String initialCommand) throws IOException { + super(settings, initialCommand); + } + + private void onNewLine(String line) { + logDebug("onNewLine: " + line); + if (!mInitialized) { + if (!isRoot() && line.endsWith(" $ sh")) { + notifyInitialized(); + } + } else { + mCommandOutputs.add(line); + } + if (mCallback != null) { + mCallback.onNewLine(line); + } + if (mWaitingExit && line.endsWith(" exit")) { + notifyExit(); + } + } + + private void onCommandOutput(ArrayList output) { + StringBuilder result = new StringBuilder(); + for (int i = 1; i < output.size(); i++) { + result.append(output.get(i)); + if (i < output.size() - 1) { + result.append("\n"); + } + } + logDebug("onCommandOutput: lines = " + output + ", output = " + result); + synchronized (mCommandOutputLock) { + mCommandOutput = result.toString(); + mCommandOutputLock.notifyAll(); + } + } + + private void onOutput(String str) { + logDebug("onOutput: " + str); + if (!mInitialized) { + if (isRoot() && str.endsWith(":/ # ")) { + notifyInitialized(); + } + } + int start = 0; + int i; + while (true) { + i = str.indexOf("\n", start); + if (i > 0) { + onNewLine((mStringBuffer.toString() + str.substring(0, i - 1)).trim()); + mStringBuffer.delete(0, mStringBuffer.length()); + } else { + if (start <= str.length() - 1) { + mStringBuffer.append(str.substring(start)); + } + break; + } + start = i + 1; + } + if (str.endsWith(" # ") || str.endsWith(" $ ")) { + onCommandOutput(mCommandOutputs); + mCommandOutputs.clear(); + } + if (mCallback != null) { + mCallback.onOutput(str.replace("\r", "")); + } + } + + + @Override + protected void processInput(byte[] data, int offset, int count) { + onOutput(new String(data, offset, count)); + } + + private void notifyExit() { + synchronized (mExitLock) { + mWaitingExit = false; + mExitLock.notify(); + } + } + + private void notifyInitialized() { + logDebug("notifyInitialized"); + mInitialized = true; + synchronized (mInitLock) { + mInitLock.notifyAll(); + } + if (mCallback != null) { + mCallback.onInitialized(); + } + } + + @Override + protected void onProcessExit() { + super.onProcessExit(); + synchronized (mExitLock) { + mWaitingExit = false; + mExitLock.notify(); + } + } + + } + +} diff --git a/app/src/main/java/com/stardust/autojs/core/web/InjectableWebClient.java b/app/src/main/java/com/stardust/autojs/core/web/InjectableWebClient.java new file mode 100644 index 00000000..95eee69c --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/web/InjectableWebClient.java @@ -0,0 +1,138 @@ +package com.stardust.autojs.core.web; + +import android.annotation.SuppressLint; +import android.util.Log; +import android.util.Pair; +import android.webkit.JavascriptInterface; +import android.webkit.ValueCallback; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; + + +import com.stardust.autojs.runtime.exception.ScriptInterruptedException; + +import org.mozilla.javascript.Context; +import org.mozilla.javascript.Scriptable; + +import java.util.LinkedList; +import java.util.Queue; + +/** + * Created by Stardust on 2017/4/1. + */ + +public class InjectableWebClient extends WebViewClient { + + private static final String TAG = "InjectableWebClient"; + + private Queue>> mToInjectJavaScripts = new LinkedList<>(); + private final ValueCallback defaultCallback = new ValueCallback() { + @Override + public void onReceiveValue(String value) { + Log.i(TAG, "onReceiveValue: " + value); + } + }; + private WebView mWebView; + private Context mContext; + private Scriptable mScriptable; + private ScriptBridge mScriptBridge = new ScriptBridge(); + + public InjectableWebClient(Context context, Scriptable scriptable) { + mContext = context; + mScriptable = scriptable; + } + + @Override + public void onPageFinished(WebView view, String url) { + mWebView = view; + setUpWebView(view); + while (!mToInjectJavaScripts.isEmpty()) { + Pair> pair = mToInjectJavaScripts.poll(); + inject(view, pair.first, pair.second); + } + super.onPageFinished(view, url); + } + + @SuppressLint("SetJavaScriptEnabled") + private void setUpWebView(WebView view) { + view.addJavascriptInterface(mScriptBridge, "rhino"); + WebSettings webSettings = view.getSettings(); + webSettings.setJavaScriptEnabled(true); + webSettings.setAllowUniversalAccessFromFileURLs(true); + } + + private void inject(WebView view, String script, ValueCallback callback) { + view.evaluateJavascript(script, callback); + } + + public void inject(String script, ValueCallback callback) { + if (mWebView != null) { + inject(mWebView, script, callback); + return; + } + mToInjectJavaScripts.offer(new Pair<>(script, callback)); + } + + public void inject(String script) { + inject(script, defaultCallback); + } + + public String injectAndWait(String script) { + InjectReturnCallback callback = new InjectReturnCallback(); + inject(script, callback); + return callback.waitResult(); + } + + + private class ScriptBridge { + + private Object result; + + @JavascriptInterface + public String eval(final String script) { + result = null; + mWebView.post(() -> { + Log.v(TAG, "ScriptBridge.eval: " + script); + result = mContext.evaluateString(mScriptable, script, "", 1, null); + Log.v(TAG, "ScriptBridge.eval = " + result); + synchronized (ScriptBridge.this) { + ScriptBridge.this.notify(); + } + }); + synchronized (ScriptBridge.this) { + try { + ScriptBridge.this.wait(); + } catch (InterruptedException e) { + throw new ScriptInterruptedException(); + } + } + return result.toString(); + } + } + + private static class InjectReturnCallback implements ValueCallback { + + private String result; + + @Override + public void onReceiveValue(String value) { + result = value; + synchronized (this) { + this.notify(); + } + } + + String waitResult() { + synchronized (this) { + try { + this.wait(); + } catch (InterruptedException e) { + throw new ScriptInterruptedException(); + } + } + return result; + } + } + +} diff --git a/app/src/main/java/com/stardust/autojs/core/web/InjectableWebView.java b/app/src/main/java/com/stardust/autojs/core/web/InjectableWebView.java new file mode 100644 index 00000000..7e34f406 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/core/web/InjectableWebView.java @@ -0,0 +1,39 @@ +package com.stardust.autojs.core.web; + +import android.content.Context; +import android.webkit.ValueCallback; +import android.webkit.WebView; + +import com.stardust.autojs.annotation.ScriptClass; +import com.stardust.autojs.annotation.ScriptInterface; + +import org.mozilla.javascript.Scriptable; + +/** + * Created by Stardust on 2017/4/1. + */ +@ScriptClass +public class InjectableWebView extends WebView { + + private InjectableWebClient mInjectableWebClient; + + public InjectableWebView(Context context, org.mozilla.javascript.Context jsCtx, Scriptable scriptable) { + super(context); + init(jsCtx, scriptable); + } + + private void init(org.mozilla.javascript.Context jsCtx, Scriptable scriptable) { + mInjectableWebClient = new InjectableWebClient(jsCtx, scriptable); + setWebViewClient(mInjectableWebClient); + } + + @ScriptInterface + public void inject(String script, ValueCallback callback) { + mInjectableWebClient.inject(script, callback); + } + + @ScriptInterface + public void inject(String script) { + mInjectableWebClient.inject(script); + } +} diff --git a/app/src/main/java/com/stardust/autojs/engine/JavaScriptEngine.java b/app/src/main/java/com/stardust/autojs/engine/JavaScriptEngine.java new file mode 100644 index 00000000..586ade0d --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/engine/JavaScriptEngine.java @@ -0,0 +1,71 @@ +package com.stardust.autojs.engine; + +import com.stardust.autojs.runtime.ScriptRuntime; +import com.stardust.autojs.script.JavaScriptSource; +import com.stardust.autojs.script.ScriptSource; + +/** + * Created by Stardust on 2017/8/3. + */ + +public abstract class JavaScriptEngine extends ScriptEngine.AbstractScriptEngine { + private ScriptRuntime mRuntime; + private Object mExecArgv; + + @Override + public Object execute(JavaScriptSource scriptSource) { + if ((scriptSource.getExecutionMode() & JavaScriptSource.EXECUTION_MODE_AUTO) != 0) { + getRuntime().ensureAccessibilityServiceEnabled(); + } + return doExecution(scriptSource); + } + + protected abstract Object doExecution(JavaScriptSource scriptSource); + + public ScriptRuntime getRuntime() { + return mRuntime; + } + + public void setRuntime(ScriptRuntime runtime) { + if (mRuntime != null) { + throw new IllegalStateException("a runtime has been set"); + } + mRuntime = runtime; + mRuntime.engines.setCurrentEngine(this); + put("runtime", runtime); + } + + public void emit(String eventName, Object... args) { + mRuntime.timers.getMainTimer().postDelayed(() -> mRuntime.events.emit(eventName, args), 0); + } + + public ScriptSource getSource() { + return (ScriptSource) getTag(TAG_SOURCE); + } + + public void setExecArgv(Object execArgv) { + if (mExecArgv != null) { + return; + } + mExecArgv = execArgv; + } + + public Object getExecArgv() { + return mExecArgv; + } + + @Override + public synchronized void destroy() { + mRuntime.onExit(); + super.destroy(); + } + + @Override + public String toString() { + return "ScriptEngine@" + Integer.toHexString(hashCode()) + "{" + + "id=" + getId() + "," + + "source='" + getTag(TAG_SOURCE) + "'," + + "cwd='" + cwd() + "'" + + "}"; + } +} diff --git a/app/src/main/java/com/stardust/autojs/engine/LoopBasedJavaScriptEngine.java b/app/src/main/java/com/stardust/autojs/engine/LoopBasedJavaScriptEngine.java new file mode 100644 index 00000000..9c118f70 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/engine/LoopBasedJavaScriptEngine.java @@ -0,0 +1,102 @@ +package com.stardust.autojs.engine; + +import android.app.Activity; +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.os.MessageQueue; +import android.util.Log; + +import com.stardust.autojs.core.looper.LooperHelper; +import com.stardust.autojs.script.JavaScriptSource; +import com.stardust.autojs.script.ScriptSource; +import com.stardust.util.Callback; + +import org.mozilla.javascript.ContinuationPending; + +/** + * Created by Stardust on 2017/7/28. + */ + +public class LoopBasedJavaScriptEngine extends RhinoJavaScriptEngine { + + public interface ExecuteCallback { + void onResult(Object r); + + void onException(Exception e); + } + + private Handler mHandler; + private boolean mLooping = false; + + public LoopBasedJavaScriptEngine(Context context) { + super(context); + } + + @Override + public Object execute(final JavaScriptSource source) { + execute(source, null); + return null; + } + + + public void execute(final ScriptSource source, final ExecuteCallback callback) { + Runnable r = () -> { + try { + Object o = LoopBasedJavaScriptEngine.super.execute((JavaScriptSource) source); + if (callback != null) + callback.onResult(o); + } catch (ContinuationPending ignored) { + } catch (Exception e) { + if (callback == null) { + throw e; + } else { + callback.onException(e); + } + } + + + }; + mHandler.post(r); + if (!mLooping && Looper.myLooper() != Looper.getMainLooper()) { + mLooping = true; + while (true) { + try { + Looper.loop(); + } catch (ContinuationPending ignored) { + continue; + } catch (Throwable t) { + mLooping = false; + throw t; + } + break; + } + } + } + + @Override + public void forceStop() { + LooperHelper.quitForThread(getThread()); + Activity activity = (Activity) getTag("activity"); + if (activity != null) { + activity.finish(); + } + super.forceStop(); + } + + @Override + public synchronized void destroy() { + Thread thread = getThread(); + LooperHelper.quitForThread(thread); + super.destroy(); + } + + @Override + public void init() { + LooperHelper.prepare(); + mHandler = new Handler(); + super.init(); + } + + +} diff --git a/app/src/main/java/com/stardust/autojs/engine/RhinoJavaScriptEngine.kt b/app/src/main/java/com/stardust/autojs/engine/RhinoJavaScriptEngine.kt new file mode 100644 index 00000000..bd9f317d --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/engine/RhinoJavaScriptEngine.kt @@ -0,0 +1,193 @@ +package com.stardust.autojs.engine + +import android.util.Log +import android.view.View +import com.stardust.autojs.core.ui.ViewExtras +import com.stardust.autojs.engine.module.AssetAndUrlModuleSourceProvider +import com.stardust.autojs.execution.ExecutionConfig +import com.stardust.autojs.project.ScriptConfig +import com.stardust.autojs.rhino.RhinoAndroidHelper +import com.stardust.autojs.rhino.TopLevelScope +import com.stardust.autojs.runtime.ScriptRuntime +import com.stardust.autojs.script.JavaScriptSource +import com.stardust.automator.UiObjectCollection +import com.stardust.pio.UncheckedIOException +import org.mozilla.javascript.* +import org.mozilla.javascript.commonjs.module.RequireBuilder +import org.mozilla.javascript.commonjs.module.provider.SoftCachingModuleScriptProvider +import java.io.File +import java.io.IOException +import java.io.InputStreamReader +import java.io.Reader +import java.net.URI +import java.util.* +import java.util.concurrent.ConcurrentHashMap + +/** + * Created by Stardust on 2017/4/2. + */ + +open class RhinoJavaScriptEngine(private val mAndroidContext: android.content.Context) : JavaScriptEngine() { + + val context: Context + private val mScriptable: TopLevelScope + lateinit var thread: Thread + private set + + private val initScript: Script + get() { + return sInitScript ?: { + try { + val reader = InputStreamReader(mAndroidContext.assets.open("init.js")) + val script = context.compileReader(reader, SOURCE_NAME_INIT, 1, null) + sInitScript = script + script + } catch (e: IOException) { + throw UncheckedIOException(e) + } + }() + } + + val scriptable: Scriptable + get() = mScriptable + + init { + this.context = enterContext() + mScriptable = createScope(this.context) + } + + override fun put(name: String, value: Any?) { + ScriptableObject.putProperty(mScriptable, name, Context.javaToJS(value, mScriptable)) + } + + override fun setRuntime(runtime: ScriptRuntime) { + super.setRuntime(runtime) + runtime.topLevelScope = mScriptable + } + + public override fun doExecution(source: JavaScriptSource): Any? { + var reader = source.nonNullScriptReader + try { + reader = preprocess(reader) + val script = context.compileReader(reader, source.toString(), 1, null) + return if (hasFeature(ScriptConfig.FEATURE_CONTINUATION)) { + context.executeScriptWithContinuations(script, mScriptable) + } else { + script.exec(context, mScriptable) + } + } catch (e: IOException) { + throw UncheckedIOException(e) + } + + } + + fun hasFeature(feature: String): Boolean { + val config = getTag(ExecutionConfig.tag) as ExecutionConfig? + return config != null && config.scriptConfig.hasFeature(feature) + } + + @Throws(IOException::class) + protected fun preprocess(script: Reader): Reader { + return script + } + + override fun forceStop() { + Log.d(LOG_TAG, "forceStop: interrupt Thread: $thread") + thread.interrupt() + } + + + @Synchronized + override fun destroy() { + super.destroy() + Log.d(LOG_TAG, "on destroy") + sContextEngineMap.remove(context) + Context.exit() + } + + override fun init() { + thread = Thread.currentThread() + ScriptableObject.putProperty(mScriptable, "__engine__", this) + initRequireBuilder(context, mScriptable) + try { + context.executeScriptWithContinuations(initScript, mScriptable) + } catch (e: IllegalArgumentException) { + if ("Script argument was not a script or was not created by interpreted mode " == e.message) { + initScript.exec(context, mScriptable) + } else { + throw e + } + } + } + + internal fun initRequireBuilder(context: Context, scope: Scriptable) { + val provider = AssetAndUrlModuleSourceProvider(mAndroidContext, MODULES_PATH, + listOf(File("/").toURI())) + RequireBuilder() + .setModuleScriptProvider(SoftCachingModuleScriptProvider(provider)) + .setSandboxed(true) + .createRequire(context, scope) + .install(scope) + + } + + protected fun createScope(context: Context): TopLevelScope { + val topLevelScope = TopLevelScope() + topLevelScope.initStandardObjects(context, false) + return topLevelScope + } + + fun enterContext(): Context { + val context = RhinoAndroidHelper(mAndroidContext).enterContext() + setupContext(context) + sContextEngineMap[context] = this + return context + } + + protected fun setupContext(context: Context) { + @Suppress("DEPRECATION") + context.optimizationLevel = -1 + context.languageVersion = Context.VERSION_ES6 + context.locale = Locale.getDefault() + context.wrapFactory = WrapFactory() + } + + private inner class WrapFactory : org.mozilla.javascript.WrapFactory() { + + override fun wrap(cx: Context, scope: Scriptable, obj: Any?, staticType: Class<*>?): Any? { + return when { + obj is String -> runtime.bridges.toString(obj.toString()) + staticType == UiObjectCollection::class.java -> runtime.bridges.asArray(obj) + else -> super.wrap(cx, scope, obj, staticType) + } + } + + override fun wrapAsJavaObject(cx: Context?, scope: Scriptable, javaObject: Any?, staticType: Class<*>?): Scriptable? { + //Log.d(LOG_TAG, "wrapAsJavaObject: java = " + javaObject + ", result = " + result + ", scope = " + scope); + return if (javaObject is View) { + ViewExtras.getNativeView(scope, javaObject, staticType, runtime) + } else { + super.wrapAsJavaObject(cx, scope, javaObject, staticType) + } + } + + } + + companion object { + + val SOURCE_NAME_INIT = "" + + private val LOG_TAG = "RhinoJavaScriptEngine" + + private val MODULES_PATH = "modules" + private var sInitScript: Script? = null + private val sContextEngineMap = ConcurrentHashMap() + + + fun getEngineOfContext(context: Context): RhinoJavaScriptEngine? { + return sContextEngineMap[context] + } + } + + +} diff --git a/app/src/main/java/com/stardust/autojs/engine/RootAutomatorEngine.java b/app/src/main/java/com/stardust/autojs/engine/RootAutomatorEngine.java new file mode 100644 index 00000000..b4d5f02d --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/engine/RootAutomatorEngine.java @@ -0,0 +1,161 @@ +package com.stardust.autojs.engine; + +import android.content.Context; +import android.preference.PreferenceManager; +import android.util.Log; + +import com.stardust.autojs.core.util.ProcessShell; +import com.stardust.autojs.core.inputevent.InputDevices; +import com.stardust.autojs.runtime.exception.ScriptException; +import com.stardust.autojs.runtime.exception.ScriptInterruptedException; +import com.stardust.autojs.script.AutoFileSource; +import com.stardust.pio.PFiles; + +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Created by Stardust on 2017/8/1. + */ + +public class RootAutomatorEngine extends ScriptEngine.AbstractScriptEngine { + + public static final int VERSION = 1; + + private static final String KEY_TOUCH_DEVICE = RootAutomatorEngine.class.getName() + ".touch_device"; + private static final String LOG_TAG = "RootAutomatorEngine"; + private static final Pattern PID_PATTERN = Pattern.compile("[0-9]{2,}"); + + private static int sTouchDevice = -1; + private static final String ROOT_AUTOMATOR_EXECUTABLE_ASSET = "binary/root_automator"; + + private Context mContext; + private final String mDeviceNameOrPath; + private Thread mThread; + private String mExecutablePath; + private String mPid; + private Process mProcess; + + public RootAutomatorEngine(Context context, String deviceNameOrPath) { + mContext = context; + mDeviceNameOrPath = getDeviceNameOrPath(context, deviceNameOrPath); + } + + + public RootAutomatorEngine(Context context) { + this(context, InputDevices.getTouchDeviceName()); + } + + public void execute(String autoFile) { + mExecutablePath = getExecutablePath(mContext); + Log.d(LOG_TAG, "exec: " + autoFile); + final String[] commands = { + "chmod 755 " + mExecutablePath, + String.format("\"%s\" \"%s\" -d \"%s\" &", mExecutablePath, autoFile, mDeviceNameOrPath), // to run root_automator + "echo $!", // to print the root_automator pid + "exit", // to exit su + "exit" // to exit shell + }; + try { + mProcess = Runtime.getRuntime().exec("su"); + executeCommands(mProcess, commands); + mPid = readPid(mProcess); + mProcess.waitFor(); + } catch (IOException e) { + throw new ScriptException(e); + } catch (InterruptedException e) { + throw new ScriptInterruptedException(); + } finally { + mProcess.destroy(); + mProcess = null; + } + } + + private String readPid(Process process) throws IOException { + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + String line; + while ((line = reader.readLine()) != null) { + Matcher matcher = PID_PATTERN.matcher(line); + if (matcher.find()) { + return matcher.group(); + } + } + return null; + } + + private void executeCommands(Process process, String[] commands) throws IOException { + DataOutputStream os = new DataOutputStream(process.getOutputStream()); + for (String command : commands) { + if (command != null) { + os.write(command.getBytes()); + os.writeBytes("\n"); + } + } + os.flush(); + } + + + public static String getDeviceNameOrPath(Context context, String deviceNameOrPath) { + if (sTouchDevice < 0) { + sTouchDevice = PreferenceManager.getDefaultSharedPreferences(context).getInt(KEY_TOUCH_DEVICE, -1); + } + if (sTouchDevice >= 0) { + deviceNameOrPath = "/dev/input/event" + sTouchDevice; + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putInt(KEY_TOUCH_DEVICE, sTouchDevice) + .apply(); + } + return deviceNameOrPath; + } + + public static String getExecutablePath(Context context) { + File tmp = new File(context.getCacheDir(), "root_automator"); + PFiles.copyAsset(context, ROOT_AUTOMATOR_EXECUTABLE_ASSET, tmp.getAbsolutePath()); + return tmp.getAbsolutePath(); + } + + public static void setTouchDevice(int device) { + sTouchDevice = device; + } + + public static int getTouchDevice(Context context) { + if (sTouchDevice >= 0) + return sTouchDevice; + return PreferenceManager.getDefaultSharedPreferences(context).getInt(KEY_TOUCH_DEVICE, -1); + } + + @Override + public void put(String name, Object value) { + + } + + @Override + public Object execute(AutoFileSource source) { + execute(source.getFile().getAbsolutePath()); + return null; + } + + @Override + public void forceStop() { + mThread.interrupt(); + if (mPid != null) { + ProcessShell.exec("kill " + mPid, true); + } + } + + @Override + public void init() { + mThread = Thread.currentThread(); + } + + @Override + public synchronized void destroy() { + super.destroy(); + } +} diff --git a/app/src/main/java/com/stardust/autojs/engine/ScriptEngine.java b/app/src/main/java/com/stardust/autojs/engine/ScriptEngine.java new file mode 100644 index 00000000..46312834 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/engine/ScriptEngine.java @@ -0,0 +1,137 @@ +package com.stardust.autojs.engine; + +import androidx.annotation.CallSuper; + +import com.stardust.autojs.execution.ScriptExecution; +import com.stardust.autojs.script.ScriptSource; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Created by Stardust on 2017/4/2. + *

+ *

+ * A ScriptEngine is created by {@link ScriptEngineManager#createEngine(String, int)} ()}, and then can be + * used to execute script with {@link ScriptEngine#execute(ScriptSource)} in the **same** thread. + * When the execution finish successfully, the engine should be destroy in the thread that created it. + *

+ * If you want to stop the engine in other threads, you should call {@link ScriptEngine#forceStop()}. + */ + +public interface ScriptEngine { + + + String TAG_ENV_PATH = "env_path"; + String TAG_SOURCE = "source"; + String TAG_WORKING_DIRECTORY = "execute_path"; + + void put(String name, Object value); + + Object execute(S scriptSource); + + void forceStop(); + + void destroy(); + + boolean isDestroyed(); + + void setTag(String key, Object value); + + Object getTag(String key); + + String cwd(); + + void uncaughtException(Throwable throwable); + + Throwable getUncaughtException(); + + void setId(int id); + + int getId(); + + /** + * @hide + */ + void setOnDestroyListener(OnDestroyListener listener); + + /** + * @hide + */ + void init(); + + interface OnDestroyListener { + void onDestroy(ScriptEngine engine); + } + + abstract class AbstractScriptEngine implements ScriptEngine { + + + private Map mTags = new ConcurrentHashMap<>(); + private OnDestroyListener mOnDestroyListener; + private volatile boolean mDestroyed = false; + private Throwable mUncaughtException; + private volatile AtomicInteger mId = new AtomicInteger(ScriptExecution.NO_ID); + + @Override + public void setTag(String key, Object value) { + if (value == null) { + mTags.remove(key); + } else { + mTags.put(key, value); + } + } + + @Override + public Object getTag(String key) { + return mTags.get(key); + } + + @Override + public boolean isDestroyed() { + return mDestroyed; + } + + @CallSuper + @Override + public void destroy() { + if (mOnDestroyListener != null) { + mOnDestroyListener.onDestroy(this); + } + mDestroyed = true; + } + + public String cwd() { + return (String) getTag(TAG_WORKING_DIRECTORY); + } + + public void setOnDestroyListener(OnDestroyListener onDestroyListener) { + if (mOnDestroyListener != null) + throw new SecurityException("setOnDestroyListener can be called only once"); + mOnDestroyListener = onDestroyListener; + } + + @Override + public void uncaughtException(Throwable throwable) { + mUncaughtException = throwable; + forceStop(); + } + + @Override + public Throwable getUncaughtException() { + return mUncaughtException; + } + + @Override + public void setId(int id) { + mId.compareAndSet(ScriptExecution.NO_ID, id); + } + + @Override + public int getId() { + return mId.get(); + } + } +} diff --git a/app/src/main/java/com/stardust/autojs/engine/ScriptEngineFactory.java b/app/src/main/java/com/stardust/autojs/engine/ScriptEngineFactory.java new file mode 100644 index 00000000..f374f5de --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/engine/ScriptEngineFactory.java @@ -0,0 +1,89 @@ +package com.stardust.autojs.engine; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.stardust.autojs.script.ScriptSource; +import com.stardust.util.Supplier; + +import java.util.HashMap; +import java.util.Map; + +/** + * Created by Stardust on 2017/8/2. + */ + +public class ScriptEngineFactory { + + public static class EngineNotFoundException extends RuntimeException { + + public EngineNotFoundException(String s) { + super(s); + } + } + + private static ScriptEngineFactory sInstance = new ScriptEngineFactory(); + private Map> mEngines = new HashMap<>(); + private Map mGlobalVariableMap = new HashMap<>(); + + ScriptEngineFactory() { + + } + + public static ScriptEngineFactory getInstance() { + return sInstance; + } + + public void putGlobal(String varName, Object value) { + mGlobalVariableMap.put(varName, value); + } + + protected void putProperties(ScriptEngine engine) { + for (Map.Entry variable : mGlobalVariableMap.entrySet()) { + engine.put(variable.getKey(), variable.getValue()); + } + } + + + @Nullable + public ScriptEngine createEngine(String name) { + Supplier s = mEngines.get(name); + if (s == null) { + return null; + } + ScriptEngine engine = s.get(); + putProperties(engine); + return engine; + } + + @Nullable + public ScriptEngine createEngineOfSource(ScriptSource source) { + return createEngine(source.getEngineName()); + } + + + @NonNull + public ScriptEngine createEngineByNameOrThrow(String name) { + ScriptEngine engine = createEngine(name); + if (engine == null) + throw new EngineNotFoundException("name: " + name); + return engine; + } + + @NonNull + public ScriptEngine createEngineOfSourceOrThrow(ScriptSource source) { + ScriptEngine engine = createEngineOfSource(source); + if (engine == null) + throw new EngineNotFoundException("source: " + source.toString()); + return engine; + } + + public void registerEngine(String name, Supplier supplier) { + mEngines.put(name, supplier); + } + + public void unregisterEngine(String name) { + mEngines.remove(name); + } + +} diff --git a/app/src/main/java/com/stardust/autojs/engine/ScriptEngineManager.java b/app/src/main/java/com/stardust/autojs/engine/ScriptEngineManager.java new file mode 100644 index 00000000..6b89fc9f --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/engine/ScriptEngineManager.java @@ -0,0 +1,138 @@ +package com.stardust.autojs.engine; + +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.stardust.autojs.execution.ScriptExecution; +import com.stardust.autojs.script.ScriptSource; +import com.stardust.util.Supplier; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Created by Stardust on 2017/1/27. + */ + +public class ScriptEngineManager { + + public interface EngineLifecycleCallback { + + void onEngineCreate(ScriptEngine engine); + + void onEngineRemove(ScriptEngine engine); + } + + private static final String TAG = "ScriptEngineManager"; + + private final Set mEngines = new HashSet<>(); + private EngineLifecycleCallback mEngineLifecycleCallback; + private Map> mEngineSuppliers = new HashMap<>(); + private Map mGlobalVariableMap = new HashMap<>(); + private android.content.Context mAndroidContext; + private ScriptEngine.OnDestroyListener mOnEngineDestroyListener = new ScriptEngine.OnDestroyListener() { + @Override + public void onDestroy(ScriptEngine engine) { + removeEngine(engine); + } + }; + + public ScriptEngineManager(Context androidContext) { + mAndroidContext = androidContext; + } + + private void addEngine(ScriptEngine engine) { + engine.setOnDestroyListener(mOnEngineDestroyListener); + synchronized (mEngines) { + mEngines.add(engine); + if (mEngineLifecycleCallback != null) { + mEngineLifecycleCallback.onEngineCreate(engine); + } + } + } + + public void setEngineLifecycleCallback(EngineLifecycleCallback engineLifecycleCallback) { + mEngineLifecycleCallback = engineLifecycleCallback; + } + + public Set getEngines() { + return mEngines; + } + + public android.content.Context getAndroidContext() { + return mAndroidContext; + } + + public void removeEngine(ScriptEngine engine) { + synchronized (mEngines) { + if (mEngines.remove(engine) && mEngineLifecycleCallback != null) { + mEngineLifecycleCallback.onEngineRemove(engine); + } + } + } + + public int stopAll() { + synchronized (mEngines) { + int n = mEngines.size(); + for (ScriptEngine engine : mEngines) { + engine.forceStop(); + } + return n; + } + } + + + public void putGlobal(String varName, Object value) { + mGlobalVariableMap.put(varName, value); + } + + protected void putProperties(ScriptEngine engine) { + for (Map.Entry variable : mGlobalVariableMap.entrySet()) { + engine.put(variable.getKey(), variable.getValue()); + } + } + + + @Nullable + public ScriptEngine createEngine(String name, int id) { + Supplier s = mEngineSuppliers.get(name); + if (s == null) { + return null; + } + ScriptEngine engine = s.get(); + engine.setId(id); + putProperties(engine); + addEngine(engine); + return engine; + } + + @Nullable + public ScriptEngine createEngineOfSource(ScriptSource source, int id) { + return createEngine(source.getEngineName(), id); + } + + @NonNull + public ScriptEngine createEngineOfSourceOrThrow(ScriptSource source, int id) { + ScriptEngine engine = createEngineOfSource(source, id); + if (engine == null) + throw new ScriptEngineFactory.EngineNotFoundException("source: " + source.toString()); + return engine; + } + + @NonNull + public ScriptEngine createEngineOfSourceOrThrow(ScriptSource source) { + return createEngineOfSourceOrThrow(source, ScriptExecution.NO_ID); + } + + public void registerEngine(String name, Supplier supplier) { + mEngineSuppliers.put(name, supplier); + } + + public void unregisterEngine(String name) { + mEngineSuppliers.remove(name); + } + +} diff --git a/app/src/main/java/com/stardust/autojs/engine/ScriptEngineProxy.java b/app/src/main/java/com/stardust/autojs/engine/ScriptEngineProxy.java new file mode 100644 index 00000000..889774d3 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/engine/ScriptEngineProxy.java @@ -0,0 +1,90 @@ +package com.stardust.autojs.engine; + +import com.stardust.autojs.script.ScriptSource; + +/** + * Created by Stardust on 2018/3/17. + */ + +public class ScriptEngineProxy implements ScriptEngine { + + private final ScriptEngine mScriptEngine; + + public ScriptEngineProxy(ScriptEngine scriptEngine) { + mScriptEngine = scriptEngine; + } + + public ScriptEngine getInner() { + return mScriptEngine; + } + + @Override + public void put(String name, Object value) { + mScriptEngine.put(name, value); + } + + @Override + public Object execute(S scriptSource) { + return mScriptEngine.execute(scriptSource); + } + + @Override + public void forceStop() { + mScriptEngine.forceStop(); + } + + @Override + public void destroy() { + mScriptEngine.destroy(); + } + + @Override + public boolean isDestroyed() { + return mScriptEngine.isDestroyed(); + } + + @Override + public void setTag(String key, Object value) { + mScriptEngine.setTag(key, value); + } + + @Override + public Object getTag(String key) { + return mScriptEngine.getTag(key); + } + + @Override + public String cwd() { + return mScriptEngine.cwd(); + } + + @Override + public void uncaughtException(Throwable throwable) { + mScriptEngine.uncaughtException(throwable); + } + + @Override + public Throwable getUncaughtException() { + return mScriptEngine.getUncaughtException(); + } + + @Override + public void setOnDestroyListener(OnDestroyListener listener) { + mScriptEngine.setOnDestroyListener(listener); + } + + @Override + public void init() { + mScriptEngine.init(); + } + + @Override + public int getId() { + return mScriptEngine.getId(); + } + + @Override + public void setId(int id) { + mScriptEngine.setId(id); + } +} diff --git a/app/src/main/java/com/stardust/autojs/engine/encryption/ScriptEncryption.kt b/app/src/main/java/com/stardust/autojs/engine/encryption/ScriptEncryption.kt new file mode 100644 index 00000000..46594c52 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/engine/encryption/ScriptEncryption.kt @@ -0,0 +1,14 @@ +package com.stardust.autojs.engine.encryption + +import com.stardust.util.AdvancedEncryptionStandard + +object ScriptEncryption { + + private var mKey = "" + private var mInitVector = "" + + fun decrypt(bytes: ByteArray, start: Int = 0, end: Int = bytes.size): ByteArray { + return AdvancedEncryptionStandard(mKey.toByteArray(), mInitVector).decrypt(bytes, start, end) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/stardust/autojs/engine/module/AssetAndUrlModuleSourceProvider.java b/app/src/main/java/com/stardust/autojs/engine/module/AssetAndUrlModuleSourceProvider.java new file mode 100644 index 00000000..cc981d8b --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/engine/module/AssetAndUrlModuleSourceProvider.java @@ -0,0 +1,66 @@ +package com.stardust.autojs.engine.module; + +import android.content.res.AssetManager; + +import com.stardust.autojs.engine.encryption.ScriptEncryption; +import com.stardust.autojs.script.EncryptedScriptFileHeader; + +import org.mozilla.javascript.commonjs.module.provider.ModuleSource; + +import java.io.ByteArrayInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLConnection; +import java.util.List; + +/** + * Created by Stardust on 2017/5/9. + */ + +public class AssetAndUrlModuleSourceProvider extends UrlModuleSourceProvider { + + private android.content.Context mContext; + private final URI mBaseURI; + private final String mAssetDirPath; + private final AssetManager mAssetManager; + + public AssetAndUrlModuleSourceProvider(android.content.Context context, String assetDirPath, List list) { + super(list, null); + mContext = context; + mAssetDirPath = assetDirPath; + mBaseURI = URI.create("file:///android_asset/" + assetDirPath); + mAssetManager = mContext.getAssets(); + } + + @Override + protected ModuleSource loadFromPrivilegedLocations(String moduleId, Object validator) throws IOException, URISyntaxException { + String moduleIdWithExtension = moduleId; + if (!moduleIdWithExtension.endsWith(".js")) { + moduleIdWithExtension += ".js"; + } + try { + return new ModuleSource(new InputStreamReader(mAssetManager.open(mAssetDirPath + "/" + moduleIdWithExtension)), null, + new URI(mBaseURI.toString() + "/" + moduleIdWithExtension), mBaseURI, validator); + } catch (FileNotFoundException e) { + return super.loadFromPrivilegedLocations(moduleId, validator); + } + } + + @Override + protected Reader getReader(URLConnection urlConnection) throws IOException { + InputStream stream = urlConnection.getInputStream(); + byte[] bytes = new byte[stream.available()]; + stream.read(bytes); + stream.close(); + if (EncryptedScriptFileHeader.INSTANCE.isValidFile(bytes)) { + byte[] clearText = ScriptEncryption.INSTANCE.decrypt(bytes, EncryptedScriptFileHeader.BLOCK_SIZE, bytes.length); + return new InputStreamReader(new ByteArrayInputStream(clearText)); + } + return new InputStreamReader(new ByteArrayInputStream(bytes)); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/stardust/autojs/engine/module/UrlModuleSourceProvider.java b/app/src/main/java/com/stardust/autojs/engine/module/UrlModuleSourceProvider.java new file mode 100644 index 00000000..c3e668df --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/engine/module/UrlModuleSourceProvider.java @@ -0,0 +1,364 @@ +package com.stardust.autojs.engine.module; + +import org.mozilla.javascript.commonjs.module.provider.DefaultUrlConnectionExpiryCalculator; +import org.mozilla.javascript.commonjs.module.provider.ModuleSource; +import org.mozilla.javascript.commonjs.module.provider.ModuleSourceProviderBase; +import org.mozilla.javascript.commonjs.module.provider.ParsedContentType; +import org.mozilla.javascript.commonjs.module.provider.UrlConnectionExpiryCalculator; +import org.mozilla.javascript.commonjs.module.provider.UrlConnectionSecurityDomainProvider; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.Serializable; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; +import java.util.Iterator; +import java.util.List; + + +/** + * A URL-based script provider that can load modules against a set of base + * privileged and fallback URIs. It is deliberately not named "URI provider" + * but a "URL provider" since it actually only works against those URIs that + * are URLs (and the JRE has a protocol handler for them). It creates cache + * validators that are suitable for use with both file: and http: URL + * protocols. Specifically, it is able to use both last-modified timestamps and + * ETags for cache revalidation, and follows the HTTP cache expiry calculation + * model, and allows for fallback heuristic expiry calculation when no server + * specified expiry is provided. + * + * @author Attila Szegedi + * @version $Id: UrlModuleSourceProvider.java,v 1.4 2011/04/07 20:26:12 hannes%helma.at Exp $ + */ +public class UrlModuleSourceProvider extends ModuleSourceProviderBase { + private static final long serialVersionUID = 1L; + + private final Iterable privilegedUris; + private final Iterable fallbackUris; + private final UrlConnectionSecurityDomainProvider + urlConnectionSecurityDomainProvider; + private final UrlConnectionExpiryCalculator urlConnectionExpiryCalculator; + + /** + * Creates a new module script provider that loads modules against a set of + * privileged and fallback URIs. It will use a fixed default cache expiry + * of 60 seconds, and provide no security domain objects for the resource. + * + * @param privilegedUris an iterable providing the privileged URIs. Can be + * null if no privileged URIs are used. + * @param fallbackUris an iterable providing the fallback URIs. Can be + * null if no fallback URIs are used. + */ + public UrlModuleSourceProvider(Iterable privilegedUris, + Iterable fallbackUris) { + this(privilegedUris, fallbackUris, + new DefaultUrlConnectionExpiryCalculator(), null); + } + + /** + * Creates a new module script provider that loads modules against a set of + * privileged and fallback URIs. It will use the specified heuristic cache + * expiry calculator and security domain provider. + * + * @param privilegedUris an iterable providing the privileged URIs. Can be + * null if no privileged URIs are used. + * @param fallbackUris an iterable providing the fallback URIs. Can be + * null if no fallback URIs are used. + * @param urlConnectionExpiryCalculator the calculator object for heuristic + * calculation of the resource expiry, used when no expiry is provided by + * the server of the resource. Can be null, in which case the maximum age + * of cached entries without validation will be zero. + * @param urlConnectionSecurityDomainProvider object that provides security + * domain objects for the loaded sources. Can be null, in which case the + * loaded sources will have no security domain associated with them. + */ + public UrlModuleSourceProvider(Iterable privilegedUris, + Iterable fallbackUris, + UrlConnectionExpiryCalculator urlConnectionExpiryCalculator, + UrlConnectionSecurityDomainProvider urlConnectionSecurityDomainProvider) { + this.privilegedUris = privilegedUris; + this.fallbackUris = fallbackUris; + this.urlConnectionExpiryCalculator = urlConnectionExpiryCalculator; + this.urlConnectionSecurityDomainProvider = + urlConnectionSecurityDomainProvider; + } + + @Override + protected ModuleSource loadFromPrivilegedLocations( + String moduleId, Object validator) + throws IOException, URISyntaxException { + return loadFromPathList(moduleId, validator, privilegedUris); + } + + @Override + protected ModuleSource loadFromFallbackLocations( + String moduleId, Object validator) + throws IOException, URISyntaxException { + return loadFromPathList(moduleId, validator, fallbackUris); + } + + private ModuleSource loadFromPathList(String moduleId, + Object validator, Iterable paths) + throws IOException, URISyntaxException { + if (paths == null) { + return null; + } + for (URI path : paths) { + final ModuleSource moduleSource = loadFromUri( + path.resolve(moduleId), path, validator); + if (moduleSource != null) { + return moduleSource; + } + } + return null; + } + + @Override + protected ModuleSource loadFromUri(URI uri, URI base, Object validator) + throws IOException, URISyntaxException { + // We expect modules to have a ".js" file name extension ... + URI fullUri = new URI(uri + ".js"); + ModuleSource source = loadFromActualUri(fullUri, base, validator); + // ... but for compatibility we support modules without extension, + // or ids with explicit extension. + return source != null ? + source : loadFromActualUri(uri, base, validator); + } + + protected ModuleSource loadFromActualUri(URI uri, URI base, Object validator) + throws IOException { + final URL url = new URL(base == null ? null : base.toURL(), uri.toString()); + final long request_time = System.currentTimeMillis(); + final URLConnection urlConnection = openUrlConnection(url); + final URLValidator applicableValidator; + if (validator instanceof URLValidator) { + final URLValidator uriValidator = ((URLValidator) validator); + applicableValidator = uriValidator.appliesTo(uri) ? uriValidator : + null; + } else { + applicableValidator = null; + } + if (applicableValidator != null) { + applicableValidator.applyConditionals(urlConnection); + } + try { + urlConnection.connect(); + if (applicableValidator != null && + applicableValidator.updateValidator(urlConnection, + request_time, urlConnectionExpiryCalculator)) { + close(urlConnection); + return NOT_MODIFIED; + } + + return new ModuleSource(getReader(urlConnection), + getSecurityDomain(urlConnection), uri, base, + new URLValidator(uri, urlConnection, request_time, + urlConnectionExpiryCalculator)); + } catch (FileNotFoundException e) { + return null; + } catch (RuntimeException e) { + close(urlConnection); + throw e; + } catch (IOException e) { + close(urlConnection); + throw e; + } + } + + protected Reader getReader(URLConnection urlConnection) + throws IOException { + return new InputStreamReader(urlConnection.getInputStream(), + getCharacterEncoding(urlConnection)); + } + + protected String getCharacterEncoding(URLConnection urlConnection) { + final ParsedContentType pct = new ParsedContentType( + urlConnection.getContentType()); + final String encoding = pct.getEncoding(); + if (encoding != null) { + return encoding; + } + final String contentType = pct.getContentType(); + if (contentType != null && contentType.startsWith("text/")) { + return "8859_1"; + } + return "utf-8"; + } + + protected Object getSecurityDomain(URLConnection urlConnection) { + return urlConnectionSecurityDomainProvider == null ? null : + urlConnectionSecurityDomainProvider.getSecurityDomain( + urlConnection); + } + + private void close(URLConnection urlConnection) { + try { + urlConnection.getInputStream().close(); + } catch (IOException e) { + onFailedClosingUrlConnection(urlConnection, e); + } + } + + /** + * Override if you want to get notified if the URL connection fails to + * close. Does nothing by default. + * + * @param urlConnection the connection + * @param cause the cause it failed to close. + */ + protected void onFailedClosingUrlConnection(URLConnection urlConnection, + IOException cause) { + } + + /** + * Can be overridden in subclasses to customize the URL connection opening + * process. By default, just calls {@link URL#openConnection()}. + * + * @param url the URL + * @return a connection to the URL. + * @throws IOException if an I/O error occurs. + */ + protected URLConnection openUrlConnection(URL url) throws IOException { + return url.openConnection(); + } + + @Override + protected boolean entityNeedsRevalidation(Object validator) { + return !(validator instanceof URLValidator) + || ((URLValidator) validator).entityNeedsRevalidation(); + } + + private static class URLValidator implements Serializable { + private static final long serialVersionUID = 1L; + + private final URI uri; + private final long lastModified; + private final String entityTags; + private long expiry; + + public URLValidator(URI uri, URLConnection urlConnection, + long request_time, UrlConnectionExpiryCalculator + urlConnectionExpiryCalculator) { + this.uri = uri; + this.lastModified = urlConnection.getLastModified(); + this.entityTags = getEntityTags(urlConnection); + expiry = calculateExpiry(urlConnection, request_time, + urlConnectionExpiryCalculator); + } + + boolean updateValidator(URLConnection urlConnection, long request_time, + UrlConnectionExpiryCalculator urlConnectionExpiryCalculator) + throws IOException { + boolean isResourceChanged = isResourceChanged(urlConnection); + if (!isResourceChanged) { + expiry = calculateExpiry(urlConnection, request_time, + urlConnectionExpiryCalculator); + } + return isResourceChanged; + } + + private boolean isResourceChanged(URLConnection urlConnection) + throws IOException { + if (urlConnection instanceof HttpURLConnection) { + return ((HttpURLConnection) urlConnection).getResponseCode() == + HttpURLConnection.HTTP_NOT_MODIFIED; + } + return lastModified != urlConnection.getLastModified(); + } + + private long calculateExpiry(URLConnection urlConnection, + long request_time, UrlConnectionExpiryCalculator + urlConnectionExpiryCalculator) { + if ("no-cache".equals(urlConnection.getHeaderField("Pragma"))) { + return 0L; + } + final String cacheControl = urlConnection.getHeaderField( + "Cache-Control"); + if (cacheControl != null) { + if (cacheControl.indexOf("no-cache") != -1) { + return 0L; + } + final int max_age = getMaxAge(cacheControl); + if (-1 != max_age) { + final long response_time = System.currentTimeMillis(); + final long apparent_age = Math.max(0, response_time - + urlConnection.getDate()); + final long corrected_received_age = Math.max(apparent_age, + urlConnection.getHeaderFieldInt("Age", 0) * 1000L); + final long response_delay = response_time - request_time; + final long corrected_initial_age = corrected_received_age + + response_delay; + final long creation_time = response_time - + corrected_initial_age; + return max_age * 1000L + creation_time; + } + } + final long explicitExpiry = urlConnection.getHeaderFieldDate( + "Expires", -1L); + if (explicitExpiry != -1L) { + return explicitExpiry; + } + return urlConnectionExpiryCalculator == null ? 0L : + urlConnectionExpiryCalculator.calculateExpiry(urlConnection); + } + + private int getMaxAge(String cacheControl) { + final int maxAgeIndex = cacheControl.indexOf("max-age"); + if (maxAgeIndex == -1) { + return -1; + } + final int eq = cacheControl.indexOf('=', maxAgeIndex + 7); + if (eq == -1) { + return -1; + } + final int comma = cacheControl.indexOf(',', eq + 1); + final String strAge; + if (comma == -1) { + strAge = cacheControl.substring(eq + 1); + } else { + strAge = cacheControl.substring(eq + 1, comma); + } + try { + return Integer.parseInt(strAge); + } catch (NumberFormatException e) { + return -1; + } + } + + private String getEntityTags(URLConnection urlConnection) { + final List etags = urlConnection.getHeaderFields().get("ETag"); + if (etags == null || etags.isEmpty()) { + return null; + } + final StringBuilder b = new StringBuilder(); + final Iterator it = etags.iterator(); + b.append(it.next()); + while (it.hasNext()) { + b.append(", ").append(it.next()); + } + return b.toString(); + } + + boolean appliesTo(URI uri) { + return this.uri.equals(uri); + } + + void applyConditionals(URLConnection urlConnection) { + if (lastModified != 0L) { + urlConnection.setIfModifiedSince(lastModified); + } + if (entityTags != null && entityTags.length() > 0) { + urlConnection.addRequestProperty("If-None-Match", entityTags); + } + } + + boolean entityNeedsRevalidation() { + return System.currentTimeMillis() > expiry; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/stardust/autojs/engine/preprocess/AbstractProcessor.java b/app/src/main/java/com/stardust/autojs/engine/preprocess/AbstractProcessor.java new file mode 100644 index 00000000..44ab0378 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/engine/preprocess/AbstractProcessor.java @@ -0,0 +1,31 @@ +package com.stardust.autojs.engine.preprocess; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.Reader; + +/** + * Created by Stardust on 2017/5/15. + */ + +public abstract class AbstractProcessor implements Preprocessor { + + @Override + public Reader preprocess(Reader reader) throws IOException { + reset(); + int ch; + if (!(reader instanceof BufferedReader)) + reader = new BufferedReader(reader); + while ((ch = reader.read()) != -1) { + handleChar(ch); + } + return getReaderAndClear(); + } + + protected abstract void handleChar(int ch); + + public abstract void reset(); + + public abstract Reader getReaderAndClear(); + +} diff --git a/app/src/main/java/com/stardust/autojs/engine/preprocess/MultiLinePreprocessor.java b/app/src/main/java/com/stardust/autojs/engine/preprocess/MultiLinePreprocessor.java new file mode 100644 index 00000000..94d18b67 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/engine/preprocess/MultiLinePreprocessor.java @@ -0,0 +1,97 @@ +package com.stardust.autojs.engine.preprocess; + +import androidx.annotation.VisibleForTesting; + +import java.io.Reader; +import java.io.StringReader; + +/** + * Created by Stardust on 2017/5/15. + */ + +public class MultiLinePreprocessor extends AbstractProcessor { + + private static final int STATE_SINGLE_QUOTE_LITERAL = 0x00000001; + private static final int STATE_DOUBLE_QUOTE_LITERAL = 0x00000010; + private static final int STATE_MULTI_LINE = 0x00001100; + + private int mState = 0; + private int mStateBeforeLiteral = 0; + private StringBuilder mNewScript; + private int mLastReturnCharPosition; + private int i; + + @Override + protected void handleChar(int ch) { + boolean shouldAppend = true; + switch (ch) { + case '"': + if (mState == STATE_DOUBLE_QUOTE_LITERAL) { + mState = mStateBeforeLiteral; + } else if (mState != STATE_SINGLE_QUOTE_LITERAL) { + mStateBeforeLiteral = mState; + mState = STATE_DOUBLE_QUOTE_LITERAL; + } + break; + case '\'': + if (mState == STATE_SINGLE_QUOTE_LITERAL) { + if ((mStateBeforeLiteral & STATE_MULTI_LINE) != 0) { + mNewScript.append('\\'); + } + mState = mStateBeforeLiteral; + } else if (mState != STATE_DOUBLE_QUOTE_LITERAL) { + mStateBeforeLiteral = mState; + mState = STATE_SINGLE_QUOTE_LITERAL; + if ((mStateBeforeLiteral & STATE_MULTI_LINE) != 0) { + mNewScript.append('\\'); + } + } + break; + case '`': + if (mState == 0) { + mState = STATE_MULTI_LINE; + mNewScript.append("'"); + shouldAppend = false; + } else if (mState == STATE_MULTI_LINE) { + mState = 0; + mNewScript.append("'"); + shouldAppend = false; + } + break; + case '\r': + case '\n': + if (ch == '\n' && mLastReturnCharPosition == i - 1) { + shouldAppend = false; + break; + } + if (ch == '\r') + mLastReturnCharPosition = i; + if (mState == STATE_MULTI_LINE) { + mNewScript.append("\\n'+\n'"); + shouldAppend = false; + } + break; + } + if (shouldAppend) { + mNewScript.append((char) ch); + } + i++; + } + + @Override + public void reset() { + mState = 0; + mStateBeforeLiteral = 0; + mNewScript = new StringBuilder(); + mLastReturnCharPosition = -2; + i = 0; + } + + @Override + public Reader getReaderAndClear() { + Reader reader = new StringReader(mNewScript.toString()); + mNewScript = null; + return reader; + } + +} diff --git a/app/src/main/java/com/stardust/autojs/engine/preprocess/Preprocessor.java b/app/src/main/java/com/stardust/autojs/engine/preprocess/Preprocessor.java new file mode 100644 index 00000000..d702634b --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/engine/preprocess/Preprocessor.java @@ -0,0 +1,13 @@ +package com.stardust.autojs.engine.preprocess; + +import java.io.IOException; +import java.io.Reader; + +/** + * Created by Stardust on 2017/5/15. + */ + +public interface Preprocessor { + + Reader preprocess(Reader reader) throws IOException; +} diff --git a/app/src/main/java/com/stardust/autojs/execution/ExecutionConfig.kt b/app/src/main/java/com/stardust/autojs/execution/ExecutionConfig.kt new file mode 100644 index 00000000..3044be3b --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/execution/ExecutionConfig.kt @@ -0,0 +1,99 @@ +package com.stardust.autojs.execution + +import android.os.Parcel +import android.os.Parcelable +import com.stardust.autojs.project.ScriptConfig +import java.util.* + +/** + * Created by Stardust on 2017/2/1. + */ +data class ExecutionConfig(var workingDirectory: String = "", + var path: Array = emptyArray(), + var intentFlags: Int = 0, + var delay: Long = 0, + var interval: Long = 0, + var loopTimes: Int = 1, + var scriptConfig: ScriptConfig = ScriptConfig()) : Parcelable { + + + private val mArguments = HashMap() + + val arguments: Map + get() = mArguments + + constructor(parcel: Parcel) : this( + parcel.readString().orEmpty(), + parcel.createStringArray().orEmpty(), + parcel.readInt(), + parcel.readLong(), + parcel.readLong(), + parcel.readInt()) + + fun setArgument(key: String, `object`: Any) { + mArguments[key] = `object` + } + + fun getArgument(key: String): Any? { + return mArguments[key] + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ExecutionConfig + + if (workingDirectory != other.workingDirectory) return false + if (!path.contentEquals(other.path)) return false + if (intentFlags != other.intentFlags) return false + if (delay != other.delay) return false + if (interval != other.interval) return false + if (loopTimes != other.loopTimes) return false + if (mArguments != other.mArguments) return false + + return true + } + + override fun hashCode(): Int { + var result = workingDirectory.hashCode() + result = 31 * result + path.contentHashCode() + result = 31 * result + intentFlags + result = 31 * result + delay.hashCode() + result = 31 * result + interval.hashCode() + result = 31 * result + loopTimes + result = 31 * result + mArguments.hashCode() + return result + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeString(workingDirectory) + parcel.writeStringArray(path) + parcel.writeInt(intentFlags) + parcel.writeLong(delay) + parcel.writeLong(interval) + parcel.writeInt(loopTimes) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + + @JvmStatic + val tag = "execution.config" + + @JvmStatic + val default: ExecutionConfig + get() = ExecutionConfig() + + override fun createFromParcel(parcel: Parcel): ExecutionConfig { + return ExecutionConfig(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} diff --git a/app/src/main/java/com/stardust/autojs/execution/LoopedBasedJavaScriptExecution.java b/app/src/main/java/com/stardust/autojs/execution/LoopedBasedJavaScriptExecution.java new file mode 100644 index 00000000..db829a74 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/execution/LoopedBasedJavaScriptExecution.java @@ -0,0 +1,51 @@ +package com.stardust.autojs.execution; + +import com.stardust.autojs.engine.LoopBasedJavaScriptEngine; +import com.stardust.autojs.engine.ScriptEngine; +import com.stardust.autojs.engine.ScriptEngineManager; +import com.stardust.autojs.core.looper.Loopers; +import com.stardust.autojs.script.JavaScriptSource; + +/** + * Created by Stardust on 2017/10/27. + */ + +public class LoopedBasedJavaScriptExecution extends RunnableScriptExecution { + + public LoopedBasedJavaScriptExecution(ScriptEngineManager manager, ScriptExecutionTask task) { + super(manager, task); + } + + + protected Object doExecution(final ScriptEngine engine) { + engine.setTag(ScriptEngine.TAG_SOURCE, getSource()); + getListener().onStart(this); + long delay = getConfig().getDelay(); + sleep(delay); + final LoopBasedJavaScriptEngine javaScriptEngine = (LoopBasedJavaScriptEngine) engine; + final long interval = getConfig().getInterval(); + javaScriptEngine.getRuntime().loopers.setMainLooperQuitHandler(new Loopers.LooperQuitHandler() { + long times = getConfig().getLoopTimes() == 0 ? Integer.MAX_VALUE : getConfig().getLoopTimes(); + + @Override + public boolean shouldQuit() { + times--; + if (times > 0) { + sleep(interval); + javaScriptEngine.execute(getSource()); + return false; + } + javaScriptEngine.getRuntime().loopers.setMainLooperQuitHandler(null); + return true; + } + }); + javaScriptEngine.execute(getSource()); + return null; + } + + @Override + public JavaScriptSource getSource() { + return (JavaScriptSource) super.getSource(); + } + +} diff --git a/app/src/main/java/com/stardust/autojs/execution/RunnableScriptExecution.java b/app/src/main/java/com/stardust/autojs/execution/RunnableScriptExecution.java new file mode 100644 index 00000000..6e35444e --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/execution/RunnableScriptExecution.java @@ -0,0 +1,111 @@ +package com.stardust.autojs.execution; + +import android.util.Log; + +import com.stardust.autojs.engine.ScriptEngine; +import com.stardust.autojs.engine.ScriptEngineManager; +import com.stardust.autojs.runtime.exception.ScriptInterruptedException; +import com.stardust.autojs.script.ScriptSource; +import com.stardust.lang.ThreadCompat; + +import org.mozilla.javascript.ContinuationPending; + +/** + * Created by Stardust on 2017/5/1. + */ + +public class RunnableScriptExecution extends ScriptExecution.AbstractScriptExecution implements Runnable { + + private static final String TAG = "RunnableJSExecution"; + private ScriptEngine mScriptEngine; + private ScriptEngineManager mScriptEngineManager; + + public RunnableScriptExecution(ScriptEngineManager manager, ScriptExecutionTask task) { + super(task); + mScriptEngineManager = manager; + } + + @Override + public void run() { + ThreadCompat.currentThread().setName("ScriptThread-" + getId() + "[" + getSource() + "]"); + execute(); + } + + public Object execute() { + mScriptEngine = mScriptEngineManager.createEngineOfSourceOrThrow(getSource(), getId()); + mScriptEngine.setTag(ExecutionConfig.getTag(), getConfig()); + return execute(mScriptEngine); + } + + private Object execute(ScriptEngine engine) { + try { + prepare(engine); + Object r = doExecution(engine); + Throwable uncaughtException = engine.getUncaughtException(); + if (uncaughtException != null) { + onException(engine, uncaughtException); + return null; + } + getListener().onSuccess(this, r); + return r; + } catch (Throwable e) { + onException(engine, e); + return null; + } finally { + Log.d(TAG, "Engine destroy"); + engine.destroy(); + } + } + + protected void onException(ScriptEngine engine, Throwable e) { + Log.w(TAG, "onException: engine = " + engine, e); + getListener().onException(this, e); + } + + private void prepare(ScriptEngine engine) { + engine.setTag(ScriptEngine.TAG_WORKING_DIRECTORY, getConfig().getWorkingDirectory()); + engine.setTag(ScriptEngine.TAG_ENV_PATH, getConfig().getPath()); + engine.init(); + } + + protected Object doExecution(ScriptEngine engine) { + engine.setTag(ScriptEngine.TAG_SOURCE, getSource()); + getListener().onStart(this); + Object result = null; + long delay = getConfig().getDelay(); + int times = getConfig().getLoopTimes(); + if (times == 0) { + times = Integer.MAX_VALUE; + } + long interval = getConfig().getInterval(); + sleep(delay); + ScriptSource source = getSource(); + for (int i = 0; i < times; i++) { + result = execute(engine, source); + sleep(interval); + } + return result; + } + + @SuppressWarnings("unchecked") + protected Object execute(ScriptEngine engine, ScriptSource source) { + return engine.execute(source); + } + + protected void sleep(long i) { + if (i <= 0) { + return; + } + try { + Thread.sleep(i); + } catch (InterruptedException e) { + throw new ScriptInterruptedException(); + } + } + + @Override + public ScriptEngine getEngine() { + return mScriptEngine; + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/stardust/autojs/execution/ScriptExecuteActivity.java b/app/src/main/java/com/stardust/autojs/execution/ScriptExecuteActivity.java new file mode 100644 index 00000000..b671b9ee --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/execution/ScriptExecuteActivity.java @@ -0,0 +1,263 @@ +package com.stardust.autojs.execution; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import androidx.appcompat.app.AppCompatActivity; + +import android.util.Log; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; + +import com.stardust.autojs.ScriptEngineService; +import com.stardust.autojs.core.eventloop.EventEmitter; +import com.stardust.autojs.core.eventloop.SimpleEvent; +import com.stardust.autojs.engine.JavaScriptEngine; +import com.stardust.autojs.engine.LoopBasedJavaScriptEngine; +import com.stardust.autojs.engine.ScriptEngine; +import com.stardust.autojs.engine.ScriptEngineManager; +import com.stardust.autojs.runtime.ScriptRuntime; +import com.stardust.autojs.script.ScriptSource; + +import org.mozilla.javascript.ContinuationPending; + +/** + * Created by Stardust on 2017/2/5. + */ + +public class ScriptExecuteActivity extends AppCompatActivity { + + + private static final String LOG_TAG = "ScriptExecuteActivity"; + private static final String EXTRA_EXECUTION_ID = ScriptExecuteActivity.class.getName() + ".execution_id"; + private Object mResult; + private ScriptEngine mScriptEngine; + private ScriptExecutionListener mExecutionListener; + private ScriptSource mScriptSource; + private ActivityScriptExecution mScriptExecution; + private ScriptRuntime mRuntime; + + + private EventEmitter mEventEmitter; + + public static ActivityScriptExecution execute(Context context, ScriptEngineManager manager, ScriptExecutionTask task) { + ActivityScriptExecution execution = new ActivityScriptExecution(manager, task); + Intent i = new Intent(context, ScriptExecuteActivity.class) + .putExtra(EXTRA_EXECUTION_ID, execution.getId()) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .addFlags(task.getConfig().getIntentFlags()); + context.startActivity(i); + return execution; + } + + // FIXME: 2018/3/16 如果Activity被回收则得不到改进 + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + int executionId = getIntent().getIntExtra(EXTRA_EXECUTION_ID, ScriptExecution.NO_ID); + if (executionId == ScriptExecution.NO_ID) { + super.finish(); + return; + } + ScriptExecution execution = ScriptEngineService.getInstance().getScriptExecution(executionId); + if (execution == null || !(execution instanceof ActivityScriptExecution)) { + super.finish(); + return; + } + mScriptExecution = (ActivityScriptExecution) execution; + mScriptSource = mScriptExecution.getSource(); + mScriptEngine = mScriptExecution.createEngine(this); + mExecutionListener = mScriptExecution.getListener(); + mRuntime = ((JavaScriptEngine) mScriptEngine).getRuntime(); + mEventEmitter = new EventEmitter(mRuntime.bridges); + runScript(); + emit("create", savedInstanceState); + } + + public EventEmitter getEventEmitter() { + return mEventEmitter; + } + + private void runScript() { + try { + prepare(); + doExecution(); + } catch (ContinuationPending pending) { + pending.printStackTrace(); + } catch (Exception e) { + onException(e); + } + } + + private void onException(Throwable e) { + mExecutionListener.onException(mScriptExecution, e); + super.finish(); + } + + @SuppressWarnings("unchecked") + private void doExecution() { + mScriptEngine.setTag(ScriptEngine.TAG_SOURCE, mScriptSource); + mExecutionListener.onStart(mScriptExecution); + ((LoopBasedJavaScriptEngine) mScriptEngine).execute(mScriptSource, new LoopBasedJavaScriptEngine.ExecuteCallback() { + @Override + public void onResult(Object r) { + mResult = r; + } + + @Override + public void onException(Exception e) { + ScriptExecuteActivity.this.onException(e); + } + }); + } + + private void prepare() { + mScriptEngine.put("activity", this); + mScriptEngine.setTag("activity", this); + mScriptEngine.setTag(ScriptEngine.TAG_ENV_PATH, mScriptExecution.getConfig().getPath()); + mScriptEngine.setTag(ScriptEngine.TAG_WORKING_DIRECTORY, mScriptExecution.getConfig().getWorkingDirectory()); + mScriptEngine.init(); + } + + @Override + public void finish() { + if (mScriptExecution == null || mExecutionListener == null) { + super.finish(); + return; + } + Throwable exception = mScriptEngine.getUncaughtException(); + if (exception != null) { + onException(exception); + } else { + mExecutionListener.onSuccess(mScriptExecution, mResult); + } + super.finish(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + Log.d(LOG_TAG, "onDestroy"); + if (mScriptEngine != null) { + mScriptEngine.put("activity", null); + mScriptEngine.setTag("activity", null); + mScriptEngine.destroy(); + } + mScriptExecution = null; + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (mScriptExecution != null) + outState.putInt(EXTRA_EXECUTION_ID, mScriptExecution.getId()); + emit("save_instance_state", outState); + } + + @Override + public void onBackPressed() { + SimpleEvent event = new SimpleEvent(); + emit("back_pressed", event); + if (!event.consumed) { + super.onBackPressed(); + } + } + + @Override + protected void onPause() { + emit("pause"); + super.onPause(); + } + + @Override + protected void onResume() { + super.onResume(); + emit("resume"); + } + + @Override + protected void onRestart() { + super.onRestart(); + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + emit("restore_instance_state", savedInstanceState); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + SimpleEvent e = new SimpleEvent(); + emit("key_down", keyCode, event, e); + return e.consumed || super.onKeyDown(keyCode, event); + + } + + @Override + public boolean onGenericMotionEvent(MotionEvent event) { + SimpleEvent e = new SimpleEvent(); + emit("generic_motion_event", event, e); + return super.onGenericMotionEvent(event); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + emit("activity_result", requestCode, resultCode, data); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + emit("create_options_menu", menu); + return menu.size() > 0; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + SimpleEvent e = new SimpleEvent(); + emit("options_item_selected", e, item); + return e.consumed || super.onOptionsItemSelected(item); + } + + public void emit(String event, Object... args) { + try { + mEventEmitter.emit(event, (Object[]) args); + } catch (Exception e) { + mRuntime.exit(e); + } + } + + private static class ActivityScriptExecution extends ScriptExecution.AbstractScriptExecution { + + private ScriptEngine mScriptEngine; + private ScriptEngineManager mScriptEngineManager; + + ActivityScriptExecution(ScriptEngineManager manager, ScriptExecutionTask task) { + super(task); + mScriptEngineManager = manager; + } + + @SuppressWarnings("unchecked") + public ScriptEngine createEngine(Activity activity) { + if (mScriptEngine != null) { + mScriptEngine.forceStop(); + } + mScriptEngine = mScriptEngineManager.createEngineOfSourceOrThrow(getSource(), getId()); + mScriptEngine.setTag(ExecutionConfig.getTag(), getConfig()); + return mScriptEngine; + } + + @Override + public ScriptEngine getEngine() { + return mScriptEngine; + } + + } + + +} diff --git a/app/src/main/java/com/stardust/autojs/execution/ScriptExecution.java b/app/src/main/java/com/stardust/autojs/execution/ScriptExecution.java new file mode 100644 index 00000000..ca131499 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/execution/ScriptExecution.java @@ -0,0 +1,76 @@ +package com.stardust.autojs.execution; + +import com.stardust.autojs.engine.ScriptEngine; +import com.stardust.autojs.runtime.ScriptRuntime; +import com.stardust.autojs.script.ScriptSource; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Created by Stardust on 2017/4/3. + */ + +public interface ScriptExecution { + + int NO_ID = -1; + + ScriptEngine getEngine(); + + ScriptSource getSource(); + + ScriptExecutionListener getListener(); + + ExecutionConfig getConfig(); + + int getId(); + + abstract class AbstractScriptExecution implements ScriptExecution { + + private static AtomicInteger sMaxId = new AtomicInteger(0); + + protected ScriptExecutionTask mScriptExecutionTask; + protected int mId; + + public AbstractScriptExecution(ScriptExecutionTask task) { + mScriptExecutionTask = task; + mId = sMaxId.getAndIncrement(); + } + + @Override + public abstract ScriptEngine getEngine(); + + @Override + public ScriptSource getSource() { + return mScriptExecutionTask.getSource(); + } + + @Override + public ScriptExecutionListener getListener() { + return mScriptExecutionTask.getListener(); + } + + @Override + public ExecutionConfig getConfig() { + return mScriptExecutionTask.getConfig(); + } + + @Override + public int getId() { + return mId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AbstractScriptExecution that = (AbstractScriptExecution) o; + return mId == that.mId; + } + + @Override + public int hashCode() { + return mId; + } + } +} diff --git a/app/src/main/java/com/stardust/autojs/execution/ScriptExecutionListener.java b/app/src/main/java/com/stardust/autojs/execution/ScriptExecutionListener.java new file mode 100644 index 00000000..9d76cef3 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/execution/ScriptExecutionListener.java @@ -0,0 +1,16 @@ +package com.stardust.autojs.execution; + +import java.io.Serializable; + +/** + * Created by Stardust on 2017/4/2. + */ + +public interface ScriptExecutionListener extends Serializable { + + void onStart(ScriptExecution execution); + + void onSuccess(ScriptExecution execution, Object result); + + void onException(ScriptExecution execution, Throwable e); +} diff --git a/app/src/main/java/com/stardust/autojs/execution/ScriptExecutionObserver.java b/app/src/main/java/com/stardust/autojs/execution/ScriptExecutionObserver.java new file mode 100644 index 00000000..117bc848 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/execution/ScriptExecutionObserver.java @@ -0,0 +1,70 @@ +package com.stardust.autojs.execution; + +import java.util.concurrent.CopyOnWriteArraySet; + +/** + * Created by Stardust on 2017/5/3. + */ + +public class ScriptExecutionObserver implements ScriptExecutionListener { + private CopyOnWriteArraySet mScriptExecutionListeners = new CopyOnWriteArraySet<>(); + + @Override + public void onStart(ScriptExecution execution) { + for (ScriptExecutionListener listener : mScriptExecutionListeners) { + listener.onStart(execution); + } + } + + @Override + public void onSuccess(ScriptExecution execution, Object result) { + for (ScriptExecutionListener listener : mScriptExecutionListeners) { + listener.onSuccess(execution, result); + } + } + + @Override + public void onException(ScriptExecution execution, Throwable e) { + for (ScriptExecutionListener listener : mScriptExecutionListeners) { + listener.onException(execution, e); + } + } + + public boolean registerScriptExecutionListener(ScriptExecutionListener listener) { + return mScriptExecutionListeners.add(listener); + } + + public boolean removeScriptExecutionListener(ScriptExecutionListener listener) { + return mScriptExecutionListeners.remove(listener); + } + + public static class Wrapper implements ScriptExecutionListener { + + private final ScriptExecutionObserver mScriptExecutionObserver; + private final ScriptExecutionListener mScriptExecutionListener; + + public Wrapper(ScriptExecutionObserver scriptExecutionObserver, ScriptExecutionListener scriptExecutionListener) { + mScriptExecutionObserver = scriptExecutionObserver; + mScriptExecutionListener = scriptExecutionListener; + } + + @Override + public void onStart(ScriptExecution execution) { + mScriptExecutionListener.onStart(execution); + mScriptExecutionObserver.onStart(execution); + } + + @Override + public void onSuccess(ScriptExecution execution, Object result) { + mScriptExecutionListener.onSuccess(execution, result); + mScriptExecutionObserver.onSuccess(execution, result); + } + + @Override + public void onException(ScriptExecution execution, Throwable e) { + mScriptExecutionListener.onException(execution, e); + mScriptExecutionObserver.onException(execution, e); + } + } + +} diff --git a/app/src/main/java/com/stardust/autojs/execution/ScriptExecutionTask.java b/app/src/main/java/com/stardust/autojs/execution/ScriptExecutionTask.java new file mode 100644 index 00000000..c900c8df --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/execution/ScriptExecutionTask.java @@ -0,0 +1,38 @@ +package com.stardust.autojs.execution; + +import com.stardust.autojs.script.ScriptSource; + +import java.io.Serializable; + +/** + * Created by Stardust on 2017/4/2. + */ + +public class ScriptExecutionTask implements Serializable { + + private ScriptSource mScriptSource; + private ScriptExecutionListener mExecutionListener; + private ExecutionConfig mExecutionConfig; + + public ScriptExecutionTask(ScriptSource source, ScriptExecutionListener listener, ExecutionConfig config) { + mScriptSource = source; + mExecutionListener = listener; + mExecutionConfig = config; + } + + public ScriptSource getSource() { + return mScriptSource; + } + + public ScriptExecutionListener getListener() { + return mExecutionListener; + } + + public ExecutionConfig getConfig() { + return mExecutionConfig; + } + + public void setExecutionListener(ScriptExecutionListener executionListener) { + mExecutionListener = executionListener; + } +} diff --git a/app/src/main/java/com/stardust/autojs/execution/SimpleScriptExecutionListener.java b/app/src/main/java/com/stardust/autojs/execution/SimpleScriptExecutionListener.java new file mode 100644 index 00000000..d6fe1398 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/execution/SimpleScriptExecutionListener.java @@ -0,0 +1,23 @@ +package com.stardust.autojs.execution; + +/** + * Created by Stardust on 2017/4/2. + */ + +public class SimpleScriptExecutionListener implements ScriptExecutionListener { + + @Override + public void onStart(ScriptExecution execution) { + + } + + @Override + public void onSuccess(ScriptExecution execution, Object result) { + + } + + @Override + public void onException(ScriptExecution execution, Throwable e) { + + } +} diff --git a/app/src/main/java/com/stardust/autojs/project/BuildInfo.java b/app/src/main/java/com/stardust/autojs/project/BuildInfo.java new file mode 100644 index 00000000..dfbae11c --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/project/BuildInfo.java @@ -0,0 +1,59 @@ +package com.stardust.autojs.project; + + +import com.google.gson.annotations.SerializedName; + +import java.util.zip.CRC32; + +public class BuildInfo { + + @SerializedName("build_time") + private long mBuildTime; + + @SerializedName("build_id") + private String mBuildId; + + @SerializedName("build_number") + private long mBuildNumber; + + public BuildInfo() { + } + + public long getBuildNumber() { + return mBuildNumber; + } + + public void setBuildNumber(long buildNumber) { + mBuildNumber = buildNumber; + } + + public long getBuildTime() { + return mBuildTime; + } + + public void setBuildTime(long buildTime) { + mBuildTime = buildTime; + } + + public String getBuildId() { + return mBuildId; + } + + public void setBuildId(String buildId) { + mBuildId = buildId; + } + + public static BuildInfo generate(long buildNumber) { + BuildInfo info = new BuildInfo(); + info.setBuildNumber(buildNumber); + info.setBuildTime(System.currentTimeMillis()); + info.setBuildId(generateBuildId(buildNumber, info.getBuildTime())); + return info; + } + + private static String generateBuildId(long buildNumber, long buildTime) { + CRC32 crc32 = new CRC32(); + crc32.update((buildNumber + "" + buildTime).getBytes()); + return String.format("%08X", crc32.getValue()) + "-" + buildNumber; + } +} diff --git a/app/src/main/java/com/stardust/autojs/project/LaunchConfig.java b/app/src/main/java/com/stardust/autojs/project/LaunchConfig.java new file mode 100644 index 00000000..a10d38e8 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/project/LaunchConfig.java @@ -0,0 +1,22 @@ +package com.stardust.autojs.project; + +import com.google.gson.annotations.SerializedName; + +/** + * Created by Stardust on 2018/1/25. + */ + +public class LaunchConfig { + + @SerializedName("hideLogs") + private boolean mHideLogs = false; + + public boolean shouldHideLogs() { + return mHideLogs; + } + + public void setHideLogs(boolean hideLogs) { + mHideLogs = hideLogs; + } + +} diff --git a/app/src/main/java/com/stardust/autojs/project/ProjectConfig.java b/app/src/main/java/com/stardust/autojs/project/ProjectConfig.java new file mode 100644 index 00000000..0f61c122 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/project/ProjectConfig.java @@ -0,0 +1,251 @@ +package com.stardust.autojs.project; + +import android.content.Context; +import android.text.TextUtils; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.annotations.SerializedName; +import com.stardust.pio.PFiles; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Created by Stardust on 2018/1/24. + */ + +public class ProjectConfig { + + public static final String CONFIG_FILE_NAME = "project.json"; + + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + @SerializedName("name") + private String mName; + + @SerializedName("versionName") + private String mVersionName; + + @SerializedName("versionCode") + private int mVersionCode = -1; + + @SerializedName("packageName") + private String mPackageName; + + @SerializedName("main") + private String mMainScriptFile; + + @SerializedName("assets") + private List mAssets = new ArrayList<>(); + + @SerializedName("launchConfig") + private LaunchConfig mLaunchConfig; + + @SerializedName("build") + private BuildInfo mBuildInfo = new BuildInfo(); + + @SerializedName("icon") + private String mIcon; + + @SerializedName("scripts") + private Map mScriptConfigs = new HashMap<>(); + + @SerializedName("useFeatures") + private List mFeatures = new ArrayList<>(); + + + public static ProjectConfig fromJson(String json) { + if (json == null) { + return null; + } + ProjectConfig config = GSON.fromJson(json, ProjectConfig.class); + if (!isValid(config)) { + return null; + } + return config; + } + + private static boolean isValid(ProjectConfig config) { + if (TextUtils.isEmpty(config.getName())) { + return false; + } + if (TextUtils.isEmpty(config.getPackageName())) { + return false; + } + if (TextUtils.isEmpty(config.getVersionName())) { + return false; + } + if (TextUtils.isEmpty(config.getMainScriptFile())) { + return false; + } + if (config.getVersionCode() == -1) { + return false; + } + return true; + } + + + public static ProjectConfig fromAssets(Context context, String path) { + try { + return fromJson(PFiles.read(context.getAssets().open(path))); + } catch (Exception e) { + return null; + } + } + + public static ProjectConfig fromFile(String path) { + try { + return fromJson(PFiles.read(path)); + } catch (Exception e) { + return null; + } + } + + public static ProjectConfig fromProjectDir(String path) { + return fromFile(configFileOfDir(path)); + } + + + public static String configFileOfDir(String projectDir) { + return PFiles.join(projectDir, CONFIG_FILE_NAME); + } + + public BuildInfo getBuildInfo() { + return mBuildInfo; + } + + public void setBuildInfo(BuildInfo buildInfo) { + mBuildInfo = buildInfo; + } + + public String getName() { + return mName; + } + + public ProjectConfig setName(String name) { + mName = name; + return this; + } + + public String getVersionName() { + return mVersionName; + } + + public ProjectConfig setVersionName(String versionName) { + mVersionName = versionName; + return this; + } + + public int getVersionCode() { + return mVersionCode; + } + + public ProjectConfig setVersionCode(int versionCode) { + mVersionCode = versionCode; + return this; + } + + public String getPackageName() { + return mPackageName; + } + + public ProjectConfig setPackageName(String packageName) { + mPackageName = packageName; + return this; + } + + public String getMainScriptFile() { + return mMainScriptFile; + } + + public ProjectConfig setMainScriptFile(String mainScriptFile) { + mMainScriptFile = mainScriptFile; + return this; + } + + public Map getScriptConfigs() { + return mScriptConfigs; + } + + public List getAssets() { + if (mAssets == null) { + mAssets = Collections.emptyList(); + } + return mAssets; + } + + public boolean addAsset(String assetRelativePath) { + if (mAssets == null) { + mAssets = new ArrayList<>(); + } + for (String asset : mAssets) { + if (new File(asset).equals(new File(assetRelativePath))) { + return false; + } + } + mAssets.add(assetRelativePath); + return true; + } + + public void setAssets(List assets) { + mAssets = assets; + } + + public LaunchConfig getLaunchConfig() { + if (mLaunchConfig == null) { + mLaunchConfig = new LaunchConfig(); + } + return mLaunchConfig; + } + + public void setLaunchConfig(LaunchConfig launchConfig) { + mLaunchConfig = launchConfig; + } + + public String toJson() { + return GSON.toJson(this); + } + + public String getIcon() { + return mIcon; + } + + public void setIcon(String icon) { + mIcon = icon; + } + + public String getBuildDir() { + return "build"; + } + + public List getFeatures() { + return mFeatures; + } + + public void setFeatures(List features) { + mFeatures = features; + } + + public ScriptConfig getScriptConfig(String path) { + ScriptConfig config = mScriptConfigs.get(path); + if (config == null) { + config = new ScriptConfig(); + } + if(mFeatures.isEmpty()){ + return config; + } + ArrayList features = new ArrayList<>(config.getFeatures()); + for (String feature : mFeatures) { + if (!features.contains(feature)) { + features.add(feature); + } + } + config.setFeatures(features); + return config; + } +} diff --git a/app/src/main/java/com/stardust/autojs/project/ProjectLauncher.java b/app/src/main/java/com/stardust/autojs/project/ProjectLauncher.java new file mode 100644 index 00000000..c8e85349 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/project/ProjectLauncher.java @@ -0,0 +1,28 @@ +package com.stardust.autojs.project; + +import com.stardust.autojs.ScriptEngineService; +import com.stardust.autojs.execution.ExecutionConfig; +import com.stardust.autojs.script.JavaScriptFileSource; + +import java.io.File; + +public class ProjectLauncher { + + private String mProjectDir; + private File mMainScriptFile; + private ProjectConfig mProjectConfig; + + public ProjectLauncher(String projectDir) { + mProjectDir = projectDir; + mProjectConfig = ProjectConfig.fromProjectDir(projectDir); + mMainScriptFile = new File(mProjectDir, mProjectConfig.getMainScriptFile()); + } + + public void launch(ScriptEngineService service) { + ExecutionConfig config = new ExecutionConfig(); + config.setWorkingDirectory(mProjectDir); + config.getScriptConfig().setFeatures(mProjectConfig.getFeatures()); + service.execute(new JavaScriptFileSource(mMainScriptFile), config); + } + +} diff --git a/app/src/main/java/com/stardust/autojs/project/ScriptConfig.kt b/app/src/main/java/com/stardust/autojs/project/ScriptConfig.kt new file mode 100644 index 00000000..5737a34b --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/project/ScriptConfig.kt @@ -0,0 +1,18 @@ +package com.stardust.autojs.project + +import com.google.gson.annotations.SerializedName + +data class ScriptConfig( + @SerializedName("useFeatures") var features: List, + @SerializedName("uiMode") var uiMode: Boolean +) { + constructor() : this(emptyList(), false) + + fun hasFeature(feature: String): Boolean { + return features.contains(feature) + } + + companion object { + val FEATURE_CONTINUATION = "continuation" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/stardust/autojs/rhino/AndroidClassLoader.java b/app/src/main/java/com/stardust/autojs/rhino/AndroidClassLoader.java new file mode 100644 index 00000000..2e6d852e --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/rhino/AndroidClassLoader.java @@ -0,0 +1,14 @@ +package com.stardust.autojs.rhino; + +import org.jetbrains.annotations.NotNull; + +import java.io.File; + +/** + * Created by Stardust on 2017/4/5. + */ +public class AndroidClassLoader extends org.autojs.autojs.rhino.AndroidClassLoader { + public AndroidClassLoader(@NotNull ClassLoader parent, @NotNull File cacheDir) { + super(parent, cacheDir); + } +} diff --git a/app/src/main/java/com/stardust/autojs/rhino/AndroidContextFactory.java b/app/src/main/java/com/stardust/autojs/rhino/AndroidContextFactory.java new file mode 100644 index 00000000..25252b27 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/rhino/AndroidContextFactory.java @@ -0,0 +1,54 @@ +package com.stardust.autojs.rhino; + + +import com.stardust.autojs.runtime.exception.ScriptInterruptedException; + +import org.mozilla.javascript.Context; +import org.mozilla.javascript.tools.shell.ShellContextFactory; + +import java.io.File; + +/** + * Created by Stardust on 2017/4/5. + */ + +public class AndroidContextFactory extends ShellContextFactory { + private final File cacheDirectory; + + /** + * Create a new factory. It will cache generated code in the given directory + * + * @param cacheDirectory the cache directory + */ + public AndroidContextFactory(File cacheDirectory) { + this.cacheDirectory = cacheDirectory; + initApplicationClassLoader(createClassLoader(AndroidContextFactory.class.getClassLoader())); + } + + /** + * Create a ClassLoader which is able to deal with bytecode + * + * @param parent the parent of the create classloader + * @return a new ClassLoader + */ + @Override + protected AndroidClassLoader createClassLoader(ClassLoader parent) { + return new AndroidClassLoader(parent, cacheDirectory); + } + + @Override + protected void observeInstructionCount(Context cx, int instructionCount) { + if (Thread.currentThread().isInterrupted()) { + throw new ScriptInterruptedException(); + } + } + + @Override + protected Context makeContext() { + Context cx = super.makeContext(); + cx.setInstructionObserverThreshold(10000); + return cx; + } + + +} diff --git a/app/src/main/java/com/stardust/autojs/rhino/AutoJsContext.kt b/app/src/main/java/com/stardust/autojs/rhino/AutoJsContext.kt new file mode 100644 index 00000000..cf1615c9 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/rhino/AutoJsContext.kt @@ -0,0 +1,27 @@ +package com.stardust.autojs.rhino + +import org.mozilla.javascript.Context +import org.mozilla.javascript.ContextFactory +import org.mozilla.javascript.ContinuationPending +import org.mozilla.javascript.Scriptable + +class AutoJsContext(factory: ContextFactory?) : Context(factory) { + + private val mContinuations = HashSet() + + override fun captureContinuation(): ContinuationPending { + val continuationPending = super.captureContinuation() + mContinuations.add(continuationPending.continuation) + return continuationPending + } + + override fun resumeContinuation(continuation: Any, scope: Scriptable?, functionResult: Any?): Any { + mContinuations.remove(continuation) + return super.resumeContinuation(continuation, scope, functionResult) + } + + fun hasPendingContinuation(): Boolean { + return mContinuations.isNotEmpty() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/stardust/autojs/rhino/BindingNativeJavaObject.java b/app/src/main/java/com/stardust/autojs/rhino/BindingNativeJavaObject.java new file mode 100644 index 00000000..82fcb541 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/rhino/BindingNativeJavaObject.java @@ -0,0 +1,30 @@ +package com.stardust.autojs.rhino; + +import org.mozilla.javascript.NativeJavaObject; +import org.mozilla.javascript.Scriptable; + +public class BindingNativeJavaObject extends NativeJavaObject { + + public BindingNativeJavaObject() { + } + + public BindingNativeJavaObject(Scriptable scope, Object javaObject, Class staticType) { + super(scope, javaObject, staticType); + } + + public BindingNativeJavaObject(Scriptable scope, Object javaObject, Class staticType, boolean isAdapter) { + super(scope, javaObject, staticType, isAdapter); + } + + @Override + protected void initMembers() { + Class dynamicType; + if (this.javaObject != null) { + dynamicType = this.javaObject.getClass(); + } else { + dynamicType = this.staticType; + } + + + } +} diff --git a/app/src/main/java/com/stardust/autojs/rhino/InterruptibleAndroidContextFactory.java b/app/src/main/java/com/stardust/autojs/rhino/InterruptibleAndroidContextFactory.java new file mode 100644 index 00000000..07fcc61b --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/rhino/InterruptibleAndroidContextFactory.java @@ -0,0 +1,55 @@ +package com.stardust.autojs.rhino; + +import android.os.Looper; +import android.util.Log; + +import com.stardust.autojs.runtime.exception.ScriptInterruptedException; + +import org.mozilla.javascript.Context; + +import java.io.File; +import java.util.concurrent.atomic.AtomicInteger; + +public class InterruptibleAndroidContextFactory extends AndroidContextFactory { + + private AtomicInteger mContextCount = new AtomicInteger(); + private static final String LOG_TAG = "ContextFactory"; + + /** + * Create a new factory. It will cache generated code in the given directory + * + * @param cacheDirectory the cache directory + */ + public InterruptibleAndroidContextFactory(File cacheDirectory) { + super(cacheDirectory); + } + + @Override + protected void observeInstructionCount(Context cx, int instructionCount) { + if (Thread.currentThread().isInterrupted() && Looper.myLooper() != Looper.getMainLooper()) { + throw new ScriptInterruptedException(); + } + } + + @Override + protected Context makeContext() { + Context cx = new AutoJsContext(this); + cx.setInstructionObserverThreshold(10000); + return cx; + } + + @Override + protected void onContextCreated(Context cx) { + super.onContextCreated(cx); + int i = mContextCount.incrementAndGet(); + Log.d(LOG_TAG, "onContextCreated: count = " + i); + } + + @Override + protected void onContextReleased(Context cx) { + super.onContextReleased(cx); + int i = mContextCount.decrementAndGet(); + Log.d(LOG_TAG, "onContextReleased: count = " + i); + } + +} diff --git a/app/src/main/java/com/stardust/autojs/rhino/NativeJavaClassWithPrototype.java b/app/src/main/java/com/stardust/autojs/rhino/NativeJavaClassWithPrototype.java new file mode 100644 index 00000000..e8c15cb9 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/rhino/NativeJavaClassWithPrototype.java @@ -0,0 +1,90 @@ +package com.stardust.autojs.rhino; + +import org.mozilla.javascript.EvaluatorException; +import org.mozilla.javascript.NativeJavaClass; +import org.mozilla.javascript.NativeObject; +import org.mozilla.javascript.Scriptable; + +import java.util.concurrent.ConcurrentHashMap; + +/** + * Created by Stardust on 2018/4/4. + */ + +public class NativeJavaClassWithPrototype extends NativeJavaClass { + + private static final Object NULL = new Object(); + private ConcurrentHashMap mProperties = new ConcurrentHashMap<>(); + + public NativeJavaClassWithPrototype(Scriptable scope, Class javaClass) { + super(scope, javaClass); + } + + @Override + public boolean has(String name, Scriptable start) { + return mProperties.containsKey(name) || super.has(name, start) || (prototype != null && prototype.has(name, start)) + || name.equals("prototype"); + } + + @Override + public Object get(String name, Scriptable start) { + if (name.equals("prototype")) { + return prototype; + } + Object value = mProperties.get(name); + if (value != null) { + return unwrapValue(value); + } + try { + value = super.get(name, start); + } catch (EvaluatorException e) { + if (!memberNotFound(e)) { + throw e; + } + } + if (value != Scriptable.NOT_FOUND) { + return value; + } + if (prototype == null) { + return Scriptable.NOT_FOUND; + } + return prototype.get(name, start); + } + + private Object unwrapValue(Object value) { + if (value == NULL) + return null; + return value; + } + + @Override + public void put(String name, Scriptable start, Object value) { + if (name.equals("prototype")) { + prototype = (Scriptable) value; + return; + } + if (mProperties.containsKey(name)) { + mProperties.put(name, wrapValue(value)); + } + try { + super.put(name, start, value); + } catch (EvaluatorException e) { + if (memberNotFound(e)) { + mProperties.put(name, wrapValue(value)); + } else { + throw e; + } + } + } + + private Object wrapValue(Object value) { + if (value == null) + return NULL; + return value; + } + + private static boolean memberNotFound(EvaluatorException e) { + return e.getMessage() != null && e.getMessage().startsWith("Java class \"com.stardust.autojs.rhino.NativeJavaObjectWithPrototype\""); + } + +} diff --git a/app/src/main/java/com/stardust/autojs/rhino/NativeJavaObjectWithPrototype.java b/app/src/main/java/com/stardust/autojs/rhino/NativeJavaObjectWithPrototype.java new file mode 100644 index 00000000..3de1ffc3 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/rhino/NativeJavaObjectWithPrototype.java @@ -0,0 +1,52 @@ +package com.stardust.autojs.rhino; + +import org.mozilla.javascript.NativeJavaObject; +import org.mozilla.javascript.Scriptable; + +/** + * Created by Stardust on 2018/4/4. + */ + +public class NativeJavaObjectWithPrototype extends NativeJavaObject { + + + public NativeJavaObjectWithPrototype(Scriptable scope, Object javaObject, Class staticType) { + super(scope, javaObject, staticType); + } + + public NativeJavaObjectWithPrototype(Scriptable scope, Object javaObject, Class staticType, boolean isAdapter) { + super(scope, javaObject, staticType, isAdapter); + } + + public NativeJavaObjectWithPrototype() { + } + + @Override + public boolean has(String name, Scriptable start) { + return super.has(name, start) || (prototype != null && prototype.has(name, start)) + || name.equals("__proto__"); + } + + @Override + public Object get(String name, Scriptable start) { + if (name.equals("__proto__")) { + return prototype; + } + if(super.has(name, start)){ + return super.get(name, start); + } + if (prototype == null) { + return Scriptable.NOT_FOUND; + } + return prototype.get(name, start); + } + + @Override + public void put(String name, Scriptable start, Object value) { + if (name.equals("__proto__")) { + prototype = (Scriptable) value; + return; + } + super.put(name, start, value); + } +} diff --git a/app/src/main/java/com/stardust/autojs/rhino/NoSecurityController.java b/app/src/main/java/com/stardust/autojs/rhino/NoSecurityController.java new file mode 100644 index 00000000..bca52b71 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/rhino/NoSecurityController.java @@ -0,0 +1,23 @@ +package com.stardust.autojs.rhino; + +import org.mozilla.javascript.Context; +import org.mozilla.javascript.GeneratedClassLoader; +import org.mozilla.javascript.SecurityController; + +import java.io.Serializable; + +/** + * Created by Stardust on 2017/4/5. + */ + +public class NoSecurityController extends SecurityController implements Serializable { + @Override + public GeneratedClassLoader createClassLoader(ClassLoader classLoader, Object o) { + return Context.getCurrentContext().createClassLoader(classLoader); + } + + @Override + public Object getDynamicSecurityDomain(Object o) { + return null; + } +} diff --git a/app/src/main/java/com/stardust/autojs/rhino/ProxyJavaObject.java b/app/src/main/java/com/stardust/autojs/rhino/ProxyJavaObject.java new file mode 100644 index 00000000..cb9bf9e3 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/rhino/ProxyJavaObject.java @@ -0,0 +1,70 @@ +package com.stardust.autojs.rhino; + +import org.mozilla.javascript.Context; +import org.mozilla.javascript.NativeFunction; +import org.mozilla.javascript.NativeJavaObject; +import org.mozilla.javascript.NativeObject; +import org.mozilla.javascript.Scriptable; +import org.mozilla.javascript.UniqueTag; + +/** + * Created by Stardust on 2017/12/6. + */ + +public class ProxyJavaObject extends NativeJavaObject { + + private NativeFunction mGetter; + private NativeFunction mSetter; + + public ProxyJavaObject() { + } + + public ProxyJavaObject(Scriptable scope, Object javaObject, Class staticType) { + super(scope, javaObject, staticType); + } + + public ProxyJavaObject(Scriptable scope, Object javaObject, Class staticType, boolean isAdapter) { + super(scope, javaObject, staticType, isAdapter); + } + + @Override + public void put(String name, Scriptable start, Object value) { + if (name.equals("__proxy__")) { + NativeObject proxy = (NativeObject) value; + Object getter = proxy.get("get", start); + if (getter instanceof NativeFunction) { + mGetter = (NativeFunction) getter; + } + Object setter = proxy.get("set", start); + if (setter instanceof NativeFunction) { + mSetter = (NativeFunction) setter; + } + } else if (mSetter != null) { + mSetter.call(Context.getCurrentContext(), start, start, new Object[]{name, value}); + } else { + super.put(name, start, value); + } + } + + public Object getWithoutProxy(String name, Scriptable start) { + return super.get(name, start); + } + + @Override + public Object get(String name, Scriptable start) { + Object value = super.get(name, start); + if (value != null && value != UniqueTag.NOT_FOUND) { + return value; + } + if (mGetter != null) { + value = mGetter.call(Context.getCurrentContext(), start, start, new Object[]{name}); + } + return value; + } + + @Override + public Object getDefaultValue(Class typeHint) { + return toString(); + } +} + diff --git a/app/src/main/java/com/stardust/autojs/rhino/ProxyObject.java b/app/src/main/java/com/stardust/autojs/rhino/ProxyObject.java new file mode 100644 index 00000000..8fd84f56 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/rhino/ProxyObject.java @@ -0,0 +1,57 @@ +package com.stardust.autojs.rhino; + +import org.mozilla.javascript.Context; +import org.mozilla.javascript.NativeFunction; +import org.mozilla.javascript.NativeObject; +import org.mozilla.javascript.Scriptable; +import org.mozilla.javascript.UniqueTag; + +/** + * Created by Stardust on 2017/5/17. + */ + +public class ProxyObject extends NativeObject { + + private NativeFunction mGetter; + private NativeFunction mSetter; + + @Override + public void put(String name, Scriptable start, Object value) { + if (name.equals("__proxy__")) { + NativeObject proxy = (NativeObject) value; + Object getter = proxy.get("get", start); + if (getter instanceof NativeFunction) { + mGetter = (NativeFunction) getter; + } + Object setter = proxy.get("set", start); + if (setter instanceof NativeFunction) { + mSetter = (NativeFunction) setter; + } + } else if (mSetter != null) { + mSetter.call(Context.getCurrentContext(), start, start, new Object[]{name, value}); + } else { + super.put(name, start, value); + } + } + + public Object getWithoutProxy(String name, Scriptable start) { + return super.get(name, start); + } + + @Override + public Object get(String name, Scriptable start) { + Object value = super.get(name, start); + if (value != null && value != UniqueTag.NOT_FOUND) { + return value; + } + if (mGetter != null) { + value = mGetter.call(Context.getCurrentContext(), start, start, new Object[]{name}); + } + return value; + } + + @Override + public Object getDefaultValue(Class typeHint) { + return toString(); + } +} diff --git a/app/src/main/java/com/stardust/autojs/rhino/RhinoAndroidHelper.java b/app/src/main/java/com/stardust/autojs/rhino/RhinoAndroidHelper.java new file mode 100644 index 00000000..b3ec0809 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/rhino/RhinoAndroidHelper.java @@ -0,0 +1,93 @@ +package com.stardust.autojs.rhino; + +import androidx.annotation.VisibleForTesting; + + +import org.mozilla.javascript.Context; +import org.mozilla.javascript.ContextFactory; +import org.mozilla.javascript.SecurityController; + +import java.io.File; +import java.io.IOException; + +/** + * Created by Stardust on 2017/4/5. + */ + +public class RhinoAndroidHelper { + + private final File cacheDirectory; + + /** + * Constructs a new helper using the default temporary directory. + * Note: It is recommended to use a custom directory, so no permission problems occur. + */ + public RhinoAndroidHelper() { + this(new File(System.getProperty("java.io.tmpdir", "."), "classes")); + } + + /** + * Constructs a new helper using a directory in the applications cache. + * + * @param context any context + */ + public RhinoAndroidHelper(android.content.Context context) { + this(new File(context.getCacheDir(), "classes")); + } + + /** + * Constructs a helper using the specified directory as cache. + * + * @param cacheDirectory the cache directory to use + */ + public RhinoAndroidHelper(File cacheDirectory) { + this.cacheDirectory = cacheDirectory; + } + + /** + * call this instead of {@link Context#enter()} + * + * @return a context prepared for android + */ + public Context enterContext() { + if (!SecurityController.hasGlobal()) + SecurityController.initGlobal(new NoSecurityController()); + return getContextFactory().enterContext(); + } + + /** + * @return The Context factory which has to be used on android. + */ + @VisibleForTesting + public AndroidContextFactory getContextFactory() { + AndroidContextFactory factory; + if (!ContextFactory.hasExplicitGlobal()) { + factory = new AndroidContextFactory(cacheDirectory); + ContextFactory.getGlobalSetter().setContextFactoryGlobal(factory); + } else if (!(ContextFactory.getGlobal() instanceof AndroidContextFactory)) { + throw new IllegalStateException("Cannot initialize factory for Android Rhino: There is already another factory"); + } else { + factory = (AndroidContextFactory) ContextFactory.getGlobal(); + } + return factory; + } + + /** + * Compiles all classes in a jar file. They will be available in Rhino. + * + * @param jar the jar to load + * @throws IOException if the jar cannot be read or is invalid or there is a problem with the cache + */ + public void loadClassJar(File jar) throws IOException { + ((AndroidClassLoader) getContextFactory().getApplicationClassLoader()).loadJar(jar); + } + + /** + * @return a context prepared for android + * @deprecated use {@link #enterContext()} instead + */ + @Deprecated + public static Context prepareContext() { + return new RhinoAndroidHelper().enterContext(); + } +} diff --git a/app/src/main/java/com/stardust/autojs/rhino/TokenStream.java b/app/src/main/java/com/stardust/autojs/rhino/TokenStream.java new file mode 100644 index 00000000..cfe6f5ba --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/rhino/TokenStream.java @@ -0,0 +1,15 @@ +/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package com.stardust.autojs.rhino; + +import java.io.Reader; + +public class TokenStream extends org.autojs.autojs.rhino.TokenStream { + public TokenStream(Reader sourceReader, String sourceString, int lineno) { + super(sourceReader, sourceString, lineno); + } +} diff --git a/app/src/main/java/com/stardust/autojs/rhino/TopLevelScope.java b/app/src/main/java/com/stardust/autojs/rhino/TopLevelScope.java new file mode 100644 index 00000000..338faade --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/rhino/TopLevelScope.java @@ -0,0 +1,6 @@ +package com.stardust.autojs.rhino; + +import org.mozilla.javascript.ImporterTopLevel; + +public class TopLevelScope extends ImporterTopLevel { +} diff --git a/app/src/main/java/com/stardust/autojs/rhino/continuation/Continuation.kt b/app/src/main/java/com/stardust/autojs/rhino/continuation/Continuation.kt new file mode 100644 index 00000000..8520711b --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/rhino/continuation/Continuation.kt @@ -0,0 +1,59 @@ +package com.stardust.autojs.rhino.continuation + +import com.stardust.autojs.core.looper.Timer +import com.stardust.autojs.core.looper.TimerThread +import com.stardust.autojs.rhino.AutoJsContext +import com.stardust.autojs.runtime.ScriptRuntime + +import org.mozilla.javascript.Context +import org.mozilla.javascript.ContinuationPending +import org.mozilla.javascript.Scriptable + +class Continuation(val context: AutoJsContext, val scope: Scriptable, private val mTimer: Timer) { + var pending: ContinuationPending? = null + private set + private val mThread: Thread = Thread.currentThread() + + class Result(val result: Any?, val error: Any?) { + companion object { + + fun success(result: Any?): Result { + return Result(result, null) + } + + fun failure(error: Any?): Result { + return Result(null, error) + } + } + } + + fun suspend() { + if (pending != null) { + throw IllegalStateException("call suspend twice!") + } + context.captureContinuation().let { + pending = it + throw it + } + } + + fun resumeWith(result: Result) { + val continuation = pending?.continuation + ?: throw IllegalStateException("call resume() without suspend()!") + if (mThread == Thread.currentThread()) { + context.resumeContinuation(continuation, scope, result) + } else { + mTimer.postDelayed({ + context.resumeContinuation(continuation, scope, result) + }, 0) + } + } + + companion object { + + fun create(runtime: ScriptRuntime, scope: Scriptable): Continuation { + val context = Context.getCurrentContext() as AutoJsContext + return Continuation(context, scope, runtime.timers.timerForCurrentThread) + } + } +} diff --git a/app/src/main/java/com/stardust/autojs/rhino/debug/DebugCallback.java b/app/src/main/java/com/stardust/autojs/rhino/debug/DebugCallback.java new file mode 100644 index 00000000..eb6d1372 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/rhino/debug/DebugCallback.java @@ -0,0 +1,17 @@ +package com.stardust.autojs.rhino.debug; + +public interface DebugCallback { + + /** + * Called when the source text of some script has been changed. + */ + void updateSourceText(Dim.SourceInfo sourceInfo); + + /** + * Called when the interrupt loop has been entered. + */ + void enterInterrupt(Dim.StackFrame lastFrame, + String threadTitle, + String alertMessage); + +} diff --git a/app/src/main/java/com/stardust/autojs/rhino/debug/DebugCallbackInternal.java b/app/src/main/java/com/stardust/autojs/rhino/debug/DebugCallbackInternal.java new file mode 100644 index 00000000..153b526d --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/rhino/debug/DebugCallbackInternal.java @@ -0,0 +1,49 @@ +package com.stardust.autojs.rhino.debug; +/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + +import com.stardust.autojs.engine.RhinoJavaScriptEngine; +import com.stardust.autojs.engine.ScriptEngine; + +import org.mozilla.javascript.Context; + +/** + * Interface for communication between the debugger and its GUI. This + * should be implemented by the GUI. + */ +interface DebugCallbackInternal { + + /** + * Called when the source text of some script has been changed. + */ + void updateSourceText(Dim.SourceInfo sourceInfo); + + /** + * Called when the interrupt loop has been entered. + */ + void enterInterrupt(Dim.StackFrame lastFrame, + String threadTitle, + String alertMessage); + + /** + * Returns whether the current thread is the GUI's event thread. + * This information is required to avoid blocking the event thread + * from the debugger. + */ + boolean isGuiEventThread(); + + /** + * Processes the next GUI event. This manual pumping of GUI events + * is necessary when the GUI event thread itself has been stopped. + */ + void dispatchNextGuiEvent() throws InterruptedException; + + /** + * Returns whether the debugger should attach to this engine or not. + */ + boolean shouldAttachDebugger(RhinoJavaScriptEngine engine); +} \ No newline at end of file diff --git a/app/src/main/java/com/stardust/autojs/rhino/debug/Debugger.java b/app/src/main/java/com/stardust/autojs/rhino/debug/Debugger.java new file mode 100644 index 00000000..b84401d7 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/rhino/debug/Debugger.java @@ -0,0 +1,10 @@ +package com.stardust.autojs.rhino.debug; + +import org.autojs.autojs.engine.ScriptEngineService; +import org.mozilla.javascript.ContextFactory; + +public class Debugger extends org.autojs.autojs.rhino.debug.Debugger { + public Debugger(ScriptEngineService scriptEngineService, ContextFactory contextFactory) { + super(scriptEngineService, contextFactory); + } +} diff --git a/app/src/main/java/com/stardust/autojs/rhino/debug/Dim.java b/app/src/main/java/com/stardust/autojs/rhino/debug/Dim.java new file mode 100644 index 00000000..3ca52694 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/rhino/debug/Dim.java @@ -0,0 +1,5 @@ +package com.stardust.autojs.rhino.debug; + +public class Dim extends org.autojs.autojs.rhino.debug.Dim { + /* Empty body. */ +} \ No newline at end of file diff --git a/app/src/main/java/com/stardust/autojs/runtime/ScriptBridges.java b/app/src/main/java/com/stardust/autojs/runtime/ScriptBridges.java new file mode 100644 index 00000000..072d9c2c --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/runtime/ScriptBridges.java @@ -0,0 +1,58 @@ +package com.stardust.autojs.runtime; + +/** + * Created by Stardust on 2017/7/21. + */ + +public class ScriptBridges { + + + public interface Bridges { + + Object[] NO_ARGUMENTS = new Object[0]; + + Object call(Object func, Object target, Object arg); + + Object toArray(Iterable o); + + Object toString(Object obj); + + Object asArray(Object obj); + } + + private Bridges mBridges; + + public void setBridges(Bridges bridges) { + mBridges = bridges; + } + + public Bridges getBridges() { + return mBridges; + } + + public Object callFunction(Object func, Object target, Object args) { + checkBridges(); + return mBridges.call(func, target, args); + } + + private void checkBridges() { + if (mBridges == null) + throw new IllegalStateException("no bridges set"); + } + + + public Object toArray(Iterable c) { + checkBridges(); + return mBridges.toArray(c); + } + + public Object toString(Object obj) { + checkBridges(); + return mBridges.toString(obj); + } + + public Object asArray(Object obj) { + checkBridges(); + return mBridges.asArray(obj); + } +} diff --git a/app/src/main/java/com/stardust/autojs/runtime/ScriptRuntime.java b/app/src/main/java/com/stardust/autojs/runtime/ScriptRuntime.java new file mode 100644 index 00000000..b9ebf55a --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/runtime/ScriptRuntime.java @@ -0,0 +1,491 @@ +package com.stardust.autojs.runtime; + +import android.content.Context; +import android.os.Build; +import android.os.Looper; +import android.util.Log; + +import com.stardust.app.GlobalAppContext; +import org.autojs.autojs6.R; +import com.stardust.autojs.ScriptEngineService; +import com.stardust.autojs.annotation.ScriptVariable; +import com.stardust.autojs.core.accessibility.AccessibilityBridge; +import com.stardust.autojs.core.image.Colors; +import com.stardust.autojs.core.permission.Permissions; +import com.stardust.autojs.rhino.AndroidClassLoader; +import com.stardust.autojs.rhino.TopLevelScope; +import com.stardust.autojs.rhino.continuation.Continuation; +import com.stardust.autojs.runtime.api.AbstractShell; +import com.stardust.autojs.runtime.api.AppUtils; +import com.stardust.autojs.runtime.api.Console; +import com.stardust.autojs.runtime.api.Device; +import com.stardust.autojs.runtime.api.Engines; +import com.stardust.autojs.runtime.api.Events; +import com.stardust.autojs.runtime.api.Files; +import com.stardust.autojs.runtime.api.Floaty; +import com.stardust.autojs.core.looper.Loopers; +import com.stardust.autojs.runtime.api.Media; +import com.stardust.autojs.runtime.api.Plugins; +import com.stardust.autojs.runtime.api.Sensors; +import com.stardust.autojs.runtime.api.Threads; +import com.stardust.autojs.runtime.api.Timers; +import com.stardust.autojs.core.accessibility.UiSelector; +import com.stardust.autojs.runtime.api.Images; +import com.stardust.autojs.core.image.capture.ScreenCaptureRequester; +import com.stardust.autojs.runtime.api.Dialogs; +import com.stardust.autojs.runtime.exception.ScriptEnvironmentException; +import com.stardust.autojs.runtime.exception.ScriptException; +import com.stardust.autojs.runtime.exception.ScriptInterruptedException; +import com.stardust.autojs.core.accessibility.SimpleActionAutomator; +import com.stardust.autojs.runtime.api.UI; +import com.stardust.concurrent.VolatileDispose; +import com.stardust.lang.ThreadCompat; +import com.stardust.pio.UncheckedIOException; +import com.stardust.util.ClipboardUtil; +import com.stardust.autojs.core.util.ProcessShell; +import com.stardust.util.ScreenMetrics; +import com.stardust.util.SdkVersionUtil; +import com.stardust.util.Supplier; +import com.stardust.util.UiHandler; +import com.stardust.autojs.core.activity.ActivityInfoProvider; + +import org.mozilla.javascript.ContextFactory; +import org.mozilla.javascript.RhinoException; +import org.mozilla.javascript.ScriptStackElement; +import org.mozilla.javascript.Scriptable; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringReader; +import java.io.StringWriter; +import java.lang.ref.WeakReference; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + + +/** + * Created by Stardust on 2017/1/27. + */ + +public class ScriptRuntime { + + private static final String TAG = "ScriptRuntime"; + + + public static class Builder { + private UiHandler mUiHandler; + private Console mConsole; + private AccessibilityBridge mAccessibilityBridge; + private Supplier mShellSupplier; + private ScreenCaptureRequester mScreenCaptureRequester; + private AppUtils mAppUtils; + private ScriptEngineService mEngineService; + + public Builder() { + + } + + public Builder setUiHandler(UiHandler uiHandler) { + mUiHandler = uiHandler; + return this; + } + + public Builder setConsole(Console console) { + mConsole = console; + return this; + } + + public Builder setAccessibilityBridge(AccessibilityBridge accessibilityBridge) { + mAccessibilityBridge = accessibilityBridge; + return this; + } + + public Builder setShellSupplier(Supplier shellSupplier) { + mShellSupplier = shellSupplier; + return this; + } + + public Builder setScreenCaptureRequester(ScreenCaptureRequester requester) { + mScreenCaptureRequester = requester; + return this; + } + + public Builder setAppUtils(AppUtils appUtils) { + mAppUtils = appUtils; + return this; + } + + public Builder setEngineService(ScriptEngineService service) { + mEngineService = service; + return this; + } + + + public ScriptRuntime build() { + return new ScriptRuntime(this); + } + + } + + + @ScriptVariable + public final AppUtils app; + + @ScriptVariable + public final Console console; + + @ScriptVariable + public final SimpleActionAutomator automator; + + @ScriptVariable + public final ActivityInfoProvider info; + + @ScriptVariable + public final UI ui; + + @ScriptVariable + public final Dialogs dialogs; + + @ScriptVariable + public Events events; + + @ScriptVariable + public final ScriptBridges bridges = new ScriptBridges(); + + @ScriptVariable + public Loopers loopers; + + @ScriptVariable + public Timers timers; + + @ScriptVariable + public Device device; + + @ScriptVariable + public final AccessibilityBridge accessibilityBridge; + + @ScriptVariable + public final Engines engines; + + @ScriptVariable + public Threads threads; + + @ScriptVariable + public final Floaty floaty; + + @ScriptVariable + public UiHandler uiHandler; + + @ScriptVariable + public final Colors colors = new Colors(); + + @ScriptVariable + public final Files files; + + @ScriptVariable + public Sensors sensors; + + @ScriptVariable + public final Media media; + + @ScriptVariable + public final Plugins plugins; + + private Images images; + + private static WeakReference applicationContext; + private Map mProperties = new ConcurrentHashMap<>(); + private AbstractShell mRootShell; + private Supplier mShellSupplier; + private ScreenMetrics mScreenMetrics = new ScreenMetrics(); + private Thread mThread; + private TopLevelScope mTopLevelScope; + + + protected ScriptRuntime(Builder builder) { + uiHandler = builder.mUiHandler; + Context context = uiHandler.getContext(); + app = builder.mAppUtils; + console = builder.mConsole; + accessibilityBridge = builder.mAccessibilityBridge; + mShellSupplier = builder.mShellSupplier; + ui = new UI(context, this); + this.automator = new SimpleActionAutomator(accessibilityBridge, this); + automator.setScreenMetrics(mScreenMetrics); + this.info = accessibilityBridge.getInfoProvider(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + images = new Images(context, this, builder.mScreenCaptureRequester); + } + engines = new Engines(builder.mEngineService, this); + dialogs = new Dialogs(this); + device = new Device(context); + floaty = new Floaty(uiHandler, ui, this); + files = new Files(this); + media = new Media(context, this); + plugins = new Plugins(context, this); + } + + public void init() { + if (loopers != null) + throw new IllegalStateException("already initialized"); + threads = new Threads(this); + timers = new Timers(this); + loopers = new Loopers(this); + events = new Events(uiHandler.getContext(), accessibilityBridge, this); + mThread = Thread.currentThread(); + sensors = new Sensors(uiHandler.getContext(), this); + } + + public TopLevelScope getTopLevelScope() { + return mTopLevelScope; + } + + public void setTopLevelScope(TopLevelScope topLevelScope) { + if (mTopLevelScope != null) { + throw new IllegalStateException("top level has already exists"); + } + mTopLevelScope = topLevelScope; + } + + public static void setApplicationContext(Context context) { + applicationContext = new WeakReference<>(context); + } + + public static Context getApplicationContext() { + if (applicationContext == null || applicationContext.get() == null) { + throw new ScriptEnvironmentException("No application context"); + } + return applicationContext.get(); + } + + public UiHandler getUiHandler() { + return uiHandler; + } + + public AccessibilityBridge getAccessibilityBridge() { + return accessibilityBridge; + } + + public void toast(final String text) { + uiHandler.toast(text); + } + + public void sleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + throw new ScriptInterruptedException(); + } + } + + public void setClip(final String text) { + if (Looper.myLooper() == Looper.getMainLooper()) { + ClipboardUtil.setClip(uiHandler.getContext(), text); + return; + } + VolatileDispose dispose = new VolatileDispose<>(); + uiHandler.post(() -> { + ClipboardUtil.setClip(uiHandler.getContext(), text); + dispose.setAndNotify(text); + }); + dispose.blockedGet(); + } + + public String getClip() { + if (Looper.myLooper() == Looper.getMainLooper()) { + return ClipboardUtil.getClipOrEmpty(uiHandler.getContext()).toString(); + } + final VolatileDispose clip = new VolatileDispose<>(); + uiHandler.post(() -> clip.setAndNotify(ClipboardUtil.getClipOrEmpty(uiHandler.getContext()).toString())); + return clip.blockedGetOrThrow(ScriptInterruptedException.class); + } + + public AbstractShell getRootShell() { + ensureRootShell(); + return mRootShell; + } + + private void ensureRootShell() { + if (mRootShell == null) { + mRootShell = mShellSupplier.get(); + mRootShell.SetScreenMetrics(mScreenMetrics); + mShellSupplier = null; + } + } + + public AbstractShell.Result shell(String cmd, int root) { + return ProcessShell.execCommand(cmd, root != 0); + } + + public UiSelector selector() { + return new UiSelector(accessibilityBridge); + } + + public boolean isStopped() { + return Thread.currentThread().isInterrupted(); + } + + public static void requiresApi(int i) { + if (Build.VERSION.SDK_INT < i) { + throw new ScriptException("Requires sdk version to run the script: " + SdkVersionUtil.sdkIntToString(i)); + } + } + + public void requestPermissions(String[] permissions) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return; + } + Context context = uiHandler.getContext(); + permissions = Permissions.getPermissionsNeedToRequest(context, permissions); + if (permissions.length == 0) + return; + Permissions.requestPermissions(context, permissions); + } + + public void loadJar(String path) { + path = files.path(path); + try { + ((AndroidClassLoader) ContextFactory.getGlobal().getApplicationClassLoader()).loadJar(new File(path)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public void loadDex(String path) { + path = files.path(path); + try { + ((AndroidClassLoader) ContextFactory.getGlobal().getApplicationClassLoader()).loadDex(new File(path)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public void exit() { + mThread.interrupt(); + engines.myEngine().forceStop(); + threads.exit(); + if (Looper.myLooper() != Looper.getMainLooper()) { + throw new ScriptInterruptedException(); + } + } + + public void exit(Throwable e) { + engines.myEngine().uncaughtException(e); + exit(); + } + + @Deprecated + public void stop() { + exit(); + } + + + public void setScreenMetrics(int width, int height) { + mScreenMetrics.setScreenMetrics(width, height); + } + + public ScreenMetrics getScreenMetrics() { + return mScreenMetrics; + } + + public void ensureAccessibilityServiceEnabled() { + accessibilityBridge.ensureServiceEnabled(); + } + + public void onExit() { + Log.d(TAG, "on exit"); + //清除interrupt状态 + ThreadCompat.interrupted(); + //悬浮窗需要第一时间关闭以免出现恶意脚本全屏悬浮窗屏蔽屏幕并且在exit中写死循环的问题 + ignoresException(floaty::closeAll); + try { + events.emit("exit"); + } catch (Throwable e) { + console.error("exception on exit: ", e); + } + ignoresException(threads::shutDownAll); + ignoresException(events::recycle); + ignoresException(media::recycle); + ignoresException(loopers::recycle); + ignoresException(() -> { + if (mRootShell != null) mRootShell.exit(); + mRootShell = null; + mShellSupplier = null; + }); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + ignoresException(images::releaseScreenCapturer); + } + ignoresException(sensors::unregisterAll); + ignoresException(timers::recycle); + ignoresException(ui::recycle); + } + + private void ignoresException(Runnable r) { + try { + r.run(); + } catch (Throwable e) { + e.printStackTrace(); + } + } + + public Object getImages() { + return images; + } + + public Object getProperty(String key) { + return mProperties.get(key); + } + + public Object putProperty(String key, Object value) { + return mProperties.put(key, value); + } + + public Object removeProperty(String key) { + return mProperties.remove(key); + } + + public Continuation createContinuation() { + return Continuation.Companion.create(this, mTopLevelScope); + } + + public Continuation createContinuation(Scriptable scope) { + return Continuation.Companion.create(this, scope); + } + + + public static String getStackTrace(Throwable e, boolean printJavaStackTrace) { + String message = e.getMessage(); + StringBuilder scriptTrace = new StringBuilder(message == null ? "" : message + "\n"); + if (e instanceof RhinoException) { + RhinoException rhinoException = (RhinoException) e; + scriptTrace.append(rhinoException.details()).append("\n"); + for (ScriptStackElement element : rhinoException.getScriptStack()) { + element.renderV8Style(scriptTrace); + scriptTrace.append("\n"); + } + if (printJavaStackTrace) { + scriptTrace.append("- - - - - - - - - - -\n"); + } else { + return scriptTrace.toString(); + } + } + try { + StringWriter stringWriter = new StringWriter(); + PrintWriter writer = new PrintWriter(stringWriter); + e.printStackTrace(writer); + writer.close(); + BufferedReader bufferedReader = new BufferedReader(new StringReader(stringWriter.toString())); + String line; + while ((line = bufferedReader.readLine()) != null) { + scriptTrace.append("\n").append(line); + } + return scriptTrace.toString(); + } catch (IOException e1) { + e1.printStackTrace(); + return message; + } + } + +} diff --git a/app/src/main/java/com/stardust/autojs/runtime/accessibility/AccessibilityConfig.java b/app/src/main/java/com/stardust/autojs/runtime/accessibility/AccessibilityConfig.java new file mode 100644 index 00000000..24d67aad --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/runtime/accessibility/AccessibilityConfig.java @@ -0,0 +1,51 @@ +package com.stardust.autojs.runtime.accessibility; + +import android.util.ArraySet; + +import com.stardust.util.DeveloperUtils; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +/** + * Created by Stardust on 2017/4/29. + */ + +public class AccessibilityConfig { + + private static boolean isUnintendedGuardEnabled = false; + + private List mWhiteList = new ArrayList<>(); + private boolean mSealed = false; + + public AccessibilityConfig() { + if (isUnintendedGuardEnabled()) { + mWhiteList.add(DeveloperUtils.selfPackage()); + } + } + + public static boolean isUnintendedGuardEnabled() { + return isUnintendedGuardEnabled; + } + + public static void setIsUnintendedGuardEnabled(boolean isUnintendedGuardEnabled) { + AccessibilityConfig.isUnintendedGuardEnabled = isUnintendedGuardEnabled; + } + + public boolean whiteListContains(String packageName) { + return mWhiteList.contains(packageName); + } + + public void addWhiteList(String packageName) { + if (mSealed) + throw new IllegalStateException("sealed"); + mWhiteList.add(packageName); + } + + public final void seal() { + mSealed = true; + } +} diff --git a/app/src/main/java/com/stardust/autojs/runtime/api/AbstractConsole.java b/app/src/main/java/com/stardust/autojs/runtime/api/AbstractConsole.java new file mode 100644 index 00000000..7ef7bb9e --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/runtime/api/AbstractConsole.java @@ -0,0 +1,67 @@ +package com.stardust.autojs.runtime.api; + +import androidx.annotation.Nullable; +import android.util.Log; + +import com.stardust.autojs.runtime.exception.ScriptException; + +/** + * Created by Stardust on 2017/5/1. + */ + +public abstract class AbstractConsole implements Console { + + public void printf(int level, @Nullable Object data, Object... options) { + println(level, format(data, options)); + } + + public CharSequence format(@Nullable Object data, Object... options) { + if (data == null) + return "\n"; + if (options == null || options.length == 0) + return data.toString(); + return String.format(data.toString(), options); + } + + @Override + public void print(int level, Object data, Object... options) { + write(level, format(data, options)); + } + + protected abstract void write(int level, CharSequence data); + + @Override + public void verbose(@Nullable Object data, Object... options) { + printf(Log.VERBOSE, data, options); + } + + @Override + public void log(@Nullable Object data, Object... options) { + printf(Log.DEBUG, data, options); + } + + @Override + public void info(@Nullable Object data, Object... options) { + printf(Log.INFO, data, options); + } + + @Override + public void warn(@Nullable Object data, Object... options) { + printf(Log.WARN, data, options); + } + + @Override + public void error(@Nullable Object data, Object... options) { + printf(Log.ERROR, data, options); + } + + @Override + public void assertTrue(boolean value, @Nullable Object data, Object... options) { + if (!value) { + printf(Log.ASSERT, data, options); + throw new ScriptException(new AssertionError(format(data, options))); + } + } + + +} diff --git a/app/src/main/java/com/stardust/autojs/runtime/api/AbstractShell.java b/app/src/main/java/com/stardust/autojs/runtime/api/AbstractShell.java new file mode 100644 index 00000000..f0d17a5b --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/runtime/api/AbstractShell.java @@ -0,0 +1,208 @@ +package com.stardust.autojs.runtime.api; + +import android.content.Context; +import android.text.TextUtils; + +import com.stardust.autojs.engine.RootAutomatorEngine; +import com.stardust.util.ScreenMetrics; + +/** + * Created by Stardust on 2017/4/24. + */ + +public abstract class AbstractShell { + + public static class Result { + public int code = -1; + public String error; + public String result; + + @Override + public String toString() { + return "ShellResult{" + + "code=" + code + + ", error='" + error + '\'' + + ", result='" + result + '\'' + + '}'; + } + } + + protected static final String COMMAND_SU = "su"; + protected static final String COMMAND_SH = "sh"; + protected static final String COMMAND_EXIT = "exit\n"; + protected static final String COMMAND_LINE_END = "\n"; + + + private int mTouchDevice = -1; + private ScreenMetrics mScreenMetrics; + + private boolean mRoot; + protected Context mContext; + + public AbstractShell() { + this(false); + } + + public AbstractShell(boolean root) { + this(null, root); + } + + public AbstractShell(Context context, boolean root) { + mContext = context; + mRoot = root; + if (context != null) + mTouchDevice = RootAutomatorEngine.getTouchDevice(context); + init(root ? COMMAND_SU : COMMAND_SH); + } + + public boolean isRoot() { + return mRoot; + } + + protected abstract void init(String initialCommand); + + public abstract void exec(String command); + + public abstract void exit(); + + public void SetTouchDevice(int touchDevice) { + if (mTouchDevice > 0) + return; + mTouchDevice = touchDevice; + } + + public void SendEvent(int type, int code, int value) { + SendEvent(mTouchDevice, type, code, value); + } + + public void SendEvent(int device, int type, int code, int value) { + exec(TextUtils.join("", new Object[]{"sendevent /dev/input/event", device, " ", type, " ", code, " ", value})); + } + + public void SetScreenMetrics(int width, int height) { + if (mScreenMetrics == null) { + mScreenMetrics = new ScreenMetrics(); + } + mScreenMetrics.setScreenMetrics(width, height); + } + + public void SetScreenMetrics(ScreenMetrics screenMetrics) { + mScreenMetrics = screenMetrics; + } + + + public void Touch(int x, int y) { + TouchX(x); + TouchY(y); + } + + public void TouchX(int x) { + SendEvent(mTouchDevice, 3, 53, scaleX(x)); + } + + private int scaleX(int x) { + if (mScreenMetrics == null) + return x; + return mScreenMetrics.scaleX(x); + } + + public void TouchY(int y) { + SendEvent(mTouchDevice, 3, 54, scaleY(y)); + } + + private int scaleY(int y) { + if (mScreenMetrics == null) + return y; + return mScreenMetrics.scaleY(y); + } + + public void Tap(int x, int y) { + exec("input tap " + scaleX(x) + " " + scaleY(y)); + } + + public void Swipe(int x1, int y1, int x2, int y2) { + exec(com.stardust.util.TextUtils.join(" ", "input", "swipe", scaleX(x1), scaleY(y1), scaleX(x2), scaleY(y2))); + } + + public void Swipe(int x1, int y1, int x2, int y2, int time) { + exec(com.stardust.util.TextUtils.join(" ", "input", "swipe", scaleX(x1), scaleY(y1), scaleX(x2), scaleY(y2), time)); + } + + public void KeyCode(int keyCode) { + exec("input keyevent " + keyCode); + } + + public void KeyCode(String keyCode) { + exec("input keyevent " + keyCode); + } + + public void Home() { + KeyCode(3); + } + + public void Back() { + KeyCode(4); + } + + public void Power() { + KeyCode(26); + } + + public void Up() { + KeyCode(19); + } + + public void Down() { + KeyCode(20); + } + + public void Left() { + KeyCode(21); + } + + public void Right() { + KeyCode(22); + } + + public void OK() { + KeyCode(23); + } + + public void VolumeUp() { + KeyCode(24); + } + + public void VolumeDown() { + KeyCode(25); + } + + public void Menu() { + KeyCode(1); + } + + public void Camera() { + KeyCode(27); + } + + public void Input(String text) { + exec("input text " + text); + } + + public void Screencap(String path) { + exec("screencap -p " + path); + } + + public void Text(String text) { + Input(text); + } + + public abstract void exitAndWaitFor(); + + public void sleep(long i) { + exec("sleep " + i); + } + + public void usleep(long l) { + exec("usleep " + l); + } +} diff --git a/app/src/main/java/com/stardust/autojs/runtime/api/AppUtils.java b/app/src/main/java/com/stardust/autojs/runtime/api/AppUtils.java new file mode 100644 index 00000000..b8c82982 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/runtime/api/AppUtils.java @@ -0,0 +1,140 @@ +package com.stardust.autojs.runtime.api; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.net.Uri; +import androidx.annotation.Nullable; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import android.util.Log; + +import com.stardust.autojs.annotation.ScriptInterface; +import com.stardust.util.IntentUtil; + +import java.lang.ref.WeakReference; +import java.util.List; + +/** + * Created by Stardust on 2017/4/2. + */ + +public class AppUtils { + + private Context mContext; + private volatile WeakReference mCurrentActivity = new WeakReference<>(null); + private final String mFileProviderAuthority; + + public AppUtils(Context context) { + mContext = context; + mFileProviderAuthority = null; + } + + public AppUtils(Context context, String fileProviderAuthority) { + mContext = context; + mFileProviderAuthority = fileProviderAuthority; + } + + @ScriptInterface + public boolean launchPackage(String packageName) { + try { + PackageManager packageManager = mContext.getPackageManager(); + mContext.startActivity(packageManager.getLaunchIntentForPackage(packageName) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + return true; + } catch (Exception e) { + return false; + } + + } + + @ScriptInterface + public void sendLocalBroadcastSync(Intent intent) { + LocalBroadcastManager.getInstance(mContext).sendBroadcastSync(intent); + } + + @ScriptInterface + public boolean launchApp(String appName) { + String pkg = getPackageName(appName); + if (pkg == null) + return false; + return launchPackage(pkg); + } + + @ScriptInterface + public String getPackageName(String appName) { + PackageManager packageManager = mContext.getPackageManager(); + List installedApplications = packageManager.getInstalledApplications(PackageManager.GET_META_DATA); + for (ApplicationInfo applicationInfo : installedApplications) { + if (packageManager.getApplicationLabel(applicationInfo).toString().equals(appName)) { + return applicationInfo.packageName; + } + } + return null; + } + + @ScriptInterface + public String getAppName(String packageName) { + PackageManager packageManager = mContext.getPackageManager(); + try { + ApplicationInfo applicationInfo = packageManager.getApplicationInfo(packageName, 0); + CharSequence appName = packageManager.getApplicationLabel(applicationInfo); + return appName == null ? null : appName.toString(); + } catch (PackageManager.NameNotFoundException e) { + return null; + } + } + + @ScriptInterface + public boolean openAppSetting(String packageName) { + return IntentUtil.goToAppDetailSettings(mContext, packageName); + } + + @ScriptInterface + public String getFileProviderAuthority() { + return mFileProviderAuthority; + } + + @Nullable + public Activity getCurrentActivity() { + Log.d("App", "getCurrentActivity: " + mCurrentActivity.get()); + return mCurrentActivity.get(); + } + + @ScriptInterface + public void uninstall(String packageName) { + mContext.startActivity(new Intent(Intent.ACTION_DELETE, Uri.parse("package:" + packageName)) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + } + + @ScriptInterface + public void viewFile(String path) { + if (path == null) + throw new NullPointerException("path == null"); + IntentUtil.viewFile(mContext, path, mFileProviderAuthority); + } + + @ScriptInterface + public void editFile(String path) { + if (path == null) + throw new NullPointerException("path == null"); + IntentUtil.editFile(mContext, path, mFileProviderAuthority); + } + + @ScriptInterface + public void openUrl(String url) { + if (!url.startsWith("http://") && !url.startsWith("https://")) { + url = "http://" + url; + } + mContext.startActivity(new Intent(Intent.ACTION_VIEW) + .setData(Uri.parse(url)) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + } + + public void setCurrentActivity(Activity currentActivity) { + mCurrentActivity = new WeakReference<>(currentActivity); + Log.d("App", "setCurrentActivity: " + currentActivity); + } +} diff --git a/app/src/main/java/com/stardust/autojs/runtime/api/Console.java b/app/src/main/java/com/stardust/autojs/runtime/api/Console.java new file mode 100644 index 00000000..bce8a902 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/runtime/api/Console.java @@ -0,0 +1,47 @@ +package com.stardust.autojs.runtime.api; + +import androidx.annotation.Nullable; + +import com.stardust.autojs.annotation.ScriptInterface; + +/** + * Created by Stardust on 2017/4/2. + */ + +public interface Console { + + @ScriptInterface + void verbose(@Nullable Object data, Object... options); + + @ScriptInterface + void log(@Nullable Object data, Object... options); + + @ScriptInterface + void print(int level, Object data, Object... options); + + @ScriptInterface + void info(@Nullable Object data, Object... options); + + @ScriptInterface + void warn(@Nullable Object data, Object... options); + + @ScriptInterface + void error(@Nullable Object data, Object... options); + + @ScriptInterface + void assertTrue(boolean value, @Nullable Object data, Object... options); + + @ScriptInterface + void clear(); + + @ScriptInterface + void show(); + + @ScriptInterface + void hide(); + + String println(int level, CharSequence charSequence); + + void setTitle(CharSequence title); + +} diff --git a/app/src/main/java/com/stardust/autojs/runtime/api/Device.java b/app/src/main/java/com/stardust/autojs/runtime/api/Device.java new file mode 100644 index 00000000..3e169844 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/runtime/api/Device.java @@ -0,0 +1,394 @@ +package com.stardust.autojs.runtime.api; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.app.ActivityManager; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.media.AudioManager; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.os.BatteryManager; +import android.os.Build; +import android.os.PowerManager; +import android.os.Vibrator; +import android.provider.Settings; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import android.telephony.TelephonyManager; + +import org.autojs.autojs6.R; +import com.stardust.autojs.runtime.exception.ScriptException; +import com.stardust.pio.PFiles; +import com.stardust.pio.UncheckedIOException; +import com.stardust.util.ScreenMetrics; + +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.Collections; +import java.util.List; + +import ezy.assist.compat.SettingsCompat; + +/** + * Created by Stardust on 2017/12/2. + */ + +public class Device { + + public static final int width = ScreenMetrics.getDeviceScreenWidth(); + + public static final int height = ScreenMetrics.getDeviceScreenHeight(); + + public static final String buildId = Build.ID; + + public static final String buildDisplay = Build.DISPLAY; + + public static final String product = Build.PRODUCT; + + public static final String board = Build.BOARD; + + public static final String brand = Build.BRAND; + + public static final String device = Build.DEVICE; + + public static final String model = Build.MODEL; + + public static final String bootloader = Build.BOOTLOADER; + + public static final String hardware = Build.HARDWARE; + + public static final String fingerprint = Build.FINGERPRINT; + + public static final int sdkInt = Build.VERSION.SDK_INT; + + public static final String incremental = Build.VERSION.INCREMENTAL; + + public static final String release = Build.VERSION.RELEASE; + + public static final String baseOS; + + public static final String securityPatch; + + static { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + baseOS = Build.VERSION.BASE_OS; + securityPatch = Build.VERSION.SECURITY_PATCH; + } else { + baseOS = null; + securityPatch = null; + } + } + + public static final String codename = Build.VERSION.CODENAME; + + @SuppressLint("HardwareIds") + public static final String serial = Build.SERIAL; + + private Context mContext; + private PowerManager.WakeLock mWakeLock; + private int mWakeLockFlag; + + public Device(Context context) { + mContext = context; + } + + @SuppressLint("HardwareIds") + @Nullable + public String getIMEI() { + checkReadPhoneStatePermission(); + try { + return ((TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE)).getDeviceId(); + } catch (SecurityException e) { + return null; + } + } + + + @SuppressLint("HardwareIds") + public String getAndroidId() { + return Settings.Secure.getString(mContext.getContentResolver(), Settings.Secure.ANDROID_ID); + } + + public int getBrightness() throws Settings.SettingNotFoundException { + return Settings.System.getInt(mContext.getContentResolver(), Settings.System.SCREEN_BRIGHTNESS); + } + + public int getBrightnessMode() throws Settings.SettingNotFoundException { + return Settings.System.getInt(mContext.getContentResolver(), Settings.System.SCREEN_BRIGHTNESS_MODE); + } + + public void setBrightness(int b) throws Settings.SettingNotFoundException { + checkWriteSettingsPermission(); + Settings.System.putInt(mContext.getContentResolver(), Settings.System.SCREEN_BRIGHTNESS, b); + } + + public void setBrightnessMode(int b) throws Settings.SettingNotFoundException { + checkWriteSettingsPermission(); + Settings.System.putInt(mContext.getContentResolver(), Settings.System.SCREEN_BRIGHTNESS_MODE, b); + } + + public int getMusicVolume() { + return ((AudioManager) getSystemService(Context.AUDIO_SERVICE)) + .getStreamVolume(AudioManager.STREAM_MUSIC); + } + + public int getNotificationVolume() { + return ((AudioManager) getSystemService(Context.AUDIO_SERVICE)) + .getStreamVolume(AudioManager.STREAM_NOTIFICATION); + } + + public int getAlarmVolume() { + return ((AudioManager) getSystemService(Context.AUDIO_SERVICE)) + .getStreamVolume(AudioManager.STREAM_ALARM); + } + + public int getMusicMaxVolume() { + return ((AudioManager) getSystemService(Context.AUDIO_SERVICE)) + .getStreamMaxVolume(AudioManager.STREAM_MUSIC); + } + + public int getNotificationMaxVolume() { + return ((AudioManager) getSystemService(Context.AUDIO_SERVICE)) + .getStreamMaxVolume(AudioManager.STREAM_NOTIFICATION); + } + + public int getAlarmMaxVolume() { + return ((AudioManager) getSystemService(Context.AUDIO_SERVICE)) + .getStreamMaxVolume(AudioManager.STREAM_ALARM); + } + + public void setMusicVolume(int i) { + checkWriteSettingsPermission(); + ((AudioManager) getSystemService(Context.AUDIO_SERVICE)) + .setStreamVolume(AudioManager.STREAM_MUSIC, i, 0); + } + + public void setAlarmVolume(int i) { + checkWriteSettingsPermission(); + ((AudioManager) getSystemService(Context.AUDIO_SERVICE)) + .setStreamVolume(AudioManager.STREAM_ALARM, i, 0); + } + + public void setNotificationVolume(int i) { + checkWriteSettingsPermission(); + ((AudioManager) getSystemService(Context.AUDIO_SERVICE)) + .setStreamVolume(AudioManager.STREAM_NOTIFICATION, i, 0); + } + + public float getBattery() { + Intent batteryIntent = mContext.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); + if (batteryIntent == null) { + return -1; + } + int level = batteryIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); + int scale = batteryIntent.getIntExtra(BatteryManager.EXTRA_SCALE, -1); + float battery = ((float) level / scale) * 100.0f; + return Math.round(battery * 10) / 10; + } + + public long getTotalMem() { + ActivityManager activityManager = getSystemService(Context.ACTIVITY_SERVICE); + ActivityManager.MemoryInfo info = new ActivityManager.MemoryInfo(); + activityManager.getMemoryInfo(info); + return info.totalMem; + } + + public long getAvailMem() { + ActivityManager activityManager = getSystemService(Context.ACTIVITY_SERVICE); + ActivityManager.MemoryInfo info = new ActivityManager.MemoryInfo(); + activityManager.getMemoryInfo(info); + return info.availMem; + } + + public boolean isCharging() { + Intent intent = mContext.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); + if (intent == null) { + throw new ScriptException("Cannot retrieve the battery state"); + } + int plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1); + return plugged == BatteryManager.BATTERY_PLUGGED_AC || plugged == BatteryManager.BATTERY_PLUGGED_USB; + } + + public void keepAwake(int flags, long timeout) { + checkWakeLock(flags); + mWakeLock.acquire(timeout); + } + + @SuppressLint("WakelockTimeout") + public void keepAwake(int flags) { + checkWakeLock(flags); + mWakeLock.acquire(); + } + + public boolean isScreenOn() { + //按照API文档来说不应该使用PowerManager.isScreenOn(),但是,isScreenOn()和实际不一致的情况通常只会出现在安卓智能手表的类似设备上 + //因此这里仍然使用PowerManager.isScreenOn() + // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { + // return ((WindowManager) getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getState() == Display.STATE_ON; + //} else { + return ((PowerManager) getSystemService(Context.POWER_SERVICE)).isScreenOn(); + //} + } + + public void wakeUpIfNeeded() { + if (!isScreenOn()) { + wakeUp(); + } + } + + public void wakeUp() { + keepScreenOn(200); + } + + public void keepScreenOn() { + keepAwake(PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP); + } + + public void keepScreenOn(long timeout) { + keepAwake(PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP, timeout); + } + + public void keepScreenDim() { + keepAwake(PowerManager.SCREEN_DIM_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP); + } + + public void keepScreenDim(long timeout) { + keepAwake(PowerManager.SCREEN_DIM_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP, timeout); + } + + private void checkWakeLock(int flags) { + if (mWakeLock == null || flags != mWakeLockFlag) { + cancelKeepingAwake(); + mWakeLock = ((PowerManager) getSystemService(Context.POWER_SERVICE)).newWakeLock(flags, Device.class.getName()); + } + } + + public void cancelKeepingAwake() { + if (mWakeLock != null && mWakeLock.isHeld()) + mWakeLock.release(); + } + + public void vibrate(long millis) { + ((Vibrator) getSystemService(Context.VIBRATOR_SERVICE)).vibrate(millis); + } + + public void cancelVibration() { + ((Vibrator) getSystemService(Context.VIBRATOR_SERVICE)).cancel(); + } + + + private void checkWriteSettingsPermission() { + if (SettingsCompat.canWriteSettings(mContext)) { + return; + } + SettingsCompat.manageWriteSettings(mContext); + throw new SecurityException(mContext.getString(R.string.error_no_write_settings_permission)); + } + + + private void checkReadPhoneStatePermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (mContext.checkSelfPermission(Manifest.permission.READ_PHONE_STATE) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException(mContext.getString(R.string.error_no_read_phone_state_permission)); + } + } + } + + + // just to avoid warning of null pointer to make android studio happy.. + @NonNull + @SuppressWarnings("unchecked") + private T getSystemService(String service) { + Object systemService = mContext.getSystemService(service); + if (systemService == null) { + throw new RuntimeException("should never happen..." + service); + } + return (T) systemService; + } + + private static final String FAKE_MAC_ADDRESS = "02:00:00:00:00:00"; + + @SuppressLint("HardwareIds") + public String getMacAddress() throws Exception { + WifiManager wifiMan = (WifiManager) mContext.getApplicationContext().getSystemService(Context.WIFI_SERVICE); + if (wifiMan == null) { + return null; + } + WifiInfo wifiInf = wifiMan.getConnectionInfo(); + if (wifiInf == null) { + return getMacByFile(); + } + + String mac = wifiInf.getMacAddress(); + if (FAKE_MAC_ADDRESS.equals(mac)) { + mac = null; + } + if (mac == null) { + mac = getMacByInterface(); + if (mac == null) { + mac = getMacByFile(); + } + } + return mac; + } + + private static String getMacByInterface() throws SocketException { + List networkInterfaces = Collections.list(NetworkInterface.getNetworkInterfaces()); + for (NetworkInterface networkInterface : networkInterfaces) { + if (networkInterface.getName().equalsIgnoreCase("wlan0")) { + byte[] macBytes = networkInterface.getHardwareAddress(); + if (macBytes == null) { + return null; + } + + StringBuilder mac = new StringBuilder(); + for (byte b : macBytes) { + mac.append(String.format("%02X:", b)); + } + + if (mac.length() > 0) { + mac.deleteCharAt(mac.length() - 1); + } + return mac.toString(); + } + } + return null; + } + + private static String getMacByFile() throws Exception { + try { + return PFiles.read("/sys/class/net/wlan0/address"); + } catch (UncheckedIOException e) { + return null; + } + } + + @Override + public String toString() { + return "Device{" + + "width=" + width + + ", height=" + height + + ", buildId='" + buildId + '\'' + + ", buildDisplay='" + buildDisplay + '\'' + + ", product='" + product + '\'' + + ", board='" + board + '\'' + + ", brand='" + brand + '\'' + + ", device='" + device + '\'' + + ", model='" + model + '\'' + + ", bootloader='" + bootloader + '\'' + + ", hardware='" + hardware + '\'' + + ", fingerprint='" + fingerprint + '\'' + + ", sdkInt=" + sdkInt + + ", incremental='" + incremental + '\'' + + ", release='" + release + '\'' + + ", baseOS='" + baseOS + '\'' + + ", securityPatch='" + securityPatch + '\'' + + ", serial='" + serial + '\'' + + '}'; + } + +} diff --git a/app/src/main/java/com/stardust/autojs/runtime/api/Dialogs.java b/app/src/main/java/com/stardust/autojs/runtime/api/Dialogs.java new file mode 100644 index 00000000..3521c8e9 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/runtime/api/Dialogs.java @@ -0,0 +1,181 @@ +package com.stardust.autojs.runtime.api; + +import android.app.Activity; +import android.content.Context; +import android.text.TextUtils; +import android.view.ContextThemeWrapper; +import android.widget.CompoundButton; + +import com.afollestad.materialdialogs.MaterialDialog; +import com.afollestad.materialdialogs.Theme; +import org.autojs.autojs6.R; +import com.stardust.autojs.annotation.ScriptInterface; +import com.stardust.autojs.annotation.ScriptVariable; +import com.stardust.autojs.core.ui.dialog.BlockedMaterialDialog; +import com.stardust.autojs.core.ui.dialog.JsDialogBuilder; +import com.stardust.autojs.runtime.ScriptBridges; +import com.stardust.autojs.runtime.ScriptRuntime; +import com.stardust.util.ArrayUtils; +import com.stardust.util.UiHandler; + +/** + * Created by Stardust on 2017/5/8. + */ + +public class Dialogs { + + private ContextThemeWrapper mThemeWrapper; + private final ScriptRuntime mRuntime; + + @ScriptVariable + public final NonUiDialogs nonUiDialogs = new NonUiDialogs(); + + public Dialogs(ScriptRuntime runtime) { + mRuntime = runtime; + } + + @ScriptInterface + public Object rawInput(String title, String prefill, Object callback) { + return ((BlockedMaterialDialog.Builder) dialogBuilder(callback) + .input(null, prefill, true) + .title(title)) + .showAndGet(); + } + + @ScriptInterface + public Object alert(String title, String content, Object callback) { + MaterialDialog.Builder builder = dialogBuilder(callback) + .alert() + .title(title) + .positiveText(R.string.text_ok); + if (!TextUtils.isEmpty(content)) { + builder.content(content); + } + return ((BlockedMaterialDialog.Builder) builder).showAndGet(); + } + + @ScriptInterface + public Object confirm(String title, String content, Object callback) { + MaterialDialog.Builder builder = dialogBuilder(callback) + .confirm() + .title(title) + .positiveText(R.string.text_ok) + .negativeText(R.string.text_cancel); + if (!TextUtils.isEmpty(content)) { + builder.content(content); + } + return ((BlockedMaterialDialog.Builder) builder).showAndGet(); +} + + private Context getContext() { + if (mThemeWrapper != null) + return mThemeWrapper; + mThemeWrapper = new ContextThemeWrapper(mRuntime.uiHandler.getContext().getApplicationContext(), androidx.appcompat.R.style.Theme_AppCompat_Light); + return mThemeWrapper; + } + + @ScriptInterface + public Object select(String title, String[] items, Object callback) { + return ((BlockedMaterialDialog.Builder) dialogBuilder(callback) + .itemsCallback() + .title(title) + .items((CharSequence[]) items)) + .showAndGet(); + } + + private String[] getItems(Object[] args) { + int len = 0; + if (args.length > 1) { + if (args[args.length - 1] instanceof CharSequence) { + len = args.length; + } else { + len = args.length - 1; + } + } + String[] items = new String[len]; + for (int i = 0; i < len; i++) { + items[i] = args[i] == null ? null : args[i].toString(); + } + return items; + } + + @ScriptInterface + public Object singleChoice(String title, int selectedIndex, String[] items, Object callback) { + return ((BlockedMaterialDialog.Builder) dialogBuilder(callback) + .itemsCallbackSingleChoice(selectedIndex) + .title(title) + .positiveText(R.string.text_ok) + .items((CharSequence[]) items)) + .showAndGet(); + } + + @ScriptInterface + public Object multiChoice(String title, int[] indices, String[] items, Object callback) { + return ((BlockedMaterialDialog.Builder) dialogBuilder(callback) + .itemsCallbackMultiChoice(ArrayUtils.box(indices)) + .title(title) + .positiveText(R.string.text_ok) + .items((CharSequence[]) items)) + .showAndGet(); + } + + @ScriptInterface + public Object selectFile(String title, String prefill, Object callback) { + return ((BlockedMaterialDialog.Builder) dialogBuilder(callback) + .input(null, prefill, true) + .title(title)) + .showAndGet(); + } + + private BlockedMaterialDialog.Builder dialogBuilder(Object callback) { + Context context = mRuntime.app.getCurrentActivity(); + if (context == null || ((Activity) context).isFinishing()) { + context = getContext(); + } + return (BlockedMaterialDialog.Builder) new BlockedMaterialDialog.Builder(context, mRuntime, callback) + .theme(Theme.LIGHT); + } + + @ScriptInterface + public MaterialDialog.Builder newBuilder() { + Context context = mRuntime.app.getCurrentActivity(); + if (context == null || ((Activity) context).isFinishing()) { + context = getContext(); + } + return new JsDialogBuilder(context, mRuntime) + .theme(Theme.LIGHT); + } + + public class NonUiDialogs { + + public String rawInput(String title, String prefill, Object callback) { + return (String) Dialogs.this.rawInput(title, prefill, callback); + } + + @ScriptInterface + public boolean confirm(String title, String content, Object callback) { + return (boolean) Dialogs.this.confirm(title, content, callback); + } + + @ScriptInterface + public int select(String title, String[] items, Object callback) { + return (Integer) Dialogs.this.select(title, items, callback); + } + + @ScriptInterface + public int singleChoice(String title, int selectedIndex, String[] items, Object callback) { + return (int) Dialogs.this.singleChoice(title, selectedIndex, items, callback); + } + + @ScriptInterface + public int[] multiChoice(String title, int[] indices, String[] items, Object callback) { + return (int[]) Dialogs.this.multiChoice(title, indices, items, callback); + } + + @ScriptInterface + public Object alert(String title, String content, Object callback) { + return Dialogs.this.alert(title, content, callback); + } + + } +} diff --git a/app/src/main/java/com/stardust/autojs/runtime/api/Engines.java b/app/src/main/java/com/stardust/autojs/runtime/api/Engines.java new file mode 100644 index 00000000..78fbaeab --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/runtime/api/Engines.java @@ -0,0 +1,61 @@ +package com.stardust.autojs.runtime.api; + +import com.stardust.autojs.ScriptEngineService; +import com.stardust.autojs.engine.JavaScriptEngine; +import com.stardust.autojs.execution.ExecutionConfig; +import com.stardust.autojs.execution.ScriptExecution; +import com.stardust.autojs.runtime.ScriptRuntime; +import com.stardust.autojs.script.AutoFileSource; +import com.stardust.autojs.script.JavaScriptFileSource; +import com.stardust.autojs.script.StringScriptSource; + +/** + * Created by Stardust on 2017/8/4. + */ + +public class Engines { + + private ScriptEngineService mEngineService; + private JavaScriptEngine mScriptEngine; + private ScriptRuntime mScriptRuntime; + + public Engines(ScriptEngineService engineService, ScriptRuntime scriptRuntime) { + mEngineService = engineService; + mScriptRuntime = scriptRuntime; + } + + public ScriptExecution execScript(String name, String script, ExecutionConfig config) { + return mEngineService.execute(new StringScriptSource(name, script), config); + } + + public ScriptExecution execScriptFile(String path, ExecutionConfig config) { + return mEngineService.execute(new JavaScriptFileSource(mScriptRuntime.files.path(path)), config); + } + + public ScriptExecution execAutoFile(String path, ExecutionConfig config) { + return mEngineService.execute(new AutoFileSource(mScriptRuntime.files.path(path)), config); + } + + public Object all() { + return mScriptRuntime.bridges.toArray(mEngineService.getEngines()); + } + + public int stopAll() { + return mEngineService.stopAll(); + } + + public void stopAllAndToast() { + mEngineService.stopAllAndToast(); + } + + + public void setCurrentEngine(JavaScriptEngine engine) { + if (mScriptEngine != null) + throw new IllegalStateException(); + mScriptEngine = engine; + } + + public JavaScriptEngine myEngine() { + return mScriptEngine; + } +} diff --git a/app/src/main/java/com/stardust/autojs/runtime/api/Events.java b/app/src/main/java/com/stardust/autojs/runtime/api/Events.java new file mode 100644 index 00000000..4f6e3b83 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/runtime/api/Events.java @@ -0,0 +1,353 @@ +package com.stardust.autojs.runtime.api; + +import android.accessibilityservice.AccessibilityServiceInfo; +import android.content.Context; +import android.content.Intent; +import android.graphics.Point; +import android.os.Build; +import android.os.Handler; +import android.provider.Settings; + +import androidx.annotation.RequiresApi; + +import android.view.KeyEvent; + +import org.autojs.autojs6.R; +import com.stardust.autojs.core.accessibility.AccessibilityBridge; +import com.stardust.autojs.core.boardcast.BroadcastEmitter; +import com.stardust.autojs.core.eventloop.EventEmitter; +import com.stardust.autojs.core.looper.Loopers; +import com.stardust.autojs.core.looper.MainThreadProxy; +import com.stardust.autojs.core.looper.Timer; +import com.stardust.autojs.runtime.ScriptRuntime; +import com.stardust.notification.Notification; +import com.stardust.notification.NotificationListenerService; +import com.stardust.autojs.runtime.exception.ScriptException; +import com.stardust.autojs.core.inputevent.InputEventObserver; +import com.stardust.autojs.core.inputevent.TouchObserver; +import com.stardust.util.MapBuilder; +import com.stardust.view.accessibility.AccessibilityNotificationObserver; +import com.stardust.view.accessibility.AccessibilityService; +import com.stardust.view.accessibility.KeyInterceptor; +import com.stardust.view.accessibility.NotificationListener; +import com.stardust.view.accessibility.OnKeyListener; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Created by Stardust on 2017/7/18. + */ + +public class Events extends EventEmitter implements OnKeyListener, TouchObserver.OnTouchEventListener, NotificationListener, AccessibilityNotificationObserver.ToastListener, AccessibilityService.GestureListener { + + private static final String PREFIX_KEY_DOWN = "__key_down__#"; + private static final String PREFIX_KEY_UP = "__key_up__#"; + private static final Map GESTURES = new MapBuilder() + .put(AccessibilityService.GESTURE_SWIPE_UP, "up") + .put(AccessibilityService.GESTURE_SWIPE_DOWN, "down") + .put(AccessibilityService.GESTURE_SWIPE_LEFT, "left") + .put(AccessibilityService.GESTURE_SWIPE_RIGHT, "right") + .put(AccessibilityService.GESTURE_SWIPE_LEFT_AND_RIGHT, "left_right") + .put(AccessibilityService.GESTURE_SWIPE_RIGHT_AND_LEFT, "right_left") + .put(AccessibilityService.GESTURE_SWIPE_UP_AND_DOWN, "up_down") + .put(AccessibilityService.GESTURE_SWIPE_DOWN_AND_UP, "down_up") + .put(AccessibilityService.GESTURE_SWIPE_LEFT_AND_UP, "left_up") + .put(AccessibilityService.GESTURE_SWIPE_LEFT_AND_DOWN, "left_down") + .put(AccessibilityService.GESTURE_SWIPE_RIGHT_AND_UP, "right_up") + .put(AccessibilityService.GESTURE_SWIPE_RIGHT_AND_DOWN, "right_down") + .put(AccessibilityService.GESTURE_SWIPE_UP_AND_LEFT, "up_left") + .put(AccessibilityService.GESTURE_SWIPE_UP_AND_RIGHT, "up_right") + .put(AccessibilityService.GESTURE_SWIPE_DOWN_AND_LEFT, "down_left") + .put(AccessibilityService.GESTURE_SWIPE_DOWN_AND_RIGHT, "down_right") + .build(); + + private AccessibilityBridge mAccessibilityBridge; + private Context mContext; + private TouchObserver mTouchObserver; + private long mLastTouchEventMillis; + private long mTouchEventTimeout = 10; + private boolean mListeningKey = false; + private Loopers mLoopers; + private Handler mHandler; + private boolean mListeningNotification = false; + private boolean mListeningGesture = false; + private boolean mListeningToast = false; + private ScriptRuntime mScriptRuntime; + private volatile boolean mInterceptsAllKey = false; + private KeyInterceptor mKeyInterceptor; + private Set mInterceptedKeys = new HashSet<>(); + + public final BroadcastEmitter broadcast; + + public Events(Context context, AccessibilityBridge accessibilityBridge, ScriptRuntime runtime) { + super(runtime.bridges); + mAccessibilityBridge = accessibilityBridge; + mContext = context; + mLoopers = runtime.loopers; + mScriptRuntime = runtime; + broadcast = new BroadcastEmitter(runtime.bridges, runtime.timers.getMainTimer()); + } + + public EventEmitter emitter() { + return new EventEmitter(mBridges); + } + + public EventEmitter emitter(Thread thread) { + Timer timer = mScriptRuntime.timers.getTimerForThread(thread); + return new EventEmitter(mBridges, timer); + } + + public EventEmitter emitter(MainThreadProxy mainThreadProxy) { + return new EventEmitter(mBridges, mScriptRuntime.timers.getMainTimer()); + } + + public void observeKey() { + if (mListeningKey) + return; + AccessibilityService service = getAccessibilityService(); + if ((service.getServiceInfo().flags & AccessibilityServiceInfo.FLAG_REQUEST_FILTER_KEY_EVENTS) == 0) { + throw new ScriptException("should_enable_key_observing"); + } + ensureHandler(); + mLoopers.waitWhenIdle(true); + mListeningKey = true; + mAccessibilityBridge.ensureServiceEnabled(); + service.getOnKeyObserver().addListener(this); + } + + private void ensureHandler() { + if (mHandler == null) { + mHandler = new Handler(); + } + } + + public void observeTouch() { + if (mTouchObserver != null) + return; + ensureHandler(); + mLoopers.waitWhenIdle(true); + mTouchObserver = new TouchObserver(InputEventObserver.getGlobal(mContext)); + mTouchObserver.setOnTouchEventListener(this); + mTouchObserver.observe(); + } + + public void setKeyInterceptionEnabled(boolean enabled) { + mInterceptsAllKey = enabled; + if (mInterceptsAllKey) { + ensureKeyInterceptor(); + } + } + + public void setKeyInterceptionEnabled(String key, boolean enabled) { + if (enabled) { + mInterceptedKeys.add(key); + ensureKeyInterceptor(); + } else { + mInterceptedKeys.remove(key); + } + } + + private void ensureKeyInterceptor() { + if (mKeyInterceptor != null) + return; + mKeyInterceptor = event -> { + if (mInterceptsAllKey) + return true; + String keyName = KeyEvent.keyCodeToString(event.getKeyCode()).substring(8).toLowerCase(); + return mInterceptedKeys.contains(keyName); + }; + getAccessibilityService().getKeyInterrupterObserver().addKeyInterrupter(mKeyInterceptor); + } + + private AccessibilityService getAccessibilityService() { + mScriptRuntime.ensureAccessibilityServiceEnabled(); + AccessibilityService service = mAccessibilityBridge.getService(); + if (service == null) + throw new ScriptException("AccessibilityService = null"); + return service; + } + + + public Events onKeyDown(String keyName, Object listener) { + on(PREFIX_KEY_DOWN + keyName, listener); + return this; + } + + public Events onceKeyDown(String keyName, Object listener) { + once(PREFIX_KEY_DOWN + keyName, listener); + return this; + } + + public Events removeAllKeyDownListeners(String keyName) { + removeAllListeners(PREFIX_KEY_DOWN + keyName); + return this; + } + + public Events onKeyUp(String keyName, Object listener) { + on(PREFIX_KEY_UP + keyName, listener); + return this; + } + + public Events onceKeyUp(String keyName, Object listener) { + once(PREFIX_KEY_UP + keyName, listener); + return this; + } + + public Events removeAllKeyUpListeners(String keyName) { + removeAllListeners(PREFIX_KEY_UP + keyName); + return this; + } + + public Events onTouch(Object listener) { + on("touch", listener); + return this; + } + + public Events removeAllTouchListeners() { + removeAllListeners("touch"); + return this; + } + + public long getTouchEventTimeout() { + return mTouchEventTimeout; + } + + public void setTouchEventTimeout(long touchEventTimeout) { + mTouchEventTimeout = touchEventTimeout; + } + + @RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2) + public void observeNotification() { + ScriptRuntime.requiresApi(18); + if (mListeningNotification) + return; + mListeningNotification = true; + ensureHandler(); + mLoopers.waitWhenIdle(true); + if (NotificationListenerService.Companion.getInstance() == null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { + mContext.startActivity(new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS)); + } + throw new ScriptException(mContext.getString(R.string.text_notification_service_disabled)); + } + NotificationListenerService.Companion.getInstance().addListener(this); + } + + public void observeToast() { + if (mListeningToast) + return; + mAccessibilityBridge.ensureServiceEnabled(); + mListeningToast = true; + ensureHandler(); + mLoopers.waitWhenIdle(true); + mAccessibilityBridge.getNotificationObserver().addToastListener(this); + } + + public void observeGesture() { + ScriptRuntime.requiresApi(Build.VERSION_CODES.O); + if (mListeningGesture) { + return; + } + AccessibilityService service = getAccessibilityService(); + if ((service.getServiceInfo().flags & AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE) == 0) { + throw new ScriptException("should_enable_gesture_observing"); + } + service.getGestureEventDispatcher().addListener(this); + ensureHandler(); + mLoopers.waitWhenIdle(true); + mListeningGesture = true; + } + + public Events onNotification(Object listener) { + on("notification", listener); + return this; + } + + public Events onToast(Object listener) { + on("toast", listener); + return this; + } + + + public void recycle() { + broadcast.unregister(); + if (mListeningKey) { + AccessibilityService service = mAccessibilityBridge.getService(); + if (service != null) { + service.getOnKeyObserver().removeListener(this); + mListeningKey = false; + } + } + if (mTouchObserver != null) { + mTouchObserver.stop(); + } + if (mListeningNotification) { + mAccessibilityBridge.getNotificationObserver().removeNotificationListener(this); + mAccessibilityBridge.getNotificationObserver().removeToastListener(this); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 + && NotificationListenerService.Companion.getInstance() != null) { + NotificationListenerService.Companion.getInstance().removeListener(this); + } + } + if (mKeyInterceptor != null) { + AccessibilityService service = mAccessibilityBridge.getService(); + if (service != null) { + service.getKeyInterrupterObserver().removeKeyInterrupter(mKeyInterceptor); + } + mKeyInterceptor = null; + } + if (mListeningGesture) { + AccessibilityService service = mAccessibilityBridge.getService(); + if (service != null) { + service.getGestureEventDispatcher().removeListener(this); + } + } + } + + @Override + public void onKeyEvent(final int keyCode, final KeyEvent event) { + mHandler.post(() -> { + String keyName = KeyEvent.keyCodeToString(keyCode).substring(8).toLowerCase(); + emit(keyName, event); + if (event.getAction() == KeyEvent.ACTION_DOWN) { + emit(PREFIX_KEY_DOWN + keyName, event); + emit("key_down", keyCode, event); + } else if (event.getAction() == KeyEvent.ACTION_UP) { + emit(PREFIX_KEY_UP + keyName, event); + emit("key_up", keyCode, event); + } + emit("key", keyCode, event); + }); + } + + @Override + public void onTouch(final int x, final int y) { + if (System.currentTimeMillis() - mLastTouchEventMillis < mTouchEventTimeout) { + return; + } + mLastTouchEventMillis = System.currentTimeMillis(); + mHandler.post(() -> emit("touch", new Point(x, y))); + } + + public void onNotification(final Notification notification) { + mHandler.post(() -> emit("notification", notification)); + + } + + @Override + public void onToast(final AccessibilityNotificationObserver.Toast toast) { + mHandler.post(() -> emit("toast", toast)); + } + + @Override + public void onGesture(int gestureId) { + mHandler.post(() -> { + String gesture = GESTURES.get(gestureId); + if (gesture != null) { + emit("gesture", gesture); + } + }); + } +} diff --git a/app/src/main/java/com/stardust/autojs/runtime/api/Files.java b/app/src/main/java/com/stardust/autojs/runtime/api/Files.java new file mode 100644 index 00000000..c69fafb8 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/runtime/api/Files.java @@ -0,0 +1,205 @@ +package com.stardust.autojs.runtime.api; + +import com.stardust.autojs.runtime.ScriptRuntime; +import com.stardust.pio.PFileInterface; +import com.stardust.pio.PFiles; +import com.stardust.pio.UncheckedIOException; +import com.stardust.util.Func1; + +import java.io.File; +import java.io.IOException; + +/** + * Created by Stardust on 2018/1/23. + */ + +public class Files { + + private final ScriptRuntime mRuntime; + + public Files(ScriptRuntime runtime) { + mRuntime = runtime; + } + + // FIXME: 2018/10/16 is not correct in sub-directory? + public String path(String relativePath) { + String cwd = cwd(); + if (cwd == null || relativePath == null || relativePath.startsWith("/")) + return relativePath; + File f = new File(cwd); + String[] paths = relativePath.split("/"); + for (String path : paths) { + if (path.equals(".")) + continue; + if (path.equals("..")) { + f = f.getParentFile(); + continue; + } + f = new File(f, path); + } + String path = f.getPath(); + return relativePath.endsWith(File.separator) ? path + "/" : path; + } + + public String cwd() { + return mRuntime.engines.myEngine().cwd(); + } + + public PFileInterface open(String path, String mode, String encoding, int bufferSize) { + return PFiles.open(path(path), mode, encoding, bufferSize); + } + + public Object open(String path, String mode, String encoding) { + return PFiles.open(path(path), mode, encoding); + } + + public Object open(String path, String mode) { + return PFiles.open(path(path), mode); + } + + public Object open(String path) { + return PFiles.open(path(path)); + } + + public boolean create(String path) { + return PFiles.create(path(path)); + } + + public boolean createIfNotExists(String path) { + return PFiles.createIfNotExists(path(path)); + } + + public boolean createWithDirs(String path) { + return PFiles.createWithDirs(path(path)); + } + + public boolean exists(String path) { + return PFiles.exists(path(path)); + } + + public boolean ensureDir(String path) { + return PFiles.ensureDir(path(path)); + } + + public String read(String path, String encoding) { + return PFiles.read(path(path), encoding); + } + + + public String read(String path) { + return PFiles.read(path(path)); + } + + public String readAssets(String path, String encoding) { + try { + return PFiles.read(mRuntime.getUiHandler().getContext().getAssets().open(path), encoding); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public String readAssets(String path) { + return readAssets(path, "UTF-8"); + } + + public byte[] readBytes(String path) { + return PFiles.readBytes(path(path)); + } + + public void write(String path, String text) { + PFiles.write(path(path), text); + } + + public void write(String path, String text, String encoding) { + PFiles.write(path(path), text, encoding); + } + + public void append(String path, String text) { + PFiles.append(path(path), text); + } + + public void append(String path, String text, String encoding) { + PFiles.append(path(path), text, encoding); + } + + public void appendBytes(String path, byte[] bytes) { + PFiles.appendBytes(path(path), bytes); + } + + public void writeBytes(String path, byte[] bytes) { + PFiles.writeBytes(path(path), bytes); + } + + public boolean copy(String pathFrom, String pathTo) { + return PFiles.copy(path(pathFrom), path(pathTo)); + } + + public boolean renameWithoutExtension(String path, String newName) { + return PFiles.renameWithoutExtension(path(path), newName); + } + + public boolean rename(String path, String newName) { + return PFiles.rename(path(path), newName); + } + + public boolean move(String path, String newPath) { + return PFiles.move(path(path), newPath); + } + + public String getExtension(String fileName) { + return PFiles.getExtension(fileName); + } + + public String getName(String filePath) { + return PFiles.getName(filePath); + } + + public String getNameWithoutExtension(String filePath) { + return PFiles.getNameWithoutExtension(filePath); + } + + public boolean remove(String path) { + return PFiles.remove(path(path)); + } + + public boolean removeDir(String path) { + return PFiles.removeDir(path(path)); + } + + public String getSdcardPath() { + return PFiles.getSdcardPath(); + } + + public String[] listDir(String path) { + return PFiles.listDir(path(path)); + } + + public String[] listDir(String path, Func1 filter) { + return PFiles.listDir(path(path), filter); + } + + public boolean isFile(String path) { + return PFiles.isFile(path(path)); + } + + public boolean isDir(String path) { + return PFiles.isDir(path(path)); + } + + public boolean isEmptyDir(String path) { + return PFiles.isEmptyDir(path(path)); + } + + public static String join(String parent, String... child) { + return PFiles.join(parent, child); + } + + public String getHumanReadableSize(long bytes) { + return PFiles.getHumanReadableSize(bytes); + } + + public String getSimplifiedPath(String path) { + return PFiles.getSimplifiedPath(path); + } + +} diff --git a/app/src/main/java/com/stardust/autojs/runtime/api/Floaty.java b/app/src/main/java/com/stardust/autojs/runtime/api/Floaty.java new file mode 100644 index 00000000..8ed7c951 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/runtime/api/Floaty.java @@ -0,0 +1,314 @@ +package com.stardust.autojs.runtime.api; + +import android.content.Context; +import android.content.Intent; +import android.os.Looper; +import android.view.ContextThemeWrapper; +import android.view.View; + +import org.autojs.autojs6.R; +import com.stardust.autojs.core.floaty.BaseResizableFloatyWindow; +import com.stardust.autojs.core.floaty.RawWindow; +import com.stardust.autojs.core.ui.JsViewHelper; +import com.stardust.autojs.core.ui.inflater.DynamicLayoutInflater; +import com.stardust.autojs.core.ui.inflater.inflaters.Exceptions; +import com.stardust.autojs.runtime.ScriptRuntime; +import com.stardust.autojs.runtime.exception.ScriptInterruptedException; +import com.stardust.autojs.util.FloatingPermission; +import org.autojs.autojs.ui.enhancedfloaty.FloatyService; +import com.stardust.util.UiHandler; +import com.stardust.util.ViewUtil; + +import java.util.concurrent.CopyOnWriteArraySet; + +/** + * Created by Stardust on 2017/12/5. + */ + +public class Floaty { + + private DynamicLayoutInflater mLayoutInflater; + private Context mContext; + private UiHandler mUiHandler; + private CopyOnWriteArraySet mWindows = new CopyOnWriteArraySet<>(); + private ScriptRuntime mRuntime; + + public Floaty(UiHandler uiHandler, UI ui, ScriptRuntime runtime) { + mUiHandler = uiHandler; + mRuntime = runtime; + mContext = new ContextThemeWrapper(mUiHandler.getContext(), R.style.ScriptTheme); + mLayoutInflater = ui.getLayoutInflater(); + } + + public JsResizableWindow window(BaseResizableFloatyWindow.ViewSupplier supplier) { + try { + FloatingPermission.waitForPermissionGranted(mContext); + } catch (InterruptedException e) { + throw new ScriptInterruptedException(); + } + JsResizableWindow window = new JsResizableWindow(supplier); + addWindow(window); + return window; + } + + public JsResizableWindow window(View view) { + try { + FloatingPermission.waitForPermissionGranted(view.getContext()); + } catch (InterruptedException e) { + throw new ScriptInterruptedException(); + } + + JsResizableWindow window = new JsResizableWindow((context, parent) -> view); + addWindow(window); + return window; + } + + public JsRawWindow rawWindow(RawWindow.RawFloaty floaty) { + try { + FloatingPermission.waitForPermissionGranted(mContext); + } catch (InterruptedException e) { + throw new ScriptInterruptedException(); + } + JsRawWindow window = new JsRawWindow(floaty); + addWindow(window); + return window; + } + + public JsRawWindow rawWindow(View view) { + try { + FloatingPermission.waitForPermissionGranted(mContext); + } catch (InterruptedException e) { + throw new ScriptInterruptedException(); + } + JsRawWindow window = new JsRawWindow((context, parent) -> view); + addWindow(window); + return window; + } + + private synchronized void addWindow(JsWindow window) { + mWindows.add(window); + } + + private synchronized boolean removeWindow(JsWindow window) { + return mWindows.remove(window); + } + + public synchronized void closeAll() { + for (JsWindow window : mWindows) { + window.close(false); + } + mWindows.clear(); + } + + public interface JsWindow { + void close(boolean removeFromWindows); + } + + public class JsRawWindow implements JsWindow { + + private RawWindow mWindow; + private boolean mExitOnClose; + + public JsRawWindow(RawWindow.RawFloaty floaty) { + mWindow = new RawWindow(floaty); + mUiHandler.post(() -> { + mUiHandler.getContext().startService(new Intent(mUiHandler.getContext(), FloatyService.class)); + FloatyService.addWindow(mWindow); + }); + RuntimeException exception = mWindow.waitForCreation(); + if (exception != Exceptions.NO_EXCEPTION && exception != null) { + throw exception; + } + } + + public View findView(String id) { + return JsViewHelper.findViewByStringId(mWindow.getContentView(), id); + } + + public int getX() { + return mWindow.getWindowBridge().getX(); + } + + public int getY() { + return mWindow.getWindowBridge().getY(); + } + + public int getWidth() { + return mWindow.getWindowView().getWidth(); + } + + public int getHeight() { + return mWindow.getWindowView().getHeight(); + } + + public void setSize(int w, int h) { + runWithWindow(() -> { + mWindow.getWindowBridge().updateMeasure(w, h); + ViewUtil.setViewMeasure(mWindow.getWindowView(), w, h); + } + ); + } + + public void setTouchable(boolean touchable) { + runWithWindow(() -> mWindow.setTouchable(touchable)); + } + + private void runWithWindow(Runnable r) { + if (mWindow == null) + return; + if (Looper.myLooper() == Looper.getMainLooper()) { + r.run(); + return; + } + mUiHandler.post(() -> { + if (mWindow == null) + return; + r.run(); + }); + } + + public void setPosition(int x, int y) { + runWithWindow(() -> mWindow.getWindowBridge().updatePosition(x, y)); + } + + public void exitOnClose() { + mExitOnClose = true; + } + + public void requestFocus() { + mWindow.requestWindowFocus(); + } + + public void disableFocus() { + mWindow.disableWindowFocus(); + } + + public void close() { + close(true); + } + + public void close(boolean removeFromWindows) { + if (removeFromWindows && !removeWindow(this)) { + return; + } + runWithWindow(() -> { + mWindow.close(); + mWindow = null; + if (mExitOnClose) { + mRuntime.exit(); + } + }); + } + + } + + public class JsResizableWindow implements JsWindow { + + private View mView; + private volatile BaseResizableFloatyWindow mWindow; + private boolean mExitOnClose = false; + + public JsResizableWindow(BaseResizableFloatyWindow.ViewSupplier supplier) { + mWindow = new BaseResizableFloatyWindow(mContext, (context, parent) -> { + mView = supplier.inflate(context, parent); + return mView; + }); + mUiHandler.post(() -> { + mUiHandler.getContext().startService(new Intent(mUiHandler.getContext(), FloatyService.class)); + FloatyService.addWindow(mWindow); + }); + RuntimeException exception = mWindow.waitForCreation(); + if (exception != Exceptions.NO_EXCEPTION && exception != null) { + throw exception; + } + mWindow.setOnCloseButtonClickListener(v -> close()); + //setSize(mWindow.getWindowBridge().getScreenWidth() / 2, mWindow.getWindowBridge().getScreenHeight() / 2); + } + + public View findView(String id) { + return JsViewHelper.findViewByStringId(mView, id); + } + + public int getX() { + return mWindow.getWindowBridge().getX(); + } + + public int getY() { + return mWindow.getWindowBridge().getY(); + } + + public int getWidth() { + return mWindow.getRootView().getWidth(); + } + + public int getHeight() { + return mWindow.getRootView().getHeight(); + } + + public void setSize(int w, int h) { + runWithWindow(() -> { + mWindow.getWindowBridge().updateMeasure(w, h); + ViewUtil.setViewMeasure(mWindow.getRootView(), w, h); + } + ); + } + + + private void runWithWindow(Runnable r) { + if (mWindow == null) + return; + if (Looper.myLooper() == Looper.getMainLooper()) { + r.run(); + return; + } + mUiHandler.post(() -> { + if (mWindow == null) + return; + r.run(); + }); + } + + public void setPosition(int x, int y) { + runWithWindow(() -> mWindow.getWindowBridge().updatePosition(x, y)); + } + + public void setAdjustEnabled(boolean enabled) { + runWithWindow(() -> mWindow.setAdjustEnabled(enabled)); + } + + public boolean isAdjustEnabled() { + return mWindow.isAdjustEnabled(); + } + + public void exitOnClose() { + mExitOnClose = true; + } + + public void requestFocus() { + mWindow.requestWindowFocus(); + } + + public void disableFocus() { + mWindow.disableWindowFocus(); + } + + public void close() { + close(true); + } + + public void close(boolean removeFromWindows) { + if (removeFromWindows && !removeWindow(this)) { + return; + } + runWithWindow(() -> { + mWindow.close(); + mWindow = null; + if (mExitOnClose) { + mRuntime.exit(); + } + }); + } + } + + +} diff --git a/app/src/main/java/com/stardust/autojs/runtime/api/Images.java b/app/src/main/java/com/stardust/autojs/runtime/api/Images.java new file mode 100644 index 00000000..8c5a4e96 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/runtime/api/Images.java @@ -0,0 +1,368 @@ +package com.stardust.autojs.runtime.api; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.media.Image; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; + +import androidx.annotation.RequiresApi; + +import android.util.Base64; +import android.view.Gravity; + +import com.stardust.autojs.annotation.ScriptVariable; +import com.stardust.autojs.core.image.ColorFinder; +import com.stardust.autojs.core.image.ImageWrapper; +import com.stardust.autojs.core.image.TemplateMatching; +import com.stardust.autojs.core.image.capture.ScreenCaptureRequester; +import com.stardust.autojs.core.image.capture.ScreenCapturer; +import com.stardust.autojs.core.opencv.Mat; +import com.stardust.autojs.core.opencv.OpenCVHelper; +import com.stardust.autojs.core.ui.inflater.util.Drawables; +import com.stardust.autojs.core.util.ScriptPromiseAdapter; +import com.stardust.autojs.runtime.ScriptRuntime; +import com.stardust.concurrent.VolatileDispose; +import com.stardust.pio.UncheckedIOException; +import com.stardust.util.ScreenMetrics; + +import org.opencv.core.Point; +import org.opencv.core.Rect; +import org.opencv.imgproc.Imgproc; + +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * Created by Stardust on 2017/5/20. + */ +@RequiresApi(api = Build.VERSION_CODES.KITKAT) +public class Images { + + private ScriptRuntime mScriptRuntime; + private ScreenCaptureRequester mScreenCaptureRequester; + private ScreenCapturer mScreenCapturer; + private Context mContext; + private Image mPreCapture; + private ImageWrapper mPreCaptureImage; + private ScreenMetrics mScreenMetrics; + private volatile boolean mOpenCvInitialized = false; + + @ScriptVariable + public final ColorFinder colorFinder; + + public Images(Context context, ScriptRuntime scriptRuntime, ScreenCaptureRequester screenCaptureRequester) { + mScriptRuntime = scriptRuntime; + mScreenCaptureRequester = screenCaptureRequester; + mContext = context; + mScreenMetrics = mScriptRuntime.getScreenMetrics(); + colorFinder = new ColorFinder(mScreenMetrics); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public ScriptPromiseAdapter requestScreenCapture(int orientation) { + ScriptRuntime.requiresApi(21); + ScriptPromiseAdapter promiseAdapter = new ScriptPromiseAdapter(); + if (mScreenCapturer != null) { + mScreenCapturer.setOrientation(orientation); + promiseAdapter.resolve(true); + return promiseAdapter; + } + Looper servantLooper = mScriptRuntime.loopers.getServantLooper(); + mScreenCaptureRequester.setOnActivityResultCallback((result, data) -> { + if (result == Activity.RESULT_OK) { + mScreenCapturer = new ScreenCapturer(mContext, data, orientation, ScreenMetrics.getDeviceScreenDensity(), + new Handler(servantLooper)); + promiseAdapter.resolve(true); + } else { + promiseAdapter.resolve(false); + } + }); + mScreenCaptureRequester.request(); + return promiseAdapter; + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public synchronized ImageWrapper captureScreen() { + ScriptRuntime.requiresApi(21); + if (mScreenCapturer == null) { + throw new SecurityException("No screen capture permission"); + } + Image capture = mScreenCapturer.capture(); + if (capture == mPreCapture && mPreCaptureImage != null) { + return mPreCaptureImage; + } + mPreCapture = capture; + if (mPreCaptureImage != null) { + mPreCaptureImage.recycle(); + } + mPreCaptureImage = ImageWrapper.ofImage(capture); + return mPreCaptureImage; + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public boolean captureScreen(String path) { + path = mScriptRuntime.files.path(path); + ImageWrapper image = captureScreen(); + if (image != null) { + image.saveTo(path); + return true; + } + return false; + } + + public ImageWrapper copy(ImageWrapper image) { + return image.clone(); + } + + public boolean save(ImageWrapper image, String path, String format, int quality) throws IOException { + Bitmap.CompressFormat compressFormat = parseImageFormat(format); + if (compressFormat == null) + throw new IllegalArgumentException("unknown format " + format); + Bitmap bitmap = image.getBitmap(); + FileOutputStream outputStream = new FileOutputStream(mScriptRuntime.files.path(path)); + return bitmap.compress(compressFormat, quality, outputStream); + } + + public static int pixel(ImageWrapper image, int x, int y) { + if (image == null) { + throw new NullPointerException("image = null"); + } + return image.pixel(x, y); + } + + public static ImageWrapper concat(ImageWrapper img1, ImageWrapper img2, int direction) { + if (!Arrays.asList(Gravity.LEFT, Gravity.RIGHT, Gravity.TOP, Gravity.BOTTOM).contains(direction)) { + throw new IllegalArgumentException("unknown direction " + direction); + } + int width; + int height; + if (direction == Gravity.LEFT || direction == Gravity.TOP) { + ImageWrapper tmp = img1; + img1 = img2; + img2 = tmp; + } + if (direction == Gravity.LEFT || direction == Gravity.RIGHT) { + width = img1.getWidth() + img2.getWidth(); + height = Math.max(img1.getHeight(), img2.getHeight()); + } else { + width = Math.max(img1.getWidth(), img2.getHeight()); + height = img1.getHeight() + img2.getHeight(); + } + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + Paint paint = new Paint(); + if (direction == Gravity.LEFT || direction == Gravity.RIGHT) { + canvas.drawBitmap(img1.getBitmap(), 0, (height - img1.getHeight()) / 2, paint); + canvas.drawBitmap(img2.getBitmap(), img1.getWidth(), (height - img2.getHeight()) / 2, paint); + } else { + canvas.drawBitmap(img1.getBitmap(), (width - img1.getWidth()) / 2, 0, paint); + canvas.drawBitmap(img2.getBitmap(), (width - img2.getWidth()) / 2, img1.getHeight(), paint); + } + return ImageWrapper.ofBitmap(bitmap); + } + + public ImageWrapper rotate(ImageWrapper img, float x, float y, float degree) { + Matrix matrix = new Matrix(); + matrix.postRotate(degree, x, y); + return ImageWrapper.ofBitmap(Bitmap.createBitmap(img.getBitmap(), 0, 0, img.getWidth(), img.getHeight(), matrix, true)); + } + + public ImageWrapper clip(ImageWrapper img, int x, int y, int w, int h) { + return ImageWrapper.ofBitmap(Bitmap.createBitmap(img.getBitmap(), x, y, w, h)); + } + + public ImageWrapper read(String path) { + path = mScriptRuntime.files.path(path); + Bitmap bitmap = BitmapFactory.decodeFile(path); + return ImageWrapper.ofBitmap(bitmap); + } + + public ImageWrapper fromBase64(String data) { + return ImageWrapper.ofBitmap(Drawables.loadBase64Data(data)); + } + + public String toBase64(ImageWrapper wrapper, String format, int quality) { + return Base64.encodeToString(toBytes(wrapper, format, quality), Base64.NO_WRAP); + } + + public byte[] toBytes(ImageWrapper wrapper, String format, int quality) { + Bitmap.CompressFormat compressFormat = parseImageFormat(format); + if (compressFormat == null) + throw new IllegalArgumentException("unknown format " + format); + Bitmap bitmap = wrapper.getBitmap(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + bitmap.compress(compressFormat, quality, outputStream); + return outputStream.toByteArray(); + } + + public ImageWrapper fromBytes(byte[] bytes) { + return ImageWrapper.ofBitmap(BitmapFactory.decodeByteArray(bytes, 0, bytes.length)); + } + + private Bitmap.CompressFormat parseImageFormat(String format) { + switch (format) { + case "png": + return Bitmap.CompressFormat.PNG; + case "jpeg": + case "jpg": + return Bitmap.CompressFormat.JPEG; + case "webp": + return Bitmap.CompressFormat.WEBP; + } + return null; + } + + public ImageWrapper load(String src) { + try { + URL url = new URL(src); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setDoInput(true); + connection.connect(); + InputStream input = connection.getInputStream(); + Bitmap bitmap = BitmapFactory.decodeStream(input); + return ImageWrapper.ofBitmap(bitmap); + } catch (IOException e) { + return null; + } + } + + public static void saveBitmap(Bitmap bitmap, String path) { + try { + bitmap.compress(Bitmap.CompressFormat.PNG, 100, new FileOutputStream(path)); + } catch (FileNotFoundException e) { + throw new UncheckedIOException(e); + } + } + + public static Bitmap scaleBitmap(Bitmap origin, int newWidth, int newHeight) { + if (origin == null) { + return null; + } + int height = origin.getHeight(); + int width = origin.getWidth(); + float scaleWidth = ((float) newWidth) / width; + float scaleHeight = ((float) newHeight) / height; + Matrix matrix = new Matrix(); + matrix.postScale(scaleWidth, scaleHeight); + return Bitmap.createBitmap(origin, 0, 0, width, height, matrix, false); + } + + public void releaseScreenCapturer() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && mScreenCapturer != null) { + mScreenCapturer.release(); + } + } + + public Point findImage(ImageWrapper image, ImageWrapper template) { + return findImage(image, template, 0.9f, null); + } + + public Point findImage(ImageWrapper image, ImageWrapper template, float threshold) { + return findImage(image, template, threshold, null); + } + + public Point findImage(ImageWrapper image, ImageWrapper template, float threshold, Rect rect) { + return findImage(image, template, 0.7f, threshold, rect, TemplateMatching.MAX_LEVEL_AUTO); + } + + public Point findImage(ImageWrapper image, ImageWrapper template, float weakThreshold, float threshold, Rect rect, int maxLevel) { + initOpenCvIfNeeded(); + if (image == null) + throw new NullPointerException("image = null"); + if (template == null) + throw new NullPointerException("template = null"); + Mat src = image.getMat(); + if (rect != null) { + src = new Mat(src, rect); + } + org.opencv.core.Point point = TemplateMatching.fastTemplateMatching(src, template.getMat(), TemplateMatching.MATCHING_METHOD_DEFAULT, + weakThreshold, threshold, maxLevel); + if (point != null) { + if (rect != null) { + point.x += rect.x; + point.y += rect.y; + } + point.x = mScreenMetrics.scaleX((int) point.x); + point.y = mScreenMetrics.scaleX((int) point.y); + } + if (src != image.getMat()) { + OpenCVHelper.release(src); + } + return point; + } + + public List matchTemplate(ImageWrapper image, ImageWrapper template, float weakThreshold, float threshold, Rect rect, int maxLevel, int limit) { + initOpenCvIfNeeded(); + if (image == null) + throw new NullPointerException("image = null"); + if (template == null) + throw new NullPointerException("template = null"); + Mat src = image.getMat(); + if (rect != null) { + src = new Mat(src, rect); + } + List result = TemplateMatching.fastTemplateMatching(src, template.getMat(), Imgproc.TM_CCOEFF_NORMED, + weakThreshold, threshold, maxLevel, limit); + for (TemplateMatching.Match match : result) { + Point point = match.point; + if (rect != null) { + point.x += rect.x; + point.y += rect.y; + } + point.x = mScreenMetrics.scaleX((int) point.x); + point.y = mScreenMetrics.scaleX((int) point.y); + } + if (src != image.getMat()) { + OpenCVHelper.release(src); + } + return result; + } + + public Mat newMat() { + return new Mat(); + } + + public Mat newMat(Mat mat, Rect roi) { + return new Mat(mat, roi); + } + + public void initOpenCvIfNeeded() { + if (mOpenCvInitialized || OpenCVHelper.isInitialized()) { + return; + } + Activity currentActivity = mScriptRuntime.app.getCurrentActivity(); + Context context = currentActivity == null ? mContext : currentActivity; + mScriptRuntime.console.info("opencv initializing"); + if (Looper.myLooper() == Looper.getMainLooper()) { + OpenCVHelper.initIfNeeded(context, () -> { + mOpenCvInitialized = true; + mScriptRuntime.console.info("opencv initialized"); + }); + } else { + VolatileDispose result = new VolatileDispose<>(); + OpenCVHelper.initIfNeeded(context, () -> { + mOpenCvInitialized = true; + result.setAndNotify(true); + mScriptRuntime.console.info("opencv initialized"); + }); + result.blockedGet(); + } + + } +} diff --git a/app/src/main/java/com/stardust/autojs/runtime/api/Media.java b/app/src/main/java/com/stardust/autojs/runtime/api/Media.java new file mode 100644 index 00000000..76622a7a --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/runtime/api/Media.java @@ -0,0 +1,193 @@ +package com.stardust.autojs.runtime.api; + +import android.content.Context; +import android.media.MediaPlayer; +import android.media.MediaScannerConnection; +import android.net.Uri; + +import com.stardust.autojs.runtime.ScriptRuntime; +import com.stardust.pio.UncheckedIOException; +import com.stardust.util.MimeTypes; + +import java.io.IOException; + +/** + * Created by Stardust on 2018/2/12. + */ + +public class Media implements MediaScannerConnection.MediaScannerConnectionClient { + + private MediaScannerConnection mScannerConnection; + private MediaPlayerWrapper mMediaPlayer; + private ScriptRuntime mRuntime; + + public Media(Context context, ScriptRuntime runtime) { + mScannerConnection = new MediaScannerConnection(context, this); + mRuntime = runtime; + mScannerConnection.connect(); + } + + public void scanFile(String path) { + String mimeType = MimeTypes.fromFileOr(path, null); + mScannerConnection.scanFile(mRuntime.files.path(path), mimeType); + } + + @Override + public void onMediaScannerConnected() { + + } + + public void playMusic(String path, float volume) { + playMusic(path, volume, false); + } + + public void playMusic(String path) { + playMusic(path, 1.0f); + } + + public void playMusic(String path, float volume, boolean looping) { + path = mRuntime.files.path(path); + if (mMediaPlayer == null) { + mMediaPlayer = new MediaPlayerWrapper(); + } + mMediaPlayer.stopAndReset(); + try { + mMediaPlayer.setDataSource(path); + mMediaPlayer.setVolume(volume, volume); + mMediaPlayer.setLooping(looping); + mMediaPlayer.prepare(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + mMediaPlayer.start(); + } + + public void musicSeekTo(int m) { + if (mMediaPlayer == null) + return; + mMediaPlayer.seekTo(m); + } + + public boolean isMusicPlaying() { + return mMediaPlayer != null && mMediaPlayer.isPlaying(); + } + + public void pauseMusic() { + if (mMediaPlayer == null) + return; + mMediaPlayer.pause(); + } + + public void resumeMusic() { + if (mMediaPlayer == null) + return; + mMediaPlayer.start(); + } + + public int getMusicDuration() { + if (mMediaPlayer == null) { + return 0; + } + return mMediaPlayer.getDuration(); + } + + public int getMusicCurrentPosition() { + if (mMediaPlayer == null) + return -1; + return mMediaPlayer.getCurrentPosition(); + } + + public void stopMusic() { + if (mMediaPlayer == null) + return; + mMediaPlayer.stop(); + } + + + @Override + public void onScanCompleted(String path, Uri uri) { + + } + + public void recycle() { + if (mScannerConnection != null) { + mScannerConnection.disconnect(); + mScannerConnection = null; + } + if (mMediaPlayer != null) { + mMediaPlayer.release(); + } + } + + private static class MediaPlayerWrapper extends MediaPlayer { + + static final int STATE_NOT_INITIALIZED = 0; + static final int STATE_PREPARING = 1; + static final int STATE_PREPARED = 2; + static final int STATE_START = 3; + static final int STATE_PAUSED = 4; + static final int STATE_STOPPED = 5; + static final int STATE_RELEASED = 6; + + private int mState = STATE_NOT_INITIALIZED; + + public int getState() { + return mState; + } + + @Override + public void prepare() throws IOException, IllegalStateException { + mState = STATE_PREPARING; + super.prepare(); + mState = STATE_PREPARED; + } + + @Override + public void prepareAsync() throws IllegalStateException { + throw new UnsupportedOperationException(); + } + + @Override + public void start() throws IllegalStateException { + super.start(); + mState = STATE_START; + } + + @Override + public void stop() throws IllegalStateException { + super.stop(); + mState = STATE_STOPPED; + } + + @Override + public void pause() throws IllegalStateException { + super.pause(); + mState = STATE_PAUSED; + } + + @Override + public void release() { + super.release(); + mState = STATE_RELEASED; + } + + @Override + public void reset() { + super.reset(); + mState = STATE_NOT_INITIALIZED; + } + + public void stopAndReset() { + try { + if (mState == STATE_START || mState == STATE_PAUSED) { + stop(); + } + if (mState != STATE_NOT_INITIALIZED) { + reset(); + } + } catch (IllegalStateException ignored) { + + } + } + } +} diff --git a/app/src/main/java/com/stardust/autojs/runtime/api/Plugins.java b/app/src/main/java/com/stardust/autojs/runtime/api/Plugins.java new file mode 100644 index 00000000..d0ac77ca --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/runtime/api/Plugins.java @@ -0,0 +1,49 @@ +package com.stardust.autojs.runtime.api; + +import android.content.Context; + +import com.stardust.autojs.core.plugin.Plugin; +import com.stardust.autojs.runtime.ScriptRuntime; +import com.stardust.pio.PFiles; + +import java.io.File; + +public class Plugins { + + private final Context mContext; + private final ScriptRuntime mRuntime; + private File mPluginCacheDir; + + public Plugins(Context context, ScriptRuntime runtime) { + mContext = context; + mRuntime = runtime; + mPluginCacheDir = new File(mContext.getCacheDir(), "plugin-scripts/"); + } + + public Plugin load(String packageName) { + try { + Context packageContext = mContext.createPackageContext(packageName, Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY); + Plugin plugin = Plugin.load(mContext, packageContext, mRuntime, mRuntime.getTopLevelScope()); + if (plugin == null) { + return null; + } + File scriptCacheDir = getScriptCacheDir(packageName); + PFiles.copyAssetDir(packageContext.getAssets(), plugin.getAssetsScriptDir(), scriptCacheDir.getPath(), null); + plugin.setMainScriptPath(new File(scriptCacheDir, "index.js").getPath()); + return plugin; + } catch (Exception e) { + throw new Plugin.PluginLoadException(e); + } + } + + private File getScriptCacheDir(String packageName) { + File dir = new File(mPluginCacheDir, packageName + "/"); + dir.mkdirs(); + return dir; + } + + public void clear() { + PFiles.deleteRecursively(mPluginCacheDir); + } + +} diff --git a/app/src/main/java/com/stardust/autojs/runtime/api/Sensors.java b/app/src/main/java/com/stardust/autojs/runtime/api/Sensors.java new file mode 100644 index 00000000..f8075300 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/runtime/api/Sensors.java @@ -0,0 +1,171 @@ +package com.stardust.autojs.runtime.api; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import androidx.annotation.NonNull; + +import com.stardust.autojs.core.eventloop.EventEmitter; +import com.stardust.autojs.core.looper.Loopers; +import com.stardust.autojs.runtime.ScriptBridges; +import com.stardust.autojs.runtime.ScriptRuntime; +import com.stardust.util.MapBuilder; + +import java.lang.reflect.Field; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Created by Stardust on 2018/2/5. + */ + +public class Sensors extends EventEmitter implements Loopers.LooperQuitHandler { + + + public class SensorEventEmitter extends EventEmitter implements SensorEventListener { + + public SensorEventEmitter(ScriptBridges bridges) { + super(bridges); + } + + @Override + public void onSensorChanged(SensorEvent event) { + Object[] args = new Object[event.values.length + 1]; + args[0] = event; + for (int i = 1; i < args.length; i++) { + args[i] = event.values[i - 1]; + } + emit("change", args); + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + emit("accuracy_change", accuracy); + } + + public void unregister() { + Sensors.this.unregister(this); + } + } + + public static class Delay { + public static final int normal = SensorManager.SENSOR_DELAY_NORMAL; + public static final int ui = SensorManager.SENSOR_DELAY_UI; + public static final int game = SensorManager.SENSOR_DELAY_GAME; + public static final int fastest = SensorManager.SENSOR_DELAY_FASTEST; + } + + + private static final Map SENSORS = new MapBuilder() + .put("ACCELEROMETER", Sensor.TYPE_ACCELEROMETER) + .put("MAGNETIC_FIELD", Sensor.TYPE_MAGNETIC_FIELD) + .put("ORIENTATION", Sensor.TYPE_ORIENTATION) + .put("GYROSCOPE", Sensor.TYPE_GYROSCOPE) + .put("LIGHT", Sensor.TYPE_LIGHT) + .put("TEMPERATURE", Sensor.TYPE_TEMPERATURE) + .put("PRESSURE", Sensor.TYPE_PRESSURE) + .put("AMBIENT_TEMPERATURE", Sensor.TYPE_AMBIENT_TEMPERATURE) + .put("PROXIMITY", Sensor.TYPE_PROXIMITY) + .put("GRAVITY", Sensor.TYPE_GRAVITY) + .put("LINEAR_ACCELERATION", Sensor.TYPE_LINEAR_ACCELERATION) + .put("RELATIVE_HUMIDITY", Sensor.TYPE_RELATIVE_HUMIDITY) + .put("AMBIENT_TEMPERATURE", Sensor.TYPE_AMBIENT_TEMPERATURE) + .build(); + + public boolean ignoresUnsupportedSensor = false; + public final Delay delay = new Delay(); + + private final Set mSensorEventEmitters = new HashSet<>(); + private final SensorManager mSensorManager; + private final ScriptBridges mScriptBridges; + private final SensorEventEmitter mNoOpSensorEventEmitter; + private final ScriptRuntime mScriptRuntime; + + + public Sensors(Context context, ScriptRuntime runtime) { + super(runtime.bridges); + mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); + mScriptBridges = runtime.bridges; + mNoOpSensorEventEmitter = new SensorEventEmitter(runtime.bridges); + mScriptRuntime = runtime; + runtime.loopers.addLooperQuitHandler(this); + } + + public SensorEventEmitter register(String sensorName) { + return register(sensorName, Delay.normal); + } + + public SensorEventEmitter register(String sensorName, int delay) { + if (sensorName == null) + throw new NullPointerException("sensorName = null"); + Sensor sensor = getSensor(sensorName); + if (sensor == null) { + if (ignoresUnsupportedSensor) { + emit("unsupported_sensor", sensorName); + return mNoOpSensorEventEmitter; + } else { + return null; + } + } + return register(sensor, delay); + } + + private SensorEventEmitter register(@NonNull Sensor sensor, int delay) { + SensorEventEmitter emitter = new SensorEventEmitter(mScriptBridges); + mSensorManager.registerListener(emitter, sensor, delay); + synchronized (mSensorEventEmitters) { + mSensorEventEmitters.add(emitter); + } + return emitter; + } + + + @Override + public boolean shouldQuit() { + if (mSensorEventEmitters.isEmpty()) { + return true; + } + return false; + } + + public Sensor getSensor(String sensorName) { + Integer type = SENSORS.get(sensorName.toUpperCase()); + if (type == null) + type = getSensorTypeByReflect(sensorName); + if (type == null) + return null; + return mSensorManager.getDefaultSensor(type); + } + + private Integer getSensorTypeByReflect(String sensorName) { + sensorName = sensorName.toUpperCase(); + try { + Field field = Sensor.class.getField("TYPE_" + sensorName); + return (Integer) field.get(null); + } catch (Exception e) { + return null; + } + } + + public void unregister(SensorEventEmitter emitter) { + if (emitter == null) + return; + synchronized (mSensorEventEmitters) { + mSensorEventEmitters.remove(emitter); + } + mSensorManager.unregisterListener(emitter); + } + + public void unregisterAll() { + synchronized (mSensorEventEmitters) { + for (SensorEventEmitter emitter : mSensorEventEmitters) { + mSensorManager.unregisterListener(emitter); + } + mSensorEventEmitters.clear(); + } + mScriptRuntime.loopers.removeLooperQuitHandler(this); + } +} diff --git a/app/src/main/java/com/stardust/autojs/runtime/api/Threads.java b/app/src/main/java/com/stardust/autojs/runtime/api/Threads.java new file mode 100644 index 00000000..23a04d54 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/runtime/api/Threads.java @@ -0,0 +1,111 @@ +package com.stardust.autojs.runtime.api; + +import androidx.annotation.NonNull; + +import com.stardust.autojs.core.looper.MainThreadProxy; +import com.stardust.autojs.core.looper.TimerThread; +import com.stardust.autojs.runtime.ScriptRuntime; +import com.stardust.concurrent.VolatileDispose; + +import java.util.HashSet; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Created by Stardust on 2017/12/3. + */ + +public class Threads { + + private final HashSet mThreads = new HashSet<>(); + private ScriptRuntime mRuntime; + private final Thread mMainThread; + private MainThreadProxy mMainThreadProxy; + private int mSpawnCount = 0; + private boolean mExit = false; + + public Threads(ScriptRuntime runtime) { + mRuntime = runtime; + mMainThread = Thread.currentThread(); + mMainThreadProxy = new MainThreadProxy(Thread.currentThread(), mRuntime); + } + + public Thread getMainThread() { + return mMainThread; + } + + public Object currentThread() { + Thread thread = Thread.currentThread(); + if (thread == mMainThread) + return mMainThreadProxy; + return thread; + } + + public TimerThread start(Runnable runnable) { + TimerThread thread = createThread(runnable); + synchronized (mThreads) { + if (mExit) { + throw new IllegalStateException("script exiting"); + } + mThreads.add(thread); + thread.setName(mMainThread.getName() + " (Spawn-" + mSpawnCount + ")"); + mSpawnCount++; + thread.start(); + } + return thread; + } + + @NonNull + private TimerThread createThread(Runnable runnable) { + return new TimerThread(mRuntime, mRuntime.timers.getMaxCallbackUptimeMillisForAllThreads(), + runnable + ) { + @Override + protected void onExit() { + synchronized (mThreads) { + mThreads.remove(Thread.currentThread()); + } + super.onExit(); + } + }; + } + + public VolatileDispose disposable() { + return new VolatileDispose(); + } + + public AtomicLong atomic(long value) { + return new AtomicLong(value); + } + + public AtomicLong atomic() { + return new AtomicLong(); + } + + public Lock lock() { + return new ReentrantLock(); + } + + public void shutDownAll() { + synchronized (mThreads) { + for (Thread thread : mThreads) { + thread.interrupt(); + } + mThreads.clear(); + } + } + + public void exit() { + synchronized (mThreads) { + shutDownAll(); + mExit = true; + } + } + + public boolean hasRunningThreads() { + synchronized (mThreads) { + return !mThreads.isEmpty(); + } + } +} diff --git a/app/src/main/java/com/stardust/autojs/runtime/api/Timers.java b/app/src/main/java/com/stardust/autojs/runtime/api/Timers.java new file mode 100644 index 00000000..233fafad --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/runtime/api/Timers.java @@ -0,0 +1,95 @@ +package com.stardust.autojs.runtime.api; + +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import android.util.Log; +import android.util.SparseArray; + +import com.stardust.autojs.core.looper.Timer; +import com.stardust.autojs.core.looper.TimerThread; +import com.stardust.autojs.runtime.ScriptBridges; +import com.stardust.autojs.runtime.ScriptRuntime; +import com.stardust.concurrent.VolatileBox; + +/** + * Created by Stardust on 2017/7/21. + */ + +public class Timers { + + private static final String LOG_TAG = "Timers"; + + private VolatileBox mMaxCallbackUptimeMillisForAllThreads = new VolatileBox<>(0L); + private Threads mThreads; + private Timer mMainTimer; + private Timer mUiTimer; + + + public Timers(ScriptRuntime runtime) { + mMainTimer = new Timer(runtime, mMaxCallbackUptimeMillisForAllThreads); + mUiTimer = new Timer(runtime, mMaxCallbackUptimeMillisForAllThreads, Looper.getMainLooper()); + mThreads = runtime.threads; + } + + public Timer getMainTimer() { + return mMainTimer; + } + + VolatileBox getMaxCallbackUptimeMillisForAllThreads() { + return mMaxCallbackUptimeMillisForAllThreads; + } + + public Timer getTimerForCurrentThread() { + return getTimerForThread(Thread.currentThread()); + } + + public Timer getTimerForThread(Thread thread) { + if (thread == mThreads.getMainThread()) { + return mMainTimer; + } + Timer timer = TimerThread.getTimerForThread(thread); + if (timer == null && Looper.myLooper() == Looper.getMainLooper()) { + return mUiTimer; + } + return timer; + } + + public int setTimeout(Object callback, long delay, Object... args) { + return getTimerForCurrentThread().setTimeout(callback, delay, args); + } + + public boolean clearTimeout(int id) { + return getTimerForCurrentThread().clearTimeout(id); + } + + public int setInterval(Object listener, long interval, Object... args) { + return getTimerForCurrentThread().setInterval(listener, interval, args); + } + + public boolean clearInterval(int id) { + return getTimerForCurrentThread().clearInterval(id); + } + + public int setImmediate(Object listener, Object... args) { + return getTimerForCurrentThread().setImmediate(listener, args); + } + + public boolean clearImmediate(int id) { + return getTimerForCurrentThread().clearImmediate(id); + } + + public boolean hasPendingCallbacks() { + //如果是脚本主线程,则检查所有子线程中的定时回调。mFutureCallbackUptimeMillis用来记录所有子线程中定时最久的一个。 + if (mThreads.getMainThread() == Thread.currentThread()) { + return mMaxCallbackUptimeMillisForAllThreads.get() > SystemClock.uptimeMillis(); + } + // 否则检查当前线程的定时回调 + return getTimerForCurrentThread().hasPendingCallbacks(); + } + + public void recycle() { + mMainTimer.removeAllCallbacks(); + } + +} diff --git a/app/src/main/java/com/stardust/autojs/runtime/api/UI.java b/app/src/main/java/com/stardust/autojs/runtime/api/UI.java new file mode 100644 index 00000000..be8f6f01 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/runtime/api/UI.java @@ -0,0 +1,119 @@ +package com.stardust.autojs.runtime.api; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import androidx.annotation.Nullable; + +import com.stardust.autojs.core.graphics.ScriptCanvasView; +import com.stardust.autojs.core.ui.inflater.DynamicLayoutInflater; +import com.stardust.autojs.core.ui.inflater.ResourceParser; +import com.stardust.autojs.core.ui.inflater.inflaters.CanvasViewInflater; +import com.stardust.autojs.core.ui.inflater.inflaters.JsGridViewInflater; +import com.stardust.autojs.core.ui.inflater.inflaters.JsImageViewInflater; +import com.stardust.autojs.core.ui.inflater.inflaters.JsListViewInflater; +import com.stardust.autojs.core.ui.widget.JsGridView; +import com.stardust.autojs.core.ui.widget.JsImageView; +import com.stardust.autojs.core.ui.widget.JsListView; +import com.stardust.autojs.rhino.ProxyObject; +import com.stardust.autojs.runtime.ScriptRuntime; + +import org.mozilla.javascript.Scriptable; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Created by Stardust on 2017/5/14. + */ + +public class UI extends ProxyObject { + + + private Context mContext; + private Map mProperties = new ConcurrentHashMap<>(); + private DynamicLayoutInflater mDynamicLayoutInflater; + private ScriptRuntime mRuntime; + private ResourceParser mResourceParser; + + public UI(Context context, ScriptRuntime runtime) { + mContext = context; + mRuntime = runtime; + mResourceParser = new ResourceParser(new Drawables()); + mDynamicLayoutInflater = new DynamicLayoutInflater(mResourceParser); + mDynamicLayoutInflater.setContext(context); + mDynamicLayoutInflater.registerViewAttrSetter(JsImageView.class.getName(), + new JsImageViewInflater(mResourceParser)); + mDynamicLayoutInflater.registerViewAttrSetter(JsListView.class.getName(), + new JsListViewInflater(mResourceParser, runtime)); + mDynamicLayoutInflater.registerViewAttrSetter(JsGridView.class.getName(), + new JsGridViewInflater(mResourceParser, runtime)); + mDynamicLayoutInflater.registerViewAttrSetter(ScriptCanvasView.class.getName(), + new CanvasViewInflater(mResourceParser, runtime)); + mProperties.put("layoutInflater", this.mDynamicLayoutInflater); + } + + public DynamicLayoutInflater getDynamicLayoutInflater() { + return mDynamicLayoutInflater; + } + + public ResourceParser getResourceParser() { + return mResourceParser; + } + + public Object getBindingContext() { + return mProperties.get("bindingContext"); + } + + public void setBindingContext(Object context) { + if (context == null) + mProperties.remove("bindingContext"); + else + mProperties.put("bindingContext", context); + } + + + public DynamicLayoutInflater getLayoutInflater() { + return mDynamicLayoutInflater; + } + + @Override + public String getClassName() { + return UI.class.getSimpleName(); + } + + + @Override + public Object get(String name, Scriptable start) { + Object value = mProperties.get(name); + if (value != null) + return value; + return super.get(name, start); + } + + @Override + public void put(String name, Scriptable start, Object value) { + if (mProperties.containsKey(name)) { + if (value == null) { + mProperties.remove(name); + } else { + mProperties.put(name, value); + } + } else { + super.put(name, start, value); + } + } + + public void recycle(){ + mDynamicLayoutInflater.setContext(null); + } + + private class Drawables extends com.stardust.autojs.core.ui.inflater.util.Drawables { + + @Override + public Drawable decodeImage(String path) { + return super.decodeImage(mRuntime.files.path(path)); + } + } + + +} diff --git a/app/src/main/java/com/stardust/autojs/runtime/exception/ScriptEnvironmentException.java b/app/src/main/java/com/stardust/autojs/runtime/exception/ScriptEnvironmentException.java new file mode 100644 index 00000000..defec15c --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/runtime/exception/ScriptEnvironmentException.java @@ -0,0 +1,11 @@ +package com.stardust.autojs.runtime.exception; + +/** + * Created by Stardust on 2017/7/1. + */ + +public class ScriptEnvironmentException extends ScriptException { + public ScriptEnvironmentException(String s) { + super(s); + } +} diff --git a/app/src/main/java/com/stardust/autojs/runtime/exception/ScriptException.java b/app/src/main/java/com/stardust/autojs/runtime/exception/ScriptException.java new file mode 100644 index 00000000..4d3774d5 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/runtime/exception/ScriptException.java @@ -0,0 +1,28 @@ +package com.stardust.autojs.runtime.exception; + +import org.mozilla.javascript.EvaluatorException; +import org.mozilla.javascript.RhinoException; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Created by Stardust on 2017/1/29. + */ +public class ScriptException extends RuntimeException { + + + public ScriptException(String message, Throwable cause) { + super(message, cause); + } + + public ScriptException(String message) { + super(message); + } + + public ScriptException() { + } + + public ScriptException(Throwable cause) { + super(cause); + } +} diff --git a/app/src/main/java/com/stardust/autojs/runtime/exception/ScriptInterruptedException.java b/app/src/main/java/com/stardust/autojs/runtime/exception/ScriptInterruptedException.java new file mode 100644 index 00000000..37d64c37 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/runtime/exception/ScriptInterruptedException.java @@ -0,0 +1,28 @@ +package com.stardust.autojs.runtime.exception; + + +/** + * Created by Stardust on 2017/4/30. + */ + +public class ScriptInterruptedException extends ScriptException { + + public ScriptInterruptedException() { + + } + + public ScriptInterruptedException(Throwable e) { + super(e); + } + + public static boolean causedByInterrupted(Throwable e) { + while (e != null) { + if (e instanceof ScriptInterruptedException || e instanceof InterruptedException) { + return true; + } + e = e.getCause(); + } + return false; + } + +} diff --git a/app/src/main/java/com/stardust/autojs/script/AutoFileSource.java b/app/src/main/java/com/stardust/autojs/script/AutoFileSource.java new file mode 100644 index 00000000..f83a2f42 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/script/AutoFileSource.java @@ -0,0 +1,39 @@ +package com.stardust.autojs.script; + +import com.stardust.pio.PFiles; + +import java.io.File; + +/** + * Created by Stardust on 2017/8/2. + */ + +public class AutoFileSource extends ScriptSource { + + public static final String ENGINE = AutoFileSource.class.getName() + ".Engine"; + private File mFile; + + public AutoFileSource(File file) { + super(PFiles.getNameWithoutExtension(file.getAbsolutePath())); + mFile = file; + } + + public AutoFileSource(String path) { + this(new File(path)); + } + + + @Override + public String getEngineName() { + return ENGINE; + } + + public File getFile() { + return mFile; + } + + @Override + public String toString() { + return mFile.toString(); + } +} diff --git a/app/src/main/java/com/stardust/autojs/script/EncryptedScriptFileHeader.kt b/app/src/main/java/com/stardust/autojs/script/EncryptedScriptFileHeader.kt new file mode 100644 index 00000000..fe8948e9 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/script/EncryptedScriptFileHeader.kt @@ -0,0 +1,47 @@ +package com.stardust.autojs.script + +import java.io.File +import java.io.FileInputStream +import java.io.OutputStream + +object EncryptedScriptFileHeader { + + const val FLAG_INVALID_FILE: Short = Short.MIN_VALUE + + const val FLAG_EXECUTION_MODE_UI: Short = 0x0001 + const val FLAG_EXECUTION_MODE_AUTO: Short = 0x0002 + + const val BLOCK_SIZE = 8 + private val BLOCK = byteArrayOf(0x77, 0x01, 0x17, 0x7F, 0x12, 0x12) + + fun getHeaderFlags(file: File): Short { + val fis = FileInputStream(file) + val bytes = ByteArray(BLOCK_SIZE) + if (fis.read(bytes) < BLOCK_SIZE) { + return FLAG_INVALID_FILE + } + if (!isValidFile(bytes)) { + return FLAG_INVALID_FILE + } + return (bytes[BLOCK.size].toShort() * 256 + bytes[BLOCK.size + 1]).toShort() + } + + fun isValidFile(bytes: ByteArray): Boolean { + for (i in 0 until BLOCK.size) { + if (bytes[i] != BLOCK[i]) { + return false + } + } + return true + } + + fun writeHeader(os: OutputStream, flags: Short = 0) { + os.write(BLOCK) + val byte6 = flags / 256 + val byte7 = flags % 256 + os.write(byte6) + os.write(byte7) + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/stardust/autojs/script/JavaScriptFileSource.java b/app/src/main/java/com/stardust/autojs/script/JavaScriptFileSource.java new file mode 100644 index 00000000..6b9f6924 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/script/JavaScriptFileSource.java @@ -0,0 +1,75 @@ +package com.stardust.autojs.script; + +import androidx.annotation.NonNull; + +import com.stardust.pio.PFiles; +import com.stardust.pio.UncheckedIOException; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.Reader; + +/** + * Created by Stardust on 2017/4/2. + */ + +public class JavaScriptFileSource extends JavaScriptSource { + + private File mFile; + private String mScript; + private boolean mCustomsName = false; + + public JavaScriptFileSource(File file) { + super(PFiles.getNameWithoutExtension(file.getName())); + mFile = file; + } + + public JavaScriptFileSource(String path) { + this(new File(path)); + } + + public JavaScriptFileSource(String name, File file) { + super(name); + mCustomsName = true; + mFile = file; + } + + @NonNull + @Override + public String getScript() { + if (mScript == null) + mScript = PFiles.read(mFile); + return mScript; + } + + @Override + protected int parseExecutionMode() { + short flags = EncryptedScriptFileHeader.INSTANCE.getHeaderFlags(mFile); + if (flags == EncryptedScriptFileHeader.FLAG_INVALID_FILE) { + return super.parseExecutionMode(); + } + return flags; + } + + @Override + public Reader getScriptReader() { + try { + return new FileReader(mFile); + } catch (FileNotFoundException e) { + throw new UncheckedIOException(e); + } + } + + public File getFile() { + return mFile; + } + + @Override + public String toString() { + if (mCustomsName) { + return super.toString(); + } + return mFile.toString(); + } +} diff --git a/app/src/main/java/com/stardust/autojs/script/JavaScriptSource.java b/app/src/main/java/com/stardust/autojs/script/JavaScriptSource.java new file mode 100644 index 00000000..9089f0d5 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/script/JavaScriptSource.java @@ -0,0 +1,118 @@ +package com.stardust.autojs.script; + +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.stardust.autojs.rhino.TokenStream; +import com.stardust.util.MapBuilder; + +import org.mozilla.javascript.Token; + +import java.io.Reader; +import java.io.StringReader; +import java.util.Map; + +/** + * Created by Stardust on 2017/8/2. + */ + +public abstract class JavaScriptSource extends ScriptSource { + + public static final String ENGINE = "com.stardust.autojs.script.JavaScriptSource.Engine"; + + public static final String EXECUTION_MODE_UI_PREFIX = "\"ui\";"; + + public static final int EXECUTION_MODE_NORMAL = 0; + public static final int EXECUTION_MODE_UI = 0x00000001; + public static final int EXECUTION_MODE_AUTO = 0x00000002; + + private static final String LOG_TAG = "JavaScriptSource"; + + private static final Map EXECUTION_MODES = new MapBuilder() + .put("ui", EXECUTION_MODE_UI) + .put("auto", EXECUTION_MODE_AUTO) + .build(); + private static final int PARSING_MAX_TOKEN = 300; + + private int mExecutionMode = -1; + + public JavaScriptSource(String name) { + super(name); + } + + @NonNull + public abstract String getScript(); + + @Nullable + public abstract Reader getScriptReader(); + + @NonNull + public Reader getNonNullScriptReader() { + Reader reader = getScriptReader(); + if (reader == null) { + return new StringReader(getScript()); + } + return reader; + } + + public String toString() { + return getName() + ".js"; + } + + + public int getExecutionMode() { + if (mExecutionMode == -1) { + mExecutionMode = parseExecutionMode(); + } + return mExecutionMode; + } + + protected int parseExecutionMode() { + String script = getScript(); + TokenStream ts = new TokenStream(new StringReader(script), null, 1); + int token; + int count = 0; + try { + while (count <= PARSING_MAX_TOKEN && (token = ts.getToken()) != Token.EOF) { + count++; + if (token == Token.EOL || token == Token.COMMENT) { + continue; + } + if (token == Token.STRING && ts.getTokenLength() > 2) { + String tokenString = script.substring(ts.getTokenBeg() + 1, ts.getTokenEnd() - 1); + if (ts.getToken() != Token.SEMI) { + break; + } + Log.d(LOG_TAG, "string = " + tokenString); + return parseExecutionMode(tokenString.split(" ")); + } + break; + } + } catch (Exception e) { + e.printStackTrace(); + return EXECUTION_MODE_NORMAL; + } + return EXECUTION_MODE_NORMAL; + + } + + private int parseExecutionMode(String[] modeStrings) { + int mode = 0; + for (String modeString : modeStrings) { + Integer i = EXECUTION_MODES.get(modeString); + if (i != null) { + mode |= i; + } + } + return mode; + } + + @Override + public String getEngineName() { + return ENGINE; + } + + +} diff --git a/app/src/main/java/com/stardust/autojs/script/JsBeautifier.java b/app/src/main/java/com/stardust/autojs/script/JsBeautifier.java new file mode 100644 index 00000000..c5b90a7e --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/script/JsBeautifier.java @@ -0,0 +1,129 @@ +package com.stardust.autojs.script; + +import android.content.Context; +import android.view.View; + +import com.stardust.autojs.engine.module.AssetAndUrlModuleSourceProvider; +import com.stardust.pio.PFiles; +import com.stardust.pio.UncheckedIOException; + +import org.mozilla.javascript.Function; +import org.mozilla.javascript.ImporterTopLevel; +import org.mozilla.javascript.Scriptable; +import org.mozilla.javascript.commonjs.module.RequireBuilder; +import org.mozilla.javascript.commonjs.module.provider.SoftCachingModuleScriptProvider; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Created by Stardust on 2017/4/12. + */ + +public class JsBeautifier { + + + public interface Callback { + + void onSuccess(String beautifiedCode); + + void onException(Exception e); + } + + private ExecutorService mExecutor = Executors.newSingleThreadExecutor(); + private Context mContext; + private Function mJsBeautifyFunction; + private org.mozilla.javascript.Context mScriptContext; + private Scriptable mScriptable; + private final String mBeautifyJsPath; + private final String mBeautifyJsDir; + private View mView; + + public JsBeautifier(View view, String beautifyJsDirPath) { + mContext = view.getContext(); + mView = view; + mBeautifyJsDir = beautifyJsDirPath; + mBeautifyJsPath = PFiles.join(beautifyJsDirPath, "beautify.js"); + } + + public void beautify(final String code, final Callback callback) { + mExecutor.execute(() -> { + try { + prepareIfNeeded(); + enterContext(); + Object beautifiedCode = mJsBeautifyFunction.call(mScriptContext, mScriptable, mScriptable, new Object[]{code}); + mView.post(() -> callback.onSuccess(beautifiedCode.toString())); + } catch (Exception e) { + mView.post(() -> callback.onException(e)); + } finally { + exitContext(); + } + }); + } + + private void exitContext() { + if (mScriptContext != null) { + org.mozilla.javascript.Context.exit(); + mScriptContext = null; + } + } + + private void enterContext() { + if (mScriptContext != null) { + return; + } + mScriptContext = org.mozilla.javascript.Context.enter(); + mScriptContext.setLanguageVersion(org.mozilla.javascript.Context.VERSION_1_8); + mScriptContext.setOptimizationLevel(-1); + if (mScriptable == null) { + ImporterTopLevel importerTopLevel = new ImporterTopLevel(); + importerTopLevel.initStandardObjects(mScriptContext, false); + mScriptable = importerTopLevel; + } + AssetAndUrlModuleSourceProvider provider = new AssetAndUrlModuleSourceProvider(mContext, mBeautifyJsDir, + Collections.singletonList(new File("/").toURI())); + new RequireBuilder() + .setModuleScriptProvider(new SoftCachingModuleScriptProvider(provider)) + .setSandboxed(false) + .createRequire(mScriptContext, mScriptable) + .install(mScriptable); + } + + public void prepare() { + mExecutor.execute(() -> { + try { + prepareIfNeeded(); + } catch (Exception e) { + e.printStackTrace(); + } + }); + } + + + private void prepareIfNeeded() { + if (mJsBeautifyFunction != null) + return; + compile(); + } + + private void compile() { + try { + enterContext(); + InputStream is = mContext.getAssets().open(mBeautifyJsPath); + mJsBeautifyFunction = (Function) mScriptContext.evaluateString(mScriptable, PFiles.read(is), "", 1, null); + } catch (IOException e) { + exitContext(); + throw new UncheckedIOException(e); + } + } + + public void shutdown(){ + mExecutor.shutdownNow(); + mView = null; + } + +} diff --git a/app/src/main/java/com/stardust/autojs/script/ScriptSource.java b/app/src/main/java/com/stardust/autojs/script/ScriptSource.java new file mode 100644 index 00000000..b66d3b74 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/script/ScriptSource.java @@ -0,0 +1,22 @@ +package com.stardust.autojs.script; + +import java.io.Serializable; + +/** + * Created by Stardust on 2017/4/2. + */ + +public abstract class ScriptSource implements Serializable { + + private String mName; + + public ScriptSource(String name) { + mName = name; + } + + public String getName() { + return mName; + } + + public abstract String getEngineName(); +} diff --git a/app/src/main/java/com/stardust/autojs/script/SequenceScriptSource.java b/app/src/main/java/com/stardust/autojs/script/SequenceScriptSource.java new file mode 100644 index 00000000..f555d0bf --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/script/SequenceScriptSource.java @@ -0,0 +1,54 @@ +package com.stardust.autojs.script; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.stardust.io.ConcatReader; + +import java.io.Reader; +import java.io.StringReader; + +/** + * Created by Stardust on 2017/4/2. + */ + +public class SequenceScriptSource extends JavaScriptSource { + + private String mScript; + private JavaScriptSource mSecondScriptSource; + private JavaScriptSource mFirstScriptSource; + + + public SequenceScriptSource(String name, JavaScriptSource firstScriptSource, JavaScriptSource secondScriptSource) { + super(name); + mSecondScriptSource = secondScriptSource; + mFirstScriptSource = firstScriptSource; + } + + @NonNull + @Override + public String getScript() { + concatScriptsIfNeeded(); + return mScript; + } + + private void concatScriptsIfNeeded() { + if (mScript != null) + return; + mScript = mFirstScriptSource.getScript() + mSecondScriptSource.getScript(); + } + + @Nullable + @Override + public Reader getScriptReader() { + if (mScript != null) + return new StringReader(mScript); + return new ConcatReader(mFirstScriptSource.getNonNullScriptReader(), mSecondScriptSource.getNonNullScriptReader()); + } + + @Override + public String toString() { + return mSecondScriptSource.toString(); + } + +} diff --git a/app/src/main/java/com/stardust/autojs/script/StringScriptSource.java b/app/src/main/java/com/stardust/autojs/script/StringScriptSource.java new file mode 100644 index 00000000..f8b55778 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/script/StringScriptSource.java @@ -0,0 +1,37 @@ +package com.stardust.autojs.script; + + +import androidx.annotation.Nullable; + +import java.io.Reader; + +/** + * Created by Stardust on 2017/4/2. + */ + +public class StringScriptSource extends JavaScriptSource { + + private String mScript; + + public StringScriptSource(String script) { + super("Tmp"); + mScript = script; + } + + public StringScriptSource(String name, String script) { + super(name); + mScript = script; + } + + @Override + public String getScript() { + return mScript; + } + + @Nullable + @Override + public Reader getScriptReader() { + return null; + } + +} diff --git a/app/src/main/java/com/stardust/autojs/util/FloatingPermission.java b/app/src/main/java/com/stardust/autojs/util/FloatingPermission.java new file mode 100644 index 00000000..1072b638 --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/util/FloatingPermission.java @@ -0,0 +1,107 @@ +package com.stardust.autojs.util; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.provider.Settings; +import androidx.annotation.RequiresApi; +import android.text.TextUtils; +import android.widget.Toast; + +import org.autojs.autojs6.R; +import org.autojs.autojs.ui.enhancedfloaty.util.FloatingWindowPermissionUtil; + +import java.lang.reflect.Method; + +import ezy.assist.compat.RomUtil; +import ezy.assist.compat.SettingsCompat; + +/** + * Created by Stardust on 2018/1/30. + */ + +public class FloatingPermission { + + + private static final int OP_SYSTEM_ALERT_WINDOW = 24; + private static Method sCheckOp; + + static { + try { + sCheckOp = SettingsCompat.class.getDeclaredMethod("checkOp", Context.class, int.class); + sCheckOp.setAccessible(true); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } + } + + public static boolean ensurePermissionGranted(Context context) { + if (!canDrawOverlays(context)) { + Toast.makeText(context, R.string.error_no_display_over_other_apps_permission, Toast.LENGTH_SHORT).show(); + manageDrawOverlays(context); + return false; + } + return true; + } + + public static void waitForPermissionGranted(Context context) throws InterruptedException { + if (canDrawOverlays(context)) { + return; + } + Runnable r = () -> { + manageDrawOverlays(context); + Toast.makeText(context, R.string.error_no_display_over_other_apps_permission, Toast.LENGTH_SHORT).show(); + }; + if (Looper.myLooper() != Looper.getMainLooper()) { + new Handler(Looper.getMainLooper()).post(r); + } else { + r.run(); + } + while (true) { + if (canDrawOverlays(context)) + return; + Thread.sleep(200); + } + + } + + + public static void manageDrawOverlays(Context context) { + try { + if (RomUtil.isMiui() && TextUtils.equals("V10", RomUtil.getVersion()) + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + manageDrawOverlaysForAndroidM(context); + } else { + SettingsCompat.manageDrawOverlays(context); + } + } catch (Exception ex) { + FloatingWindowPermissionUtil.goToAppDetailSettings(context, context.getPackageName()); + } + } + + @RequiresApi(api = Build.VERSION_CODES.M) + public static void manageDrawOverlaysForAndroidM(Context context) { + Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION); + intent.setData(Uri.parse("package:" + context.getPackageName())); + context.startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + } + + public static boolean canDrawOverlays(Context context) { + return SettingsCompat.canDrawOverlays(context); + } + + private static boolean checkOp(Context context, int op) { + if (sCheckOp == null) { + return SettingsCompat.canDrawOverlays(context); + } + try { + return (boolean) sCheckOp.invoke(null, context, op); + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } +} diff --git a/app/src/main/java/com/stardust/autojs/util/ProcessUtils.java b/app/src/main/java/com/stardust/autojs/util/ProcessUtils.java new file mode 100644 index 00000000..365c4c1d --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/util/ProcessUtils.java @@ -0,0 +1,29 @@ +package com.stardust.autojs.util; + +import android.util.Log; + +import com.stardust.autojs.core.util.ProcessShell; + +import java.lang.reflect.Field; + +/** + * Created by Stardust on 2017/8/3. + */ + +public class ProcessUtils { + + + private static final String LOG_TAG = "ProcessUtils"; + + public static int getProcessPid(Process process) { + try { + Field pid = process.getClass().getDeclaredField("pid"); + pid.setAccessible(true); + return (int) pid.get(process); + } catch (Exception e) { + e.printStackTrace(); + return -1; + } + } + +} diff --git a/app/src/main/java/com/stardust/autojs/workground/WrapContentLinearLayoutManager.java b/app/src/main/java/com/stardust/autojs/workground/WrapContentLinearLayoutManager.java new file mode 100644 index 00000000..02bed10e --- /dev/null +++ b/app/src/main/java/com/stardust/autojs/workground/WrapContentLinearLayoutManager.java @@ -0,0 +1,36 @@ +package com.stardust.autojs.workground; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; + +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +/** + * Created by Stardust on 2017/2/13. + * http://stackoverflow.com/questions/31759171/recyclerview-and-java-lang-indexoutofboundsexception-inconsistency-detected-in + */ + +public class WrapContentLinearLayoutManager extends LinearLayoutManager { + public WrapContentLinearLayoutManager(Context context) { + super(context); + } + + public WrapContentLinearLayoutManager(Context context, int orientation, boolean reverseLayout) { + super(context, orientation, reverseLayout); + } + + public WrapContentLinearLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { + try { + super.onLayoutChildren(recycler, state); + } catch (IndexOutOfBoundsException e) { + Log.e("LinearLayoutManager", "Android bug ", e); + } + } +} diff --git a/app/src/main/java/com/stardust/automator/ActionArgument.kt b/app/src/main/java/com/stardust/automator/ActionArgument.kt new file mode 100644 index 00000000..160f79c1 --- /dev/null +++ b/app/src/main/java/com/stardust/automator/ActionArgument.kt @@ -0,0 +1,34 @@ +package com.stardust.automator + +import android.os.Bundle + +/** + * Created by Stardust on 2017/3/9. + */ + +abstract class ActionArgument private constructor(protected val mKey: String) { + + abstract fun putIn(bundle: Bundle) + + class IntActionArgument(name: String, private val mInt: Int) : ActionArgument(name) { + + override fun putIn(bundle: Bundle) { + bundle.putInt(mKey, mInt) + } + } + + class CharSequenceActionArgument(name: String, private val mCharSequence: CharSequence) : ActionArgument(name) { + + override fun putIn(bundle: Bundle) { + bundle.putCharSequence(mKey, mCharSequence) + } + } + + class FloatActionArgument(name: String, private val mFloat: Float) : ActionArgument(name) { + + override fun putIn(bundle: Bundle) { + bundle.putFloat(mKey, mFloat) + } + } + +} diff --git a/app/src/main/java/com/stardust/automator/BuildConfig.kt b/app/src/main/java/com/stardust/automator/BuildConfig.kt new file mode 100644 index 00000000..90cb7fa3 --- /dev/null +++ b/app/src/main/java/com/stardust/automator/BuildConfig.kt @@ -0,0 +1,3 @@ +package com.stardust.automator + +typealias BuildConfig = org.autojs.autojs6.BuildConfig diff --git a/app/src/main/java/com/stardust/automator/GlobalActionAutomator.kt b/app/src/main/java/com/stardust/automator/GlobalActionAutomator.kt new file mode 100644 index 00000000..3f6eb1cb --- /dev/null +++ b/app/src/main/java/com/stardust/automator/GlobalActionAutomator.kt @@ -0,0 +1,171 @@ +package com.stardust.automator + +import android.accessibilityservice.AccessibilityService +import android.accessibilityservice.GestureDescription +import android.graphics.Path +import android.os.Handler +import android.os.Looper +import android.view.ViewConfiguration +import com.stardust.concurrent.VolatileBox +import com.stardust.concurrent.VolatileDispose +import com.stardust.util.ScreenMetrics + +/** + * Created by Stardust on 2017/5/16. + */ + +class GlobalActionAutomator(private val mHandler: Handler?, private val serviceProvider: () -> AccessibilityService) { + + private val service: AccessibilityService + get() = serviceProvider() + + private var mScreenMetrics: ScreenMetrics? = null + + fun setScreenMetrics(screenMetrics: ScreenMetrics?) { + mScreenMetrics = screenMetrics + } + + fun back(): Boolean { + return performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK) + } + + fun home(): Boolean { + return performGlobalAction(AccessibilityService.GLOBAL_ACTION_HOME) + } + + fun powerDialog(): Boolean { + return performGlobalAction(AccessibilityService.GLOBAL_ACTION_POWER_DIALOG) + } + + private fun performGlobalAction(globalAction: Int): Boolean { + return service.performGlobalAction(globalAction) + } + + fun notifications(): Boolean { + return performGlobalAction(AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS) + } + + fun quickSettings(): Boolean { + return performGlobalAction(AccessibilityService.GLOBAL_ACTION_QUICK_SETTINGS) + } + + fun recents(): Boolean { + return performGlobalAction(AccessibilityService.GLOBAL_ACTION_RECENTS) + } + + fun splitScreen(): Boolean { + return performGlobalAction(AccessibilityService.GLOBAL_ACTION_TOGGLE_SPLIT_SCREEN) + } + + fun gesture(start: Long, duration: Long, vararg points: IntArray): Boolean { + val path = pointsToPath(points) + return gestures(GestureDescription.StrokeDescription(path, start, duration)) + } + + private fun pointsToPath(points: Array): Path { + val path = Path() + path.moveTo(scaleX(points[0][0]).toFloat(), scaleY(points[0][1]).toFloat()) + for (i in 1 until points.size) { + val point = points[i] + path.lineTo(scaleX(point[0]).toFloat(), scaleY(point[1]).toFloat()) + } + return path + } + + fun gestureAsync(start: Long, duration: Long, vararg points: IntArray) { + val path = pointsToPath(points) + gesturesAsync(GestureDescription.StrokeDescription(path, start, duration)) + } + + fun gestures(vararg strokes: GestureDescription.StrokeDescription): Boolean { + val builder = GestureDescription.Builder() + for (stroke in strokes) { + builder.addStroke(stroke) + } + val handler = mHandler + return if (handler == null) { + gesturesWithoutHandler(builder.build()) + } else { + gesturesWithHandler(handler, builder.build()) + } + } + + private fun gesturesWithHandler(handler: Handler, description: GestureDescription): Boolean { + val result = VolatileDispose() + service.dispatchGesture(description, object : AccessibilityService.GestureResultCallback() { + override fun onCompleted(gestureDescription: GestureDescription) { + result.setAndNotify(true) + } + + override fun onCancelled(gestureDescription: GestureDescription) { + result.setAndNotify(false) + } + }, handler) + return result.blockedGet() + } + + private fun gesturesWithoutHandler(description: GestureDescription): Boolean { + prepareLooperIfNeeded() + val result = VolatileBox(false) + @Suppress("DEPRECATION") val handler = Looper.myLooper()?.let { + Handler() + } + service.dispatchGesture(description, object : AccessibilityService.GestureResultCallback() { + override fun onCompleted(gestureDescription: GestureDescription) { + result.set(true) + quitLoop() + } + + override fun onCancelled(gestureDescription: GestureDescription) { + result.set(false) + quitLoop() + } + }, handler) + Looper.loop() + return result.get() + } + + fun gesturesAsync(vararg strokes: GestureDescription.StrokeDescription) { + val builder = GestureDescription.Builder() + for (stroke in strokes) { + builder.addStroke(stroke) + } + service.dispatchGesture(builder.build(), null, null) + } + + private fun quitLoop() { + val looper = Looper.myLooper() + looper?.quit() + } + + private fun prepareLooperIfNeeded() { + if (Looper.myLooper() == null) { + Looper.prepare() + } + } + + fun click(x: Int, y: Int): Boolean { + return press(x, y, ViewConfiguration.getTapTimeout() + 50) + } + + fun press(x: Int, y: Int, delay: Int): Boolean { + return gesture(0, delay.toLong(), intArrayOf(x, y)) + } + + fun longClick(x: Int, y: Int): Boolean { + return gesture(0, (ViewConfiguration.getLongPressTimeout() + 200).toLong(), intArrayOf(x, y)) + } + + private fun scaleX(x: Int): Int { + return mScreenMetrics?.scaleX(x) ?: x + } + + private fun scaleY(y: Int): Int { + return mScreenMetrics?.scaleX(y) ?: y + } + + fun swipe(x1: Int, y1: Int, x2: Int, y2: Int, delay: Long): Boolean { + return gesture(0, delay, intArrayOf(x1, y1), intArrayOf(x2, y2)) + } + +} diff --git a/app/src/main/java/com/stardust/automator/UiGlobalSelector.kt b/app/src/main/java/com/stardust/automator/UiGlobalSelector.kt new file mode 100644 index 00000000..51758b63 --- /dev/null +++ b/app/src/main/java/com/stardust/automator/UiGlobalSelector.kt @@ -0,0 +1,440 @@ +package com.stardust.automator + +import android.graphics.Rect +import android.os.Build +import com.stardust.automator.filter.* +import com.stardust.automator.search.BFS +import com.stardust.automator.search.DFS +import com.stardust.automator.search.SearchAlgorithm +import java.lang.IllegalArgumentException + +/** + * Created by Stardust on 2017/3/8. + */ + +open class UiGlobalSelector { + + private val mSelector = Selector() + private var mSearchAlgorithm: SearchAlgorithm = DFS + + //// 第一类筛选条件 + + open fun id(id: String): UiGlobalSelector { + mSelector.add(IdFilter.equals(id)) + return this + } + + fun idContains(str: String): UiGlobalSelector { + mSelector.add(IdFilter.contains(str)) + return this + } + + open fun idStartsWith(prefix: String): UiGlobalSelector { + mSelector.add(IdFilter.startsWith(prefix)) + return this + } + + fun idEndsWith(suffix: String): UiGlobalSelector { + mSelector.add(IdFilter.endsWith(suffix)) + return this + } + + open fun idMatches(regex: String): UiGlobalSelector { + mSelector.add(IdFilter.matches(regex)) + return this + } + + fun text(text: String): UiGlobalSelector { + mSelector.add(TextFilters.equals(text)) + return this + } + + fun textContains(str: String): UiGlobalSelector { + mSelector.add(TextFilters.contains(str)) + return this + } + + fun textStartsWith(prefix: String): UiGlobalSelector { + mSelector.add(TextFilters.startsWith(prefix)) + return this + } + + fun textEndsWith(suffix: String): UiGlobalSelector { + mSelector.add(TextFilters.endsWith(suffix)) + return this + } + + open fun textMatches(regex: String): UiGlobalSelector { + mSelector.add(TextFilters.matches(regex)) + return this + } + + fun desc(desc: String): UiGlobalSelector { + mSelector.add(DescFilters.equals(desc)) + return this + } + + fun descContains(str: String): UiGlobalSelector { + mSelector.add(DescFilters.contains(str)) + return this + } + + fun descStartsWith(prefix: String): UiGlobalSelector { + mSelector.add(DescFilters.startsWith(prefix)) + return this + } + + fun descEndsWith(suffix: String): UiGlobalSelector { + mSelector.add(DescFilters.endsWith(suffix)) + return this + } + + open fun descMatches(regex: String): UiGlobalSelector { + mSelector.add(DescFilters.matches(regex)) + return this + } + + fun className(className: String): UiGlobalSelector { + mSelector.add(ClassNameFilters.equals(className)) + return this + } + + fun classNameContains(str: String): UiGlobalSelector { + mSelector.add(ClassNameFilters.contains(str)) + return this + } + + fun classNameStartsWith(prefix: String): UiGlobalSelector { + mSelector.add(ClassNameFilters.startsWith(prefix)) + return this + } + + fun classNameEndsWith(suffix: String): UiGlobalSelector { + mSelector.add(ClassNameFilters.endsWith(suffix)) + return this + } + + open fun classNameMatches(regex: String): UiGlobalSelector { + mSelector.add(ClassNameFilters.matches(regex)) + return this + } + + fun packageName(packageName: String): UiGlobalSelector { + mSelector.add(PackageNameFilter.equals(packageName)) + return this + } + + fun packageNameContains(str: String): UiGlobalSelector { + mSelector.add(PackageNameFilter.contains(str)) + return this + } + + fun packageNameStartsWith(prefix: String): UiGlobalSelector { + mSelector.add(PackageNameFilter.startsWith(prefix)) + return this + } + + fun packageNameEndsWith(suffix: String): UiGlobalSelector { + mSelector.add(PackageNameFilter.endsWith(suffix)) + return this + } + + open fun packageNameMatches(regex: String): UiGlobalSelector { + mSelector.add(PackageNameFilter.matches(regex)) + return this + } + + fun bounds(l: Int, t: Int, r: Int, b: Int): UiGlobalSelector { + mSelector.add(BoundsFilter(Rect(l, t, r, b), BoundsFilter.TYPE_EQUALS)) + return this + } + + fun boundsInside(l: Int, t: Int, r: Int, b: Int): UiGlobalSelector { + mSelector.add(BoundsFilter(Rect(l, t, r, b), BoundsFilter.TYPE_INSIDE)) + return this + } + + fun boundsContains(l: Int, t: Int, r: Int, b: Int): UiGlobalSelector { + mSelector.add(BoundsFilter(Rect(l, t, r, b), BoundsFilter.TYPE_CONTAINS)) + return this + } + + fun drawingOrder(order: Int): UiGlobalSelector { + mSelector.add(object : Filter { + override fun filter(node: UiObject): Boolean { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && node.drawingOrder == order + } + + override fun toString(): String { + return "drawingOrder($order)" + } + }) + return this + } + + //// 第二类筛选条件 -able + + fun checkable(b: Boolean): UiGlobalSelector { + mSelector.add(BooleanFilter[BooleanFilter.CHECKABLE, b]) + return this + } + + fun checked(b: Boolean): UiGlobalSelector { + mSelector.add(BooleanFilter[BooleanFilter.CHECKED, b]) + return this + } + + fun focusable(b: Boolean): UiGlobalSelector { + mSelector.add(BooleanFilter[BooleanFilter.FOCUSABLE, b]) + return this + } + + fun focused(b: Boolean): UiGlobalSelector { + mSelector.add(BooleanFilter[BooleanFilter.FOCUSED, b]) + return this + } + + fun visibleToUser(b: Boolean): UiGlobalSelector { + mSelector.add(BooleanFilter[BooleanFilter.VISIBLE_TO_USER, b]) + return this + } + + fun accessibilityFocused(b: Boolean): UiGlobalSelector { + mSelector.add(BooleanFilter[BooleanFilter.ACCESSIBILITY_FOCUSED, b]) + return this + } + + fun selected(b: Boolean): UiGlobalSelector { + mSelector.add(BooleanFilter[BooleanFilter.SELECTED, b]) + return this + } + + fun clickable(b: Boolean): UiGlobalSelector { + mSelector.add(BooleanFilter[BooleanFilter.CLICKABLE, b]) + return this + } + + fun longClickable(b: Boolean): UiGlobalSelector { + mSelector.add(BooleanFilter[BooleanFilter.LONG_CLICKABLE, b]) + return this + } + + fun enabled(b: Boolean): UiGlobalSelector { + mSelector.add(BooleanFilter[BooleanFilter.ENABLED, b]) + return this + } + + fun password(b: Boolean): UiGlobalSelector { + mSelector.add(BooleanFilter[BooleanFilter.PASSWORD, b]) + return this + } + + fun scrollable(b: Boolean): UiGlobalSelector { + mSelector.add(BooleanFilter[BooleanFilter.SCROLLABLE, b]) + return this + } + + fun editable(b: Boolean): UiGlobalSelector { + mSelector.add(BooleanFilter[BooleanFilter.EDITABLE, b]) + return this + } + + fun contentInvalid(b: Boolean): UiGlobalSelector { + mSelector.add(BooleanFilter[BooleanFilter.CONTENT_INVALID, b]) + return this + } + + fun contextClickable(b: Boolean): UiGlobalSelector { + mSelector.add(BooleanFilter[BooleanFilter.CONTEXT_CLICKABLE, b]) + return this + } + + fun multiLine(b: Boolean): UiGlobalSelector { + mSelector.add(BooleanFilter[BooleanFilter.MULTI_LINE, b]) + return this + } + + fun dismissable(b: Boolean): UiGlobalSelector { + mSelector.add(BooleanFilter[BooleanFilter.DISMISSABLE, b]) + return this + } + + fun checkable(): UiGlobalSelector { + mSelector.add(BooleanFilter[BooleanFilter.CHECKABLE, true]) + return this + } + + fun checked(): UiGlobalSelector { + mSelector.add(BooleanFilter[BooleanFilter.CHECKED, true]) + return this + } + + fun focusable(): UiGlobalSelector { + mSelector.add(BooleanFilter[BooleanFilter.FOCUSABLE, true]) + return this + } + + fun focused(): UiGlobalSelector { + mSelector.add(BooleanFilter[BooleanFilter.FOCUSED, true]) + return this + } + + fun visibleToUser(): UiGlobalSelector { + mSelector.add(BooleanFilter[BooleanFilter.VISIBLE_TO_USER, true]) + return this + } + + fun accessibilityFocused(): UiGlobalSelector { + mSelector.add(BooleanFilter[BooleanFilter.ACCESSIBILITY_FOCUSED, true]) + return this + } + + fun selected(): UiGlobalSelector { + mSelector.add(BooleanFilter[BooleanFilter.SELECTED, true]) + return this + } + + fun clickable(): UiGlobalSelector { + mSelector.add(BooleanFilter[BooleanFilter.CLICKABLE, true]) + return this + } + + fun longClickable(): UiGlobalSelector { + mSelector.add(BooleanFilter[BooleanFilter.LONG_CLICKABLE, true]) + return this + } + + fun enabled(): UiGlobalSelector { + mSelector.add(BooleanFilter[BooleanFilter.ENABLED, true]) + return this + } + + fun password(): UiGlobalSelector { + mSelector.add(BooleanFilter[BooleanFilter.PASSWORD, true]) + return this + } + + fun scrollable(): UiGlobalSelector { + mSelector.add(BooleanFilter[BooleanFilter.SCROLLABLE, true]) + return this + } + + fun editable(): UiGlobalSelector { + mSelector.add(BooleanFilter[BooleanFilter.EDITABLE, true]) + return this + } + + fun contentInvalid(): UiGlobalSelector { + mSelector.add(BooleanFilter[BooleanFilter.CONTENT_INVALID, true]) + return this + } + + fun contextClickable(): UiGlobalSelector { + mSelector.add(BooleanFilter[BooleanFilter.CONTEXT_CLICKABLE, true]) + return this + } + + fun multiLine(): UiGlobalSelector { + mSelector.add(BooleanFilter[BooleanFilter.MULTI_LINE, true]) + return this + } + + fun dismissable(): UiGlobalSelector { + mSelector.add(BooleanFilter[BooleanFilter.DISMISSABLE, true]) + return this + } + + + //第三类 int + fun depth(d: Int): UiGlobalSelector { + mSelector.add(IntFilter(IntFilter.DEPTH, d)) + return this + } + + fun row(d: Int): UiGlobalSelector { + mSelector.add(IntFilter(IntFilter.ROW, d)) + return this + } + + fun rowCount(d: Int): UiGlobalSelector { + mSelector.add(IntFilter(IntFilter.ROW_COUNT, d)) + return this + } + + fun rowSpan(d: Int): UiGlobalSelector { + mSelector.add(IntFilter(IntFilter.ROW_SPAN, d)) + return this + } + + fun column(d: Int): UiGlobalSelector { + mSelector.add(IntFilter(IntFilter.COLUMN, d)) + return this + } + + fun columnCount(d: Int): UiGlobalSelector { + mSelector.add(IntFilter(IntFilter.COLUMN_COUNT, d)) + return this + } + + fun columnSpan(d: Int): UiGlobalSelector { + mSelector.add(IntFilter(IntFilter.COLUMN_SPAN, d)) + return this + } + + fun indexInParent(index: Int): UiGlobalSelector { + mSelector.add(IntFilter(IntFilter.INDEX_IN_PARENT, index)) + return this + } + + + fun filter(filter: BooleanFilter.BooleanSupplier): UiGlobalSelector { + mSelector.add(object : Filter { + override fun filter(node: UiObject): Boolean { + return filter[node] + } + }) + return this + } + + fun findOf(node: UiObject, max: Int): UiObjectCollection { + return UiObjectCollection.of(findAndReturnList(node, max)) + } + + fun findOf(node: UiObject): UiObjectCollection { + return findOf(node, Int.MAX_VALUE) + } + + + fun findOneOf(node: UiObject): UiObject? { + val collection = findOf(node, 1) + return if (collection.size() == 0) { + null + } else collection[0] + } + + fun addFilter(filter: Filter): UiGlobalSelector { + mSelector.add(filter) + return this + } + + fun algorithm(algorithm: String): UiGlobalSelector { + if (algorithm.equals("BFS", true)) { + mSearchAlgorithm = BFS + return this + } + if (algorithm.equals("DFS", true)) { + mSearchAlgorithm = DFS + return this + } + throw IllegalArgumentException("unknown algorithm: $algorithm") + } + + fun findAndReturnList(node: UiObject, max: Int = Int.MAX_VALUE): List { + return mSearchAlgorithm.search(node, mSelector, max) + } + + + override fun toString(): String { + return mSelector.toString() + } +} diff --git a/app/src/main/java/com/stardust/automator/UiObject.kt b/app/src/main/java/com/stardust/automator/UiObject.kt new file mode 100644 index 00000000..5eedffac --- /dev/null +++ b/app/src/main/java/com/stardust/automator/UiObject.kt @@ -0,0 +1,420 @@ +package com.stardust.automator + +import android.graphics.Rect +import android.os.Bundle +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat +import android.util.Log +import android.view.accessibility.AccessibilityNodeInfo + +import com.stardust.view.accessibility.AccessibilityNodeInfoAllocator +import com.stardust.view.accessibility.AccessibilityNodeInfoHelper + +import java.util.ArrayList +import java.util.Arrays + +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CONTEXT_CLICK +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_DOWN +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_LEFT +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_RIGHT +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_UP +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SET_PROGRESS +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SHOW_ON_SCREEN + +/** + * Created by Stardust on 2017/3/9. + */ + +@Suppress("DEPRECATION") +open class UiObject(info: Any?, private val allocator: AccessibilityNodeInfoAllocator?, depth: Int, private val mIndexInParent: Int) : AccessibilityNodeInfoCompat(info) { + + + private var mStackTrace = "" + private var mDepth = 0 + + val isHierarchically: Boolean + get() = collectionInfo != null && collectionInfo.isHierarchical + + init { + mDepth = depth + if (DEBUG) + mStackTrace = Arrays.toString(Thread.currentThread().stackTrace) + + } + + + constructor(info: Any?, allocator: AccessibilityNodeInfoAllocator, indexInParent: Int) : this(info, allocator, 0, indexInParent) {} + + @JvmOverloads + constructor(info: Any?, depth: Int = 0, indexInParent: Int = -1) : this(info, null, depth, indexInParent) + + open fun parent(): UiObject? { + try { + val parent = super.getParent() ?: return null + return UiObject(parent.info, mDepth - 1, -1) + } catch (e: IllegalStateException) { + // FIXME: 2017/5/5 + return null + } + + } + + open fun child(i: Int): UiObject? { + try { + val child = super.getChild(i) ?: return null + return UiObject(child.info, mDepth + 1, i) + } catch (e: IllegalStateException) { + // FIXME: 2017/5/5 + return null + } + + } + + fun indexInParent(): Int { + return mIndexInParent + } + + fun find(selector: UiGlobalSelector): UiObjectCollection { + return selector.findOf(this) + } + + fun findOne(selector: UiGlobalSelector): UiObject? { + return selector.findOneOf(this) + } + + fun children(): UiObjectCollection { + val list = ArrayList(childCount) + for (i in 0 until childCount) { + list.add(child(i)) + } + return UiObjectCollection.of(list) + } + + open fun childCount(): Int { + return childCount + } + + open fun bounds(): Rect { + return AccessibilityNodeInfoHelper.getBoundsInScreen(this) + } + + open fun boundsInParent(): Rect { + return AccessibilityNodeInfoHelper.getBoundsInParent(this) + } + + open fun drawingOrder(): Int { + return drawingOrder + } + + open fun id(): String? { + return viewIdResourceName + } + + open fun text(): String { + val t = text + return t?.toString() ?: "" + } + + override fun getText(): CharSequence? { + return if (isPassword) { + "" + } else super.getText() + } + + open fun desc(): String? { + val d = contentDescription + return d?.toString() + } + + open fun className(): String? { + val d = className + return d?.toString() + } + + open fun packageName(): String? { + val d = packageName + return d?.toString() + } + + open fun depth(): Int { + return mDepth + } + + fun performAction(action: Int, vararg arguments: ActionArgument): Boolean { + val bundle = argumentsToBundle(arguments) + return performAction(action, bundle) + } + + override fun performAction(action: Int, bundle: Bundle): Boolean { + return try { + super.performAction(action, bundle) + } catch (e: IllegalStateException) { + // FIXME: 2017/5/5 + false + } + + } + + override fun performAction(action: Int): Boolean { + return try { + super.performAction(action) + } catch (e: IllegalStateException) { + // FIXME: 2017/5/5 + false + } + } + + fun click(): Boolean { + return performAction(AccessibilityNodeInfoCompat.ACTION_CLICK) + } + + fun longClick(): Boolean { + return performAction(AccessibilityNodeInfoCompat.ACTION_LONG_CLICK) + } + + fun accessibilityFocus(): Boolean { + return performAction(AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS) + } + + fun clearAccessibilityFocus(): Boolean { + return performAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS) + } + + fun focus(): Boolean { + return performAction(AccessibilityNodeInfoCompat.ACTION_FOCUS) + } + + fun clearFocus(): Boolean { + return performAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_FOCUS) + } + + fun copy(): Boolean { + return performAction(AccessibilityNodeInfoCompat.ACTION_COPY) + } + + fun paste(): Boolean { + return performAction(AccessibilityNodeInfoCompat.ACTION_PASTE) + } + + fun select(): Boolean { + return performAction(AccessibilityNodeInfoCompat.ACTION_SELECT) + } + + fun cut(): Boolean { + return performAction(AccessibilityNodeInfoCompat.ACTION_CUT) + } + + fun collapse(): Boolean { + return performAction(AccessibilityNodeInfoCompat.ACTION_COLLAPSE) + } + + fun expand(): Boolean { + return performAction(AccessibilityNodeInfoCompat.ACTION_EXPAND) + } + + fun dismiss(): Boolean { + return performAction(AccessibilityNodeInfoCompat.ACTION_DISMISS) + } + + fun show(): Boolean { + return performAction(ACTION_SHOW_ON_SCREEN.id) + } + + fun scrollForward(): Boolean { + return performAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD) + } + + fun scrollBackward(): Boolean { + return performAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD) + } + + fun scrollUp(): Boolean { + return performAction(ACTION_SCROLL_UP.id) + } + + fun scrollDown(): Boolean { + return performAction(ACTION_SCROLL_DOWN.id) + } + + fun scrollLeft(): Boolean { + return performAction(ACTION_SCROLL_LEFT.id) + } + + fun scrollRight(): Boolean { + return performAction(ACTION_SCROLL_RIGHT.id) + } + + fun contextClick(): Boolean { + return performAction(ACTION_CONTEXT_CLICK.id) + } + + fun setSelection(s: Int, e: Int): Boolean { + return performAction(AccessibilityNodeInfoCompat.ACTION_SET_SELECTION, + ActionArgument.IntActionArgument(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_SELECTION_START_INT, s), + ActionArgument.IntActionArgument(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_SELECTION_END_INT, e)) + } + + fun setText(text: String): Boolean { + return performAction(AccessibilityNodeInfoCompat.ACTION_SET_TEXT, + ActionArgument.CharSequenceActionArgument(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text)) + } + + fun setProgress(value: Float): Boolean { + return performAction(ACTION_SET_PROGRESS.id, + ActionArgument.FloatActionArgument(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_PROGRESS_VALUE, value)) + } + + fun scrollTo(row: Int, column: Int): Boolean { + return performAction(ACTION_SCROLL_TO_POSITION.id, + ActionArgument.IntActionArgument(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_ROW_INT, row), + ActionArgument.IntActionArgument(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_COLUMN_INT, column)) + } + + override fun getChild(index: Int): AccessibilityNodeInfoCompat { + return if (allocator == null) super.getChild(index) else allocator.getChild(this, index) + } + + override fun getParent(): AccessibilityNodeInfoCompat { + return if (allocator == null) super.getParent() else allocator.getParent(this) + } + + + open fun checkable(): Boolean { + return isCheckable + } + + + open fun checked(): Boolean { + return isChecked + } + + + open fun focusable(): Boolean { + return isFocusable + } + + + open fun focused(): Boolean { + return isFocused + } + + + open fun visibleToUser(): Boolean { + return isVisibleToUser + } + + + open fun accessibilityFocused(): Boolean { + return isAccessibilityFocused + } + + + open fun selected(): Boolean { + return isSelected + } + + open fun clickable(): Boolean { + return isClickable + } + + + open fun longClickable(): Boolean { + return isLongClickable + } + + + open fun enabled(): Boolean { + return isEnabled + } + + + fun password(): Boolean { + return isPassword + } + + + open fun scrollable(): Boolean { + return isScrollable + } + + open fun row(): Int { + return if (collectionItemInfo == null) -1 else collectionItemInfo.rowIndex + } + + open fun column(): Int { + return if (collectionItemInfo == null) -1 else collectionItemInfo.columnIndex + } + + open fun rowSpan(): Int { + return if (collectionItemInfo == null) -1 else collectionItemInfo.rowSpan + } + + + open fun columnSpan(): Int { + return if (collectionItemInfo == null) -1 else collectionItemInfo.columnSpan + } + + open fun rowCount(): Int { + return if (collectionInfo == null) 0 else collectionInfo.rowCount + } + + + open fun columnCount(): Int { + return if (collectionInfo == null) 0 else collectionInfo.columnCount + } + + override fun findAccessibilityNodeInfosByText(text: String): List { + return allocator?.findAccessibilityNodeInfosByText(this, text) + ?: super.findAccessibilityNodeInfosByText(text) + } + + fun findByText(text: String): List { + return UiGlobalSelector().textContains(text).findAndReturnList(this) + } + + override fun findAccessibilityNodeInfosByViewId(viewId: String): List { + return allocator?.findAccessibilityNodeInfosByViewId(this, viewId) + ?: super.findAccessibilityNodeInfosByViewId(viewId) + } + + fun findByViewId(viewId: String): List { + return UiGlobalSelector().id(viewId).findAndReturnList(this) + } + + @Deprecated("Deprecated in Java") + override fun recycle() { + try { + super.recycle() + } catch (e: Exception) { + Log.w(TAG, mStackTrace, e) + } + + } + + companion object { + + val ACTION_APPEND_TEXT = 0x00200001 + + private const val TAG = "UiObject" + private const val DEBUG = false + + + fun createRoot(root: AccessibilityNodeInfo): UiObject { + return UiObject(root, null, 0, -1) + } + + fun createRoot(root: AccessibilityNodeInfo, allocator: AccessibilityNodeInfoAllocator?): UiObject { + return UiObject(root, allocator, 0, -1) + } + + private fun argumentsToBundle(arguments: Array): Bundle { + val bundle = Bundle() + for (arg in arguments) { + arg.putIn(bundle) + } + return bundle + } + } + + +} diff --git a/app/src/main/java/com/stardust/automator/UiObjectCollection.kt b/app/src/main/java/com/stardust/automator/UiObjectCollection.kt new file mode 100644 index 00000000..712cccfa --- /dev/null +++ b/app/src/main/java/com/stardust/automator/UiObjectCollection.kt @@ -0,0 +1,263 @@ +package com.stardust.automator + +import android.os.Bundle + +import com.stardust.util.Consumer + +import java.util.ArrayList +import java.util.Collections + +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ARGUMENT_COLUMN_INT +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ARGUMENT_PROGRESS_VALUE +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ARGUMENT_ROW_INT +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ARGUMENT_SELECTION_END_INT +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ARGUMENT_SELECTION_START_INT +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_CLEAR_FOCUS +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_CLICK +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_COLLAPSE +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_COPY +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_CUT +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_DISMISS +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_EXPAND +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_FOCUS +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_LONG_CLICK +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_PASTE +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_SELECT +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_SET_SELECTION +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_SET_TEXT +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CONTEXT_CLICK +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_DOWN +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_LEFT +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_RIGHT +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_UP +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SET_PROGRESS +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SHOW_ON_SCREEN + +/** + * Created by Stardust on 2017/3/9. + */ + +class UiObjectCollection private constructor(private val mNodes: List) { + + + val isEmpty: Boolean + get() = mNodes.isEmpty() + + fun toArray(): Array { + return mNodes.toTypedArray() + } + + fun performAction(action: Int): Boolean { + var fail = false + for (node in mNodes) { + val succeed = node?.performAction(action) ?: false + if (!succeed) { + fail = true + } + } + return !fail + } + + fun performAction(action: Int, vararg arguments: ActionArgument): Boolean { + var fail = false + val bundle = argumentsToBundle(arguments) + for (node in mNodes) { + val succeed = node?.performAction(action, bundle) ?: false + if (succeed) { + fail = true + } + } + return !fail + } + + private fun argumentsToBundle(arguments: Array): Bundle { + val bundle = Bundle() + for (arg in arguments) { + arg.putIn(bundle) + } + return bundle + } + + fun click(): Boolean { + return performAction(ACTION_CLICK) + } + + fun longClick(): Boolean { + return performAction(ACTION_LONG_CLICK) + } + + fun accessibilityFocus(): Boolean { + return performAction(ACTION_ACCESSIBILITY_FOCUS) + } + + fun clearAccessibilityFocus(): Boolean { + return performAction(ACTION_CLEAR_ACCESSIBILITY_FOCUS) + } + + fun focus(): Boolean { + return performAction(ACTION_FOCUS) + } + + fun clearFocus(): Boolean { + return performAction(ACTION_CLEAR_FOCUS) + } + + fun copy(): Boolean { + return performAction(ACTION_COPY) + } + + fun paste(): Boolean { + return performAction(ACTION_PASTE) + } + + fun select(): Boolean { + return performAction(ACTION_SELECT) + } + + fun cut(): Boolean { + return performAction(ACTION_CUT) + } + + fun collapse(): Boolean { + return performAction(ACTION_COLLAPSE) + } + + fun expand(): Boolean { + return performAction(ACTION_EXPAND) + } + + fun dismiss(): Boolean { + return performAction(ACTION_DISMISS) + } + + fun show(): Boolean { + return performAction(ACTION_SHOW_ON_SCREEN.id) + } + + fun scrollForward(): Boolean { + return performAction(ACTION_SCROLL_FORWARD) + } + + fun scrollBackward(): Boolean { + return performAction(ACTION_SCROLL_BACKWARD) + } + + fun scrollUp(): Boolean { + return performAction(ACTION_SCROLL_UP.id) + } + + fun scrollDown(): Boolean { + return performAction(ACTION_SCROLL_DOWN.id) + } + + fun scrollLeft(): Boolean { + return performAction(ACTION_SCROLL_LEFT.id) + } + + fun scrollRight(): Boolean { + return performAction(ACTION_SCROLL_RIGHT.id) + } + + fun contextClick(): Boolean { + return performAction(ACTION_CONTEXT_CLICK.id) + } + + fun setSelection(s: Int, e: Int): Boolean { + return performAction(ACTION_SET_SELECTION, + ActionArgument.IntActionArgument(ACTION_ARGUMENT_SELECTION_START_INT, s), + ActionArgument.IntActionArgument(ACTION_ARGUMENT_SELECTION_END_INT, e)) + } + + fun setText(text: CharSequence): Boolean { + return performAction(ACTION_SET_TEXT, + ActionArgument.CharSequenceActionArgument(ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text)) + } + + fun setProgress(value: Float): Boolean { + return performAction(ACTION_SET_PROGRESS.id, + ActionArgument.FloatActionArgument(ACTION_ARGUMENT_PROGRESS_VALUE, value)) + + } + + fun scrollTo(row: Int, column: Int): Boolean { + return performAction(ACTION_SCROLL_TO_POSITION.id, + ActionArgument.IntActionArgument(ACTION_ARGUMENT_ROW_INT, row), + ActionArgument.IntActionArgument(ACTION_ARGUMENT_COLUMN_INT, column)) + } + + operator fun get(i: Int): UiObject? { + return mNodes[i] + } + + fun indexOf(o: UiObject?): Int { + return mNodes.indexOf(o) + } + + + fun lastIndexOf(o: UiObject?): Int { + return mNodes.lastIndexOf(o) + } + + fun size(): Int { + return mNodes.size + } + + + operator fun contains(o: UiObject?): Boolean { + return mNodes.contains(o) + } + + operator fun iterator(): Iterator { + return mNodes.iterator() + } + + fun each(consumer: Consumer): UiObjectCollection { + for (uiObject in mNodes) { + consumer.accept(uiObject) + } + return this + } + + fun find(selector: UiGlobalSelector): UiObjectCollection { + val list = ArrayList() + for (`object` in mNodes) { + `object`?.let { + list.addAll(selector.findOf(it).mNodes) + } + } + return of(list) + } + + fun findOne(selector: UiGlobalSelector): UiObject? { + for (`object` in mNodes) { + val result = `object`?.run { selector.findOneOf(this) } + if (result != null) + return result + } + return null + } + + fun empty(): Boolean { + return size() == 0 + } + + fun nonEmpty(): Boolean { + return size() != 0 + } + + companion object { + + val EMPTY = UiObjectCollection.of(emptyList()) + + fun of(list: List): UiObjectCollection { + return UiObjectCollection(list) + } + } + +} diff --git a/app/src/main/java/com/stardust/automator/filter/BooleanFilter.kt b/app/src/main/java/com/stardust/automator/filter/BooleanFilter.kt new file mode 100644 index 00000000..dea6c533 --- /dev/null +++ b/app/src/main/java/com/stardust/automator/filter/BooleanFilter.kt @@ -0,0 +1,234 @@ +package com.stardust.automator.filter + +import android.os.Build +import androidx.annotation.RequiresApi + +import com.stardust.automator.UiObject + +import java.util.HashMap + +/** + * Created by Stardust on 2017/3/9. + */ + +class BooleanFilter(private val mBooleanSupplier: BooleanSupplier, private val mExceptedValue: Boolean) : Filter { + + interface BooleanSupplier { + + operator fun get(node: UiObject): Boolean + + } + + override fun filter(node: UiObject): Boolean { + return mBooleanSupplier[node] == mExceptedValue + } + + override fun toString(): String { + return mBooleanSupplier.toString() + "(" + mExceptedValue + ")" + } + + companion object { + + private val cache = HashMap>() + + operator fun get(supplier: BooleanSupplier, b: Boolean): BooleanFilter { + var booleanFilters: Array? = cache[supplier] + if (booleanFilters == null) { + booleanFilters = Array(2) { + BooleanFilter(supplier, it != 0) + } + cache[supplier] = booleanFilters + } + val i = if (b) 1 else 0 + return booleanFilters[i] + } + + val CHECKABLE: BooleanSupplier = object : BooleanSupplier { + + override fun get(node: UiObject): Boolean { + return node.isCheckable + } + + override fun toString(): String { + return "checkable" + } + } + + val CHECKED: BooleanSupplier = object : BooleanSupplier { + + override fun get(node: UiObject): Boolean { + return node.isChecked + } + + override fun toString(): String { + return "checked" + } + } + + val FOCUSABLE: BooleanSupplier = object : BooleanSupplier { + + override fun get(node: UiObject): Boolean { + return node.isFocusable + } + + override fun toString(): String { + return "focusable" + } + } + + val FOCUSED: BooleanSupplier = object : BooleanSupplier { + + override fun get(node: UiObject): Boolean { + return node.isFocused + } + + override fun toString(): String { + return "focused" + } + } + + val VISIBLE_TO_USER: BooleanSupplier = object : BooleanSupplier { + + override fun get(node: UiObject): Boolean { + return node.isVisibleToUser + } + + override fun toString(): String { + return "visibleToUser" + } + } + + val ACCESSIBILITY_FOCUSED: BooleanSupplier = object : BooleanSupplier { + + override fun get(node: UiObject): Boolean { + return node.isAccessibilityFocused + } + + override fun toString(): String { + return "accessibilityFocused" + } + } + + val SELECTED: BooleanSupplier = object : BooleanSupplier { + + override fun get(node: UiObject): Boolean { + return node.isSelected + } + + override fun toString(): String { + return "selected" + } + } + + val CLICKABLE: BooleanSupplier = object : BooleanSupplier { + + override fun get(node: UiObject): Boolean { + return node.isClickable + } + + override fun toString(): String { + return "clickable" + } + } + + val LONG_CLICKABLE: BooleanSupplier = object : BooleanSupplier { + + override fun get(node: UiObject): Boolean { + return node.isLongClickable + } + + override fun toString(): String { + return "longClickable" + } + } + + val ENABLED: BooleanSupplier = object : BooleanSupplier { + + override fun get(node: UiObject): Boolean { + return node.isEnabled + } + + override fun toString(): String { + return "enabled" + } + } + + + val PASSWORD: BooleanSupplier = object : BooleanSupplier { + + override fun get(node: UiObject): Boolean { + return node.isPassword + } + + override fun toString(): String { + return "password" + } + } + + val SCROLLABLE: BooleanSupplier = object : BooleanSupplier { + + override fun get(node: UiObject): Boolean { + return node.isScrollable + } + + override fun toString(): String { + return "scrollable" + } + } + + val EDITABLE: BooleanSupplier = object : BooleanSupplier { + + override fun get(node: UiObject): Boolean { + return node.isEditable + } + + override fun toString(): String { + return "editable" + } + } + val CONTENT_INVALID: BooleanSupplier = object : BooleanSupplier { + + override fun get(node: UiObject): Boolean { + return node.isContentInvalid + } + + override fun toString(): String { + return "contentInvalid" + } + } + + val CONTEXT_CLICKABLE: BooleanSupplier = object : BooleanSupplier { + + @RequiresApi(api = Build.VERSION_CODES.M) + override fun get(node: UiObject): Boolean { + return node.isContextClickable + } + + override fun toString(): String { + return "checkable" + } + } + + val MULTI_LINE: BooleanSupplier = object : BooleanSupplier { + + override fun get(node: UiObject): Boolean { + return node.isMultiLine + } + + override fun toString(): String { + return "multiLine" + } + } + + val DISMISSABLE: BooleanSupplier = object : BooleanSupplier { + + override fun get(node: UiObject): Boolean { + return node.isDismissable + } + + override fun toString(): String { + return "dismissable" + } + } + } +} diff --git a/app/src/main/java/com/stardust/automator/filter/BoundsFilter.kt b/app/src/main/java/com/stardust/automator/filter/BoundsFilter.kt new file mode 100644 index 00000000..1a351e2f --- /dev/null +++ b/app/src/main/java/com/stardust/automator/filter/BoundsFilter.kt @@ -0,0 +1,39 @@ +package com.stardust.automator.filter + +import android.graphics.Rect + +import com.stardust.automator.UiObject +import com.stardust.view.accessibility.AccessibilityNodeInfoHelper + +import java.util.Locale + +/** + * Created by Stardust on 2017/3/9. + */ + +class BoundsFilter(private val mBounds: Rect, private val mType: Int) : Filter { + + override fun filter(node: UiObject): Boolean { + if (mType == TYPE_CONTAINS) { + return AccessibilityNodeInfoHelper.getBoundsInScreen(node).contains(mBounds) + } + val boundsInScreen = AccessibilityNodeInfoHelper.getBoundsInScreen(node) + return if (mType == TYPE_EQUALS) boundsInScreen == mBounds else mBounds.contains(boundsInScreen) + } + + override fun toString(): String { + return String.format(Locale.getDefault(), "bounds%s(%d, %d, %d, %d)", when (mType) { + TYPE_EQUALS -> "" + TYPE_INSIDE -> "Inside" + else -> "Contains" + }, + mBounds.left, mBounds.top, mBounds.right, mBounds.bottom) + } + + companion object { + + const val TYPE_EQUALS = 0 + const val TYPE_INSIDE = 1 + const val TYPE_CONTAINS = 2 + } +} diff --git a/app/src/main/java/com/stardust/automator/filter/ClassNameFilters.kt b/app/src/main/java/com/stardust/automator/filter/ClassNameFilters.kt new file mode 100644 index 00000000..1b4de4c6 --- /dev/null +++ b/app/src/main/java/com/stardust/automator/filter/ClassNameFilters.kt @@ -0,0 +1,45 @@ +package com.stardust.automator.filter + +import com.stardust.automator.UiObject + +/** + * Created by Stardust on 2017/3/9. + */ + +object ClassNameFilters { + + private val CLASS_NAME_GETTER = object : KeyGetter { + override fun getKey(nodeInfo: UiObject): String? { + val charSequence = nodeInfo.className + return charSequence?.toString() + } + + override fun toString(): String { + return "className" + } + } + + fun equals(text: String): Filter { + var className = text + if (!className.contains(".")) { + className = "android.widget.$className" + } + return StringEqualsFilter(className, CLASS_NAME_GETTER) + } + + fun contains(str: String): Filter { + return StringContainsFilter(str, CLASS_NAME_GETTER) + } + + fun startsWith(prefix: String): Filter { + return StringStartsWithFilter(prefix, CLASS_NAME_GETTER) + } + + fun endsWith(suffix: String): Filter { + return StringEndsWithFilter(suffix, CLASS_NAME_GETTER) + } + + fun matches(regex: String): Filter { + return StringMatchesFilter(regex, CLASS_NAME_GETTER) + } +} diff --git a/app/src/main/java/com/stardust/automator/filter/DescFilters.kt b/app/src/main/java/com/stardust/automator/filter/DescFilters.kt new file mode 100644 index 00000000..6cf29e1a --- /dev/null +++ b/app/src/main/java/com/stardust/automator/filter/DescFilters.kt @@ -0,0 +1,41 @@ +package com.stardust.automator.filter + +import com.stardust.automator.UiObject + +/** + * Created by Stardust on 2017/3/9. + */ + +object DescFilters { + + private val DESC_GETTER = object : KeyGetter { + override fun getKey(nodeInfo: UiObject): String? { + val charSequence = nodeInfo.contentDescription + return charSequence?.toString() + } + + override fun toString(): String { + return "desc" + } + } + + fun equals(text: String): Filter { + return StringEqualsFilter(text, DESC_GETTER) + } + + fun contains(str: String): Filter { + return StringContainsFilter(str, DESC_GETTER) + } + + fun startsWith(prefix: String): Filter { + return StringStartsWithFilter(prefix, DESC_GETTER) + } + + fun endsWith(suffix: String): Filter { + return StringEndsWithFilter(suffix, DESC_GETTER) + } + + fun matches(regex: String): Filter { + return StringMatchesFilter(regex, DESC_GETTER) + } +} diff --git a/app/src/main/java/com/stardust/automator/filter/Filter.kt b/app/src/main/java/com/stardust/automator/filter/Filter.kt new file mode 100644 index 00000000..53804119 --- /dev/null +++ b/app/src/main/java/com/stardust/automator/filter/Filter.kt @@ -0,0 +1,13 @@ +package com.stardust.automator.filter + +import com.stardust.automator.UiObject + +/** + * Created by Stardust on 2017/3/9. + */ + +interface Filter { + + fun filter(node: UiObject): Boolean + +} diff --git a/app/src/main/java/com/stardust/automator/filter/IdFilter.kt b/app/src/main/java/com/stardust/automator/filter/IdFilter.kt new file mode 100644 index 00000000..4592cb46 --- /dev/null +++ b/app/src/main/java/com/stardust/automator/filter/IdFilter.kt @@ -0,0 +1,42 @@ +package com.stardust.automator.filter + +import com.stardust.automator.UiObject + +/** + * Created by Stardust on 2017/3/9. + */ + +object IdFilter { + + private val ID_GETTER = object : KeyGetter { + + override fun getKey(nodeInfo: UiObject): String? { + return nodeInfo.viewIdResourceName + } + + override fun toString(): String { + return "id" + } + } + + fun equals(id: String): StringEqualsFilter { + return StringEqualsFilter(id, ID_GETTER) + } + + fun startsWith(prefix: String): StringStartsWithFilter { + return StringStartsWithFilter(prefix, ID_GETTER) + } + + fun endsWith(suffix: String): StringEndsWithFilter { + return StringEndsWithFilter(suffix, ID_GETTER) + } + + fun contains(contains: String): StringContainsFilter { + return StringContainsFilter(contains, ID_GETTER) + } + + fun matches(regex: String): StringMatchesFilter { + return StringMatchesFilter(regex, ID_GETTER) + } + +} diff --git a/app/src/main/java/com/stardust/automator/filter/IntFilter.kt b/app/src/main/java/com/stardust/automator/filter/IntFilter.kt new file mode 100644 index 00000000..638ed761 --- /dev/null +++ b/app/src/main/java/com/stardust/automator/filter/IntFilter.kt @@ -0,0 +1,105 @@ +package com.stardust.automator.filter + +import com.stardust.automator.UiObject + +/** + * Created by Stardust on 2017/11/5. + */ + +class IntFilter(private val mIntProperty: IntProperty, private val mValue: Int) : Filter { + + interface IntProperty { + operator fun get(`object`: UiObject): Int + } + + override fun filter(node: UiObject): Boolean { + return mIntProperty[node] == mValue + } + + override fun toString(): String { + return mIntProperty.toString() + "(" + mValue + ")" + } + + companion object { + + val DEPTH: IntProperty = object : IntProperty { + override fun get(`object`: UiObject): Int { + return `object`.depth() + } + + override fun toString(): String { + return "depth" + } + } + + val ROW: IntProperty = object : IntProperty { + override fun get(`object`: UiObject): Int { + return `object`.row() + } + + override fun toString(): String { + return "row" + } + } + + val ROW_COUNT: IntProperty = object : IntProperty { + override fun get(`object`: UiObject): Int { + return `object`.rowCount() + } + + override fun toString(): String { + return "rowCount" + } + } + + val ROW_SPAN: IntProperty = object : IntProperty { + override fun get(`object`: UiObject): Int { + return `object`.rowSpan() + } + + override fun toString(): String { + return "rowSpan" + } + } + + val COLUMN: IntProperty = object : IntProperty { + override fun get(`object`: UiObject): Int { + return `object`.column() + } + + override fun toString(): String { + return "column" + } + } + + val COLUMN_COUNT: IntProperty = object : IntProperty { + override fun get(`object`: UiObject): Int { + return `object`.columnCount() + } + + override fun toString(): String { + return "columnCount" + } + } + + val COLUMN_SPAN: IntProperty = object : IntProperty { + override fun get(`object`: UiObject): Int { + return `object`.columnSpan() + } + + override fun toString(): String { + return "columnSpan" + } + } + + val INDEX_IN_PARENT: IntProperty = object : IntProperty { + override fun get(`object`: UiObject): Int { + return `object`.indexInParent() + } + + override fun toString(): String { + return "indexInParent" + } + } + } +} diff --git a/app/src/main/java/com/stardust/automator/filter/KeyGetter.kt b/app/src/main/java/com/stardust/automator/filter/KeyGetter.kt new file mode 100644 index 00000000..17e8c432 --- /dev/null +++ b/app/src/main/java/com/stardust/automator/filter/KeyGetter.kt @@ -0,0 +1,12 @@ +package com.stardust.automator.filter + +import com.stardust.automator.UiObject + +/** + * Created by Stardust on 2017/3/9. + */ + +interface KeyGetter { + + fun getKey(nodeInfo: UiObject): String? +} diff --git a/app/src/main/java/com/stardust/automator/filter/PackageNameFilter.kt b/app/src/main/java/com/stardust/automator/filter/PackageNameFilter.kt new file mode 100644 index 00000000..8ca2ca4f --- /dev/null +++ b/app/src/main/java/com/stardust/automator/filter/PackageNameFilter.kt @@ -0,0 +1,41 @@ +package com.stardust.automator.filter + +import com.stardust.automator.UiObject + +/** + * Created by Stardust on 2017/3/9. + */ + +object PackageNameFilter { + + private val PACKAGE_NAME_GETTER = object : KeyGetter { + override fun getKey(nodeInfo: UiObject): String? { + val charSequence = nodeInfo.packageName + return charSequence?.toString() + } + + override fun toString(): String { + return "packageName" + } + } + + fun equals(text: String): Filter { + return StringEqualsFilter(text, PACKAGE_NAME_GETTER) + } + + fun contains(str: String): Filter { + return StringContainsFilter(str, PACKAGE_NAME_GETTER) + } + + fun startsWith(prefix: String): Filter { + return StringStartsWithFilter(prefix, PACKAGE_NAME_GETTER) + } + + fun endsWith(suffix: String): Filter { + return StringEndsWithFilter(suffix, PACKAGE_NAME_GETTER) + } + + fun matches(regex: String): Filter { + return StringMatchesFilter(regex, PACKAGE_NAME_GETTER) + } +} diff --git a/app/src/main/java/com/stardust/automator/filter/Selector.kt b/app/src/main/java/com/stardust/automator/filter/Selector.kt new file mode 100644 index 00000000..5b14bc12 --- /dev/null +++ b/app/src/main/java/com/stardust/automator/filter/Selector.kt @@ -0,0 +1,33 @@ +package com.stardust.automator.filter + +import com.stardust.automator.UiObject +import java.util.* + +class Selector : Filter { + private val mFilters = LinkedList() + + override fun filter(node: UiObject): Boolean { + for (filter in mFilters) { + if (!filter.filter(node)) { + return false + } + } + return true + } + + fun add(filter: Filter) { + mFilters.add(filter) + } + + override fun toString(): String { + val str = StringBuilder() + for (filter in mFilters) { + str.append(filter.toString()).append(".") + } + if (str.isNotEmpty()) { + str.deleteCharAt(str.length - 1) + } + return str.toString() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/stardust/automator/filter/StringContainsFilter.kt b/app/src/main/java/com/stardust/automator/filter/StringContainsFilter.kt new file mode 100644 index 00000000..7406e5c6 --- /dev/null +++ b/app/src/main/java/com/stardust/automator/filter/StringContainsFilter.kt @@ -0,0 +1,19 @@ +package com.stardust.automator.filter + +import com.stardust.automator.UiObject + +/** + * Created by Stardust on 2017/3/9. + */ + +class StringContainsFilter internal constructor(private val mContains: String, private val mKeyGetter: KeyGetter) : Filter { + + override fun filter(node: UiObject): Boolean { + val key = mKeyGetter.getKey(node) + return key != null && key.contains(mContains) + } + + override fun toString(): String { + return mKeyGetter.toString() + "Contains(\"" + mContains + "\")" + } +} diff --git a/app/src/main/java/com/stardust/automator/filter/StringEndsWithFilter.kt b/app/src/main/java/com/stardust/automator/filter/StringEndsWithFilter.kt new file mode 100644 index 00000000..7e040c40 --- /dev/null +++ b/app/src/main/java/com/stardust/automator/filter/StringEndsWithFilter.kt @@ -0,0 +1,19 @@ +package com.stardust.automator.filter + +import com.stardust.automator.UiObject + +/** + * Created by Stardust on 2017/3/9. + */ + +class StringEndsWithFilter(private val mSuffix: String, private val mKeyGetter: KeyGetter) : Filter { + + override fun filter(node: UiObject): Boolean { + val key = mKeyGetter.getKey(node) + return key != null && key.endsWith(mSuffix) + } + + override fun toString(): String { + return mKeyGetter.toString() + "EndsWith(\"" + mSuffix + "\")" + } +} diff --git a/app/src/main/java/com/stardust/automator/filter/StringEqualsFilter.kt b/app/src/main/java/com/stardust/automator/filter/StringEqualsFilter.kt new file mode 100644 index 00000000..bc70123e --- /dev/null +++ b/app/src/main/java/com/stardust/automator/filter/StringEqualsFilter.kt @@ -0,0 +1,21 @@ +package com.stardust.automator.filter + +import com.stardust.automator.UiObject + +/** + * Created by Stardust on 2017/3/9. + */ + +class StringEqualsFilter(private val mValue: String, private val mKeyGetter: KeyGetter) : Filter { + + override fun filter(node: UiObject): Boolean { + val key = mKeyGetter.getKey(node) + return if (key != null) { + key == mValue + } else false + } + + override fun toString(): String { + return mKeyGetter.toString() + "(\"" + mValue + "\")" + } +} diff --git a/app/src/main/java/com/stardust/automator/filter/StringMatchesFilter.kt b/app/src/main/java/com/stardust/automator/filter/StringMatchesFilter.kt new file mode 100644 index 00000000..325ed168 --- /dev/null +++ b/app/src/main/java/com/stardust/automator/filter/StringMatchesFilter.kt @@ -0,0 +1,19 @@ +package com.stardust.automator.filter + +import com.stardust.automator.UiObject + +/** + * Created by Stardust on 2017/3/9. + */ + +class StringMatchesFilter internal constructor(private val mRegex: String, private val mKeyGetter: KeyGetter) : Filter { + + override fun filter(node: UiObject): Boolean { + val key = mKeyGetter.getKey(node) + return key != null && key.matches(mRegex.toRegex()) + } + + override fun toString(): String { + return mKeyGetter.toString() + "Matches(\"" + mRegex + "\")" + } +} diff --git a/app/src/main/java/com/stardust/automator/filter/StringStartsWithFilter.kt b/app/src/main/java/com/stardust/automator/filter/StringStartsWithFilter.kt new file mode 100644 index 00000000..d4be471b --- /dev/null +++ b/app/src/main/java/com/stardust/automator/filter/StringStartsWithFilter.kt @@ -0,0 +1,19 @@ +package com.stardust.automator.filter + +import com.stardust.automator.UiObject + +/** + * Created by Stardust on 2017/3/9. + */ + +class StringStartsWithFilter(private val mPrefix: String, private val mKeyGetter: KeyGetter) : Filter { + + override fun filter(node: UiObject): Boolean { + val key = mKeyGetter.getKey(node) + return key != null && key.startsWith(mPrefix) + } + + override fun toString(): String { + return mKeyGetter.toString() + "StartsWith(\"" + mPrefix + "\")" + } +} diff --git a/app/src/main/java/com/stardust/automator/filter/TextFilters.kt b/app/src/main/java/com/stardust/automator/filter/TextFilters.kt new file mode 100644 index 00000000..ad4eb847 --- /dev/null +++ b/app/src/main/java/com/stardust/automator/filter/TextFilters.kt @@ -0,0 +1,41 @@ +package com.stardust.automator.filter + +import com.stardust.automator.UiObject + +/** + * Created by Stardust on 2017/3/9. + */ + +object TextFilters { + + private val TEXT_GETTER = object : KeyGetter { + override fun getKey(nodeInfo: UiObject): String? { + val charSequence = nodeInfo.text + return charSequence?.toString() + } + + override fun toString(): String { + return "text" + } + } + + fun equals(text: String): Filter { + return StringEqualsFilter(text, TEXT_GETTER) + } + + fun contains(str: String): Filter { + return StringContainsFilter(str, TEXT_GETTER) + } + + fun startsWith(prefix: String): Filter { + return StringStartsWithFilter(prefix, TEXT_GETTER) + } + + fun endsWith(suffix: String): Filter { + return StringEndsWithFilter(suffix, TEXT_GETTER) + } + + fun matches(regex: String): Filter { + return StringMatchesFilter(regex, TEXT_GETTER) + } +} diff --git a/app/src/main/java/com/stardust/automator/search/BFS.kt b/app/src/main/java/com/stardust/automator/search/BFS.kt new file mode 100644 index 00000000..771babc2 --- /dev/null +++ b/app/src/main/java/com/stardust/automator/search/BFS.kt @@ -0,0 +1,35 @@ +@file:Suppress("DEPRECATION") + +package com.stardust.automator.search + +import com.stardust.automator.UiObject +import com.stardust.automator.filter.Filter +import java.util.* +import kotlin.collections.ArrayList + +object BFS : SearchAlgorithm { + + override fun search(root: UiObject, filter: Filter, limit: Int): ArrayList { + val result = ArrayList() + val queue = ArrayDeque() + queue.add(root) + while (!queue.isEmpty()) { + val top = queue.poll() + if (top == null) continue + val isTarget = filter.filter(top) + if (isTarget) { + result.add(top) + if (result.size > limit) { + return result + } + } + for (i in 0 until top.childCount) { + queue.add(top.child(i) ?: continue) + } + if (!isTarget && top !== root) { + top.recycle() + } + } + return result + } +} \ No newline at end of file diff --git a/app/src/main/java/com/stardust/automator/search/DFS.kt b/app/src/main/java/com/stardust/automator/search/DFS.kt new file mode 100644 index 00000000..9ba860dc --- /dev/null +++ b/app/src/main/java/com/stardust/automator/search/DFS.kt @@ -0,0 +1,40 @@ +@file:Suppress("DEPRECATION") + +package com.stardust.automator.search + +import com.stardust.automator.UiObject +import com.stardust.automator.filter.Filter +import java.util.* + +import kotlin.collections.ArrayList + +/** + * Created by Stardust on 2017/3/9. + */ + +object DFS : SearchAlgorithm { + + override fun search(root: UiObject, filter: Filter, limit: Int): ArrayList { + val result = ArrayList() + val stack = LinkedList() + stack.push(root) + while (stack.isNotEmpty()) { + val parent = stack.pop() + for (i in parent.childCount - 1 downTo 0) { + val child = parent.child(i) ?: continue + stack.push(child) + } + if (filter.filter(parent)) { + result.add(parent) + if (result.size >= limit) { + break + } + } else { + if (parent !== root) { + parent.recycle() + } + } + } + return result + } +} diff --git a/app/src/main/java/com/stardust/automator/search/SearchAlgorithm.kt b/app/src/main/java/com/stardust/automator/search/SearchAlgorithm.kt new file mode 100644 index 00000000..81206ecb --- /dev/null +++ b/app/src/main/java/com/stardust/automator/search/SearchAlgorithm.kt @@ -0,0 +1,9 @@ +package com.stardust.automator.search + +import com.stardust.automator.UiObject +import com.stardust.automator.filter.Filter + +interface SearchAlgorithm { + + fun search(root: UiObject, filter: Filter, limit: Int = Int.MAX_VALUE): ArrayList +} \ No newline at end of file diff --git a/app/src/main/java/com/stardust/automator/simple_action/Able.kt b/app/src/main/java/com/stardust/automator/simple_action/Able.kt new file mode 100644 index 00000000..345a4523 --- /dev/null +++ b/app/src/main/java/com/stardust/automator/simple_action/Able.kt @@ -0,0 +1,46 @@ +package com.stardust.automator.simple_action + +import android.view.accessibility.AccessibilityNodeInfo +import com.stardust.automator.UiObject +import com.stardust.util.SparseArrayEntries + +/** + * Created by Stardust on 2017/1/27. + */ + +interface Able { + + fun isAble(node: UiObject): Boolean + + companion object { + + val ABLE_MAP = SparseArrayEntries() + .entry(AccessibilityNodeInfo.ACTION_CLICK, object : Able { + override fun isAble(nodeInfo: UiObject): Boolean { + return nodeInfo.isClickable + } + }) + .entry(AccessibilityNodeInfo.ACTION_LONG_CLICK, object : Able { + override fun isAble(nodeInfo: UiObject): Boolean { + return nodeInfo.isLongClickable + } + }) + .entry(AccessibilityNodeInfo.ACTION_FOCUS, object : Able { + override fun isAble(nodeInfo: UiObject): Boolean { + return nodeInfo.isFocusable + } + }) + .entry(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD, object : Able { + override fun isAble(nodeInfo: UiObject): Boolean { + return nodeInfo.isScrollable + } + }) + .entry(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD, object : Able { + override fun isAble(nodeInfo: UiObject): Boolean { + return nodeInfo.isScrollable + } + }) + .sparseArray() + } + +} diff --git a/app/src/main/java/com/stardust/automator/simple_action/ActionFactory.kt b/app/src/main/java/com/stardust/automator/simple_action/ActionFactory.kt new file mode 100644 index 00000000..0135866a --- /dev/null +++ b/app/src/main/java/com/stardust/automator/simple_action/ActionFactory.kt @@ -0,0 +1,69 @@ +package com.stardust.automator.simple_action + +import android.graphics.Rect +import android.os.Build +import android.os.Bundle +import androidx.annotation.RequiresApi +import android.view.accessibility.AccessibilityNodeInfo + +import com.stardust.automator.UiObject +import com.stardust.util.MapBuilder + +/** + * Created by Stardust on 2017/1/27. + */ + +object ActionFactory { + + private val searchUpAction = MapBuilder() + .put(AccessibilityNodeInfo.ACTION_CLICK, null) + .put(AccessibilityNodeInfo.ACTION_LONG_CLICK, null) + .put(AccessibilityNodeInfo.ACTION_SELECT, null) + .put(AccessibilityNodeInfo.ACTION_FOCUS, null) + .put(AccessibilityNodeInfo.ACTION_SELECT, null) + .put(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD, null) + .put(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD, null) + .build() + + fun createActionWithTextFilter(action: Int, text: String, index: Int): SimpleAction { + return if (searchUpAction.containsKey(action)) + SearchUpTargetAction(action, FilterAction.TextFilter(text, index)) + else + DepthFirstSearchTargetAction(action, FilterAction.TextFilter(text, index)) + } + + fun createActionWithBoundsFilter(action: Int, rect: Rect): SimpleAction { + return if (searchUpAction.containsKey(action)) + SearchUpTargetAction(action, FilterAction.BoundsFilter(rect)) + else + DepthFirstSearchTargetAction(action, FilterAction.BoundsFilter(rect)) + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + fun createActionWithEditableFilter(action: Int, index: Int, text: String): SimpleAction { + return object : SearchTargetAction(action, FilterAction.EditableFilter(index)) { + + override fun performAction(node: UiObject): Boolean { + val args = Bundle() + if (action == AccessibilityNodeInfo.ACTION_SET_TEXT) { + args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text) + } else { + args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, node.text() + text) + } + return node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args) + } + } + } + + fun createScrollMaxAction(action: Int): SimpleAction { + return ScrollMaxAction(action) + } + + fun createScrollAction(action: Int, i: Int): SimpleAction { + return ScrollAction(action, i) + } + + fun createActionWithIdFilter(action: Int, id: String): SimpleAction { + return FilterAction.SimpleFilterAction(action, FilterAction.IdFilter(id)) + } +} diff --git a/app/src/main/java/com/stardust/automator/simple_action/ActionTarget.kt b/app/src/main/java/com/stardust/automator/simple_action/ActionTarget.kt new file mode 100644 index 00000000..89a7b72b --- /dev/null +++ b/app/src/main/java/com/stardust/automator/simple_action/ActionTarget.kt @@ -0,0 +1,43 @@ +package com.stardust.automator.simple_action + +import android.graphics.Rect +import android.os.Build +import androidx.annotation.RequiresApi + +/** + * Created by Stardust on 2017/1/27. + */ + +interface ActionTarget { + + fun createAction(action: Int, vararg params: Any): SimpleAction + + class TextActionTarget(private var mText: String, internal var mIndex: Int) : ActionTarget { + + override fun createAction(action: Int, vararg params: Any): SimpleAction { + return ActionFactory.createActionWithTextFilter(action, mText, mIndex) + } + } + + class BoundsActionTarget(private var mBoundsInRect: Rect) : ActionTarget { + + override fun createAction(action: Int, vararg params: Any): SimpleAction { + return ActionFactory.createActionWithBoundsFilter(action, mBoundsInRect) + } + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + class EditableActionTarget(private val mIndex: Int) : ActionTarget { + + override fun createAction(action: Int, vararg params: Any): SimpleAction { + return ActionFactory.createActionWithEditableFilter(action, mIndex, params[0].toString()) + } + } + + class IdActionTarget(private val mId: String) : ActionTarget { + + override fun createAction(action: Int, vararg params: Any): SimpleAction { + return ActionFactory.createActionWithIdFilter(action, mId) + } + } +} diff --git a/app/src/main/java/com/stardust/automator/simple_action/DepthFirstSearchTargetAction.kt b/app/src/main/java/com/stardust/automator/simple_action/DepthFirstSearchTargetAction.kt new file mode 100644 index 00000000..1c06839a --- /dev/null +++ b/app/src/main/java/com/stardust/automator/simple_action/DepthFirstSearchTargetAction.kt @@ -0,0 +1,28 @@ +package com.stardust.automator.simple_action + +import com.stardust.automator.UiObject + +/** + * Created by Stardust on 2017/1/27. + */ + +class DepthFirstSearchTargetAction(action: Int, filter: FilterAction.Filter) : SearchTargetAction(action, filter) { + + private val mAble: Able = Able.ABLE_MAP.get(action) + + override fun searchTarget(node: UiObject?): UiObject? { + if (node == null) + return null + if (mAble.isAble(node)) + return node + for (i in 0 until node.childCount) { + val child = node.child(i) ?: continue + val targetNode = searchTarget(child) + if (targetNode != null) + return targetNode + } + return null + } + + +} diff --git a/app/src/main/java/com/stardust/automator/simple_action/FilterAction.kt b/app/src/main/java/com/stardust/automator/simple_action/FilterAction.kt new file mode 100644 index 00000000..04d17c00 --- /dev/null +++ b/app/src/main/java/com/stardust/automator/simple_action/FilterAction.kt @@ -0,0 +1,142 @@ +package com.stardust.automator.simple_action + +import android.graphics.Rect +import com.stardust.automator.UiObject +import java.util.* + +/** + * Created by Stardust on 2017/1/27. + */ + +abstract class FilterAction(private val mFilter: Filter) : SimpleAction() { + + abstract fun perform(nodes: List): Boolean + + interface Filter { + + fun filter(root: UiObject): List + } + + class TextFilter(private var mText: String, internal var mIndex: Int) : Filter { + + override fun filter(root: UiObject): List { + val list = root.findByText(mText) + if (mIndex == -1) + return list + return if (mIndex >= list.size) emptyList() else listOf(list[mIndex]) + } + + override fun toString(): String { + return "TextFilter{" + + "mText='" + mText + '\''.toString() + + ", mIndex=" + mIndex + + '}'.toString() + } + } + + class BoundsFilter(private var mBoundsInScreen: Rect) : Filter { + + override fun filter(root: UiObject): List { + val list = ArrayList() + findAccessibilityNodeInfosByBounds(root, list) + return list + } + + private fun findAccessibilityNodeInfosByBounds(root: UiObject?, list: MutableList) { + if (root == null) + return + val rect = Rect() + root.getBoundsInScreen(rect) + if (rect == mBoundsInScreen) { + list.add(root) + } + val oldSize = list.size + for (i in 0 until root.childCount) { + val child = root.child(i) ?: continue + findAccessibilityNodeInfosByBounds(child, list) + } + if (oldSize == list.size && rect.contains(mBoundsInScreen)) { + list.add(root) + } + } + + override fun toString(): String { + return "BoundsFilter{" + + "mBoundsInScreen=" + mBoundsInScreen + + '}'.toString() + } + } + + class EditableFilter(private val mIndex: Int) : Filter { + + override fun filter(root: UiObject): List { + val editableList = findEditable(root) + if (mIndex == -1) + return editableList + return if (mIndex >= editableList.size) emptyList() else listOf(editableList[mIndex]) + } + + override fun toString(): String { + return "EditableFilter{" + + "mIndex=" + mIndex + + '}'.toString() + } + + companion object { + + fun findEditable(root: UiObject?): List { + if (root == null) { + return emptyList() + } + if (root.isEditable) { + return listOf(root) + } + val list = LinkedList() + for (i in 0 until root.childCount) { + list.addAll(findEditable(root.child(i))) + } + return list + } + } + } + + class IdFilter(private val mId: String) : Filter { + + override fun filter(root: UiObject): List { + return root.findByViewId(mId) + } + + override fun toString(): String { + return "IdFilter{" + + "mId='" + mId + '\''.toString() + + '}'.toString() + } + } + + override fun perform(root: UiObject): Boolean { + val list = mFilter.filter(root) + return perform(list) + } + + + class SimpleFilterAction(private val mAction: Int, filter: Filter) : FilterAction(filter) { + + override fun perform(nodes: List): Boolean { + if (nodes.isEmpty()) + return false + var succeed = true + for (nodeInfo in nodes) { + if (!nodeInfo.performAction(mAction)) { + succeed = false + } + } + return succeed + } + } + + override fun toString(): String { + return "FilterAction{" + + "mFilter=" + mFilter + + '}'.toString() + } +} diff --git a/app/src/main/java/com/stardust/automator/simple_action/ScrollAction.kt b/app/src/main/java/com/stardust/automator/simple_action/ScrollAction.kt new file mode 100644 index 00000000..96f759e7 --- /dev/null +++ b/app/src/main/java/com/stardust/automator/simple_action/ScrollAction.kt @@ -0,0 +1,53 @@ +@file:Suppress("DEPRECATION") + +package com.stardust.automator.simple_action + +import com.stardust.automator.UiObject + +import java.util.ArrayList + +/** + * Created by Stardust on 2017/2/12. + */ +class ScrollAction(private val mAction: Int, private val mIndex: Int) : SimpleAction() { + + override fun perform(root: UiObject): Boolean { + val scrollableNodes = findScrollableNodes(root) + val result = mIndex < scrollableNodes.size && scrollableNodes[mIndex].performAction(mAction) + recycle(scrollableNodes, root) + return result + } + + private fun recycle(list: List, root: UiObject) { + for (nodeInfo in list) { + if (nodeInfo !== root) + nodeInfo.recycle() + } + } + + private fun findScrollableNodes(root: UiObject?): List { + val list = ArrayList() + if (root != null) { + findScrollableNodes(root, list) + if (root.isScrollable) { + list.add(root) + } + } + return list + } + + private fun findScrollableNodes(node: UiObject?, list: MutableList) { + if (node == null) { + return + } + for (i in 0 until node.childCount) { + val child = node.child(i) ?: continue + findScrollableNodes(child, list) + if (child.isScrollable) { + list.add(child) + } else { + child.recycle() + } + } + } +} diff --git a/app/src/main/java/com/stardust/automator/simple_action/ScrollMaxAction.kt b/app/src/main/java/com/stardust/automator/simple_action/ScrollMaxAction.kt new file mode 100644 index 00000000..80c1986c --- /dev/null +++ b/app/src/main/java/com/stardust/automator/simple_action/ScrollMaxAction.kt @@ -0,0 +1,82 @@ +@file:Suppress("DEPRECATION") + +package com.stardust.automator.simple_action + +import android.graphics.Rect +import android.util.Log + +import com.stardust.automator.UiObject + +import java.util.HashSet + +/** + * Created by Stardust on 2017/1/27. + */ + +class ScrollMaxAction(private val mScrollAction: Int) : SimpleAction() { + private var mMaxScrollableNode: UiObject? = null + private var mRootNode: UiObject? = null + private val mRecycledMaxUiObjects = HashSet() + + override fun perform(root: UiObject): Boolean { + reset() + mRootNode = root + findMaxScrollableNodeInfo(root) + val result = mMaxScrollableNode != null && mMaxScrollableNode!!.performAction(mScrollAction) + reset() + return result + } + + private fun reset() { + mMaxScrollableNode?.let { + if(it != mRootNode){ + it.recycle() + } + } + mRootNode = null + @Suppress("KotlinConstantConditions") + mMaxScrollableNode = mRootNode + mRecycledMaxUiObjects.clear() + } + + private fun findMaxScrollableNodeInfo(nodeInfo: UiObject?) { + if (nodeInfo == null) + return + + if (nodeInfo.isScrollable) { + val maxScrollableNode = mMaxScrollableNode + if (maxScrollableNode == null) { + mMaxScrollableNode = nodeInfo + } else if (getAreaInScreen(maxScrollableNode) < getAreaInScreen(nodeInfo)) { + if (maxScrollableNode !== mRootNode) { + mRecycledMaxUiObjects.add(maxScrollableNode) + maxScrollableNode.recycle() + } + mMaxScrollableNode = nodeInfo + } + } + for (i in 0 until nodeInfo.childCount) { + val child = nodeInfo.child(i) + if (child != null) { + findMaxScrollableNodeInfo(child) + if (mMaxScrollableNode !== child && !mRecycledMaxUiObjects.contains(child)) { + child.recycle() + } + } + } + } + + private fun getAreaInScreen(nodeInfo: UiObject): Long { + val rect = Rect() + nodeInfo.getBoundsInScreen(rect) + val area = rect.width().toLong() * rect.height() + Log.v(TAG, "area=$area") + return area + } + + companion object { + + private val TAG = ScrollMaxAction::class.java.simpleName + } + +} diff --git a/app/src/main/java/com/stardust/automator/simple_action/SearchTargetAction.kt b/app/src/main/java/com/stardust/automator/simple_action/SearchTargetAction.kt new file mode 100644 index 00000000..7480664e --- /dev/null +++ b/app/src/main/java/com/stardust/automator/simple_action/SearchTargetAction.kt @@ -0,0 +1,38 @@ +package com.stardust.automator.simple_action + +import android.graphics.BitmapFactory + +import com.stardust.automator.UiObject + +/** + * Created by Stardust on 2017/1/27. + */ + +abstract class SearchTargetAction(val action: Int, filter: FilterAction.Filter) : FilterAction(filter) { + + override fun perform(nodes: List): Boolean { + var performed = false + for (node in nodes) { + val targetNode = searchTarget(node) + if (targetNode != null && performAction(targetNode)) { + performed = true + } + } + return performed + } + + protected open fun performAction(node: UiObject): Boolean { + return node.performAction(action) + } + + open fun searchTarget(node: UiObject?): UiObject? { + return node + } + + override fun toString(): String { + return "SearchTargetAction{" + + "mAction=" + action + ", " + + super.toString() + + "}" + } +} diff --git a/app/src/main/java/com/stardust/automator/simple_action/SearchUpTargetAction.kt b/app/src/main/java/com/stardust/automator/simple_action/SearchUpTargetAction.kt new file mode 100644 index 00000000..b02f091e --- /dev/null +++ b/app/src/main/java/com/stardust/automator/simple_action/SearchUpTargetAction.kt @@ -0,0 +1,38 @@ +package com.stardust.automator.simple_action + + +import com.stardust.automator.UiObject + +/** + * Created by Stardust on 2017/1/27. + */ + +class SearchUpTargetAction(action: Int, filter: FilterAction.Filter) : SearchTargetAction(action, filter) { + private val mAble: Able = Able.ABLE_MAP.get(action) + + override fun searchTarget(n: UiObject?): UiObject? { + var node = n + var i = 0 + while (node != null && !mAble.isAble(node)) { + i++ + if (i > LOOP_MAX) { + return null + } + node = node.parent() + } + return node + } + + override fun toString(): String { + return "SearchUpTargetAction{" + + "mAble=" + mAble + ", " + + super.toString() + + '}'.toString() + } + + companion object { + + private val TAG = SearchUpTargetAction::class.java!!.getSimpleName() + private val LOOP_MAX = 20 + } +} diff --git a/app/src/main/java/com/stardust/automator/simple_action/SimpleAction.kt b/app/src/main/java/com/stardust/automator/simple_action/SimpleAction.kt new file mode 100644 index 00000000..f7492c55 --- /dev/null +++ b/app/src/main/java/com/stardust/automator/simple_action/SimpleAction.kt @@ -0,0 +1,18 @@ +package com.stardust.automator.simple_action + +import com.stardust.automator.UiObject + +/** + * Created by Stardust on 2017/1/27. + */ + +abstract class SimpleAction { + + @Volatile + var isValid = true + @Volatile + var result = false + + abstract fun perform(root: UiObject): Boolean + +} diff --git a/app/src/main/java/com/stardust/automator/test/TestUiObject.kt b/app/src/main/java/com/stardust/automator/test/TestUiObject.kt new file mode 100644 index 00000000..4768122b --- /dev/null +++ b/app/src/main/java/com/stardust/automator/test/TestUiObject.kt @@ -0,0 +1,85 @@ +package com.stardust.automator.test + +import android.graphics.Rect +import android.os.Bundle +import com.stardust.automator.UiObject +import java.util.* + +/** + * Created by Stardust on 2017/5/5. + */ + +class TestUiObject @JvmOverloads constructor(private val mChildCount: Int = 0.coerceAtLeast(random.nextInt(6) - 2)) : UiObject(null) { + + private val mHashCode = random.nextInt() + private var mRecycled = false + + init { + count++ + max = max.coerceAtLeast(count) + } + + override fun child(i: Int): UiObject? { + return TestUiObject() + } + + override fun parent(): UiObject? { + return TestUiObject() + } + + override fun getChildCount(): Int { + return mChildCount + } + + override fun isScrollable(): Boolean { + return random.nextInt(4) == 0 + } + + override fun isClickable(): Boolean { + return random.nextBoolean() + } + + override fun getBoundsInScreen(outBounds: Rect) { + val left = random.nextInt(1080) + val top = random.nextInt(1920) + val right = random.nextInt(1080 - left) + left + val bottom = random.nextInt(1920 - top) + top + outBounds.set(left, top, right, bottom) + } + + override fun performAction(action: Int, bundle: Bundle): Boolean { + return random.nextBoolean() + } + + override fun performAction(action: Int): Boolean { + return random.nextBoolean() + } + + @Deprecated("Deprecated in Java") + override fun recycle() { + if (mRecycled) { + throw IllegalStateException() + } + mRecycled = true + count-- + } + + override fun toString(): String { + return "UiObject@" + Integer.toHexString(hashCode()) + } + + override fun hashCode(): Int { + return mHashCode + } + + override fun equals(other: Any?): Boolean { + return this === other + } + + companion object { + + var count = 0 + var max = 0 + private val random = Random() + } +} diff --git a/app/src/main/java/com/stardust/concurrent/ConcurrentArrayList.java b/app/src/main/java/com/stardust/concurrent/ConcurrentArrayList.java new file mode 100644 index 00000000..429342c1 --- /dev/null +++ b/app/src/main/java/com/stardust/concurrent/ConcurrentArrayList.java @@ -0,0 +1,62 @@ +package com.stardust.concurrent; + +import java.lang.reflect.Array; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Created by Stardust on 2017/12/30. + */ + +public class ConcurrentArrayList { + + private final Class mTClass; + private volatile T[] mArray; + private final AtomicInteger mSize = new AtomicInteger(); + private final Object mArrayResizeLock = mSize; + + public ConcurrentArrayList(Class tClass) { + mTClass = tClass; + mArray = newArray(10); + } + + public void add(T element) { + int index = mSize.getAndIncrement(); + ensureCapacity(index); + mArray[index] = element; + } + + @SuppressWarnings("unchecked") + public T get(int i) { + int size = mSize.get(); + if (i >= size) { + throw new IndexOutOfBoundsException("i = " + i + ", size = " + size); + } + return mArray[i]; + } + + public int size() { + return mSize.get(); + } + + public Object getArrayResizeLock() { + return mArrayResizeLock; + } + + @SuppressWarnings("unchecked") + private T[] newArray(int size) { + return (T[]) Array.newInstance(mTClass, size); + } + + private void ensureCapacity(int index) { + if (index < mArray.length) + return; + synchronized (mArrayResizeLock) { + if (index < mArray.length) + return; + T[] newArray = newArray(mArray.length * 2); + System.arraycopy(mArray, 0, newArray, 0, mArray.length); + mArray = newArray; + } + } + +} diff --git a/app/src/main/java/com/stardust/concurrent/Value.java b/app/src/main/java/com/stardust/concurrent/Value.java new file mode 100644 index 00000000..628e3729 --- /dev/null +++ b/app/src/main/java/com/stardust/concurrent/Value.java @@ -0,0 +1,27 @@ +package com.stardust.concurrent; + +/** + * Created by Stardust on 2017/12/27. + */ + +public class Value { + + private T mValue; + + public Value(T value) { + mValue = value; + } + + public Value() { + } + + public T get() { + return mValue; + } + + public void set(T value) { + mValue = value; + } + + +} diff --git a/app/src/main/java/com/stardust/concurrent/VolatileBox.java b/app/src/main/java/com/stardust/concurrent/VolatileBox.java new file mode 100644 index 00000000..894ee098 --- /dev/null +++ b/app/src/main/java/com/stardust/concurrent/VolatileBox.java @@ -0,0 +1,72 @@ +package com.stardust.concurrent; + + +import java.lang.reflect.Constructor; + +/** + * Created by Stardust on 2017/5/8. + */ + +public class VolatileBox { + + private volatile T mValue; + + public VolatileBox() { + + } + + public VolatileBox(T value) { + set(value); + } + + public T get() { + return mValue; + } + + public void set(T value) { + mValue = value; + } + + public boolean isNull() { + return mValue == null; + } + + public boolean notNull() { + return mValue != null; + } + + public void setAndNotify(T value) { + mValue = value; + synchronized (this) { + notify(); + } + } + + public T blockedGet() { + synchronized (this) { + try { + this.wait(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + return mValue; + } + + public T blockedGetOrThrow(Class exception) { + synchronized (this) { + try { + this.wait(); + } catch (InterruptedException e) { + try { + throw exception.newInstance(); + } catch (InstantiationException e1) { + throw new RuntimeException(e1); + } catch (IllegalAccessException e1) { + throw new RuntimeException(e1); + } + } + } + return mValue; + } +} diff --git a/app/src/main/java/com/stardust/concurrent/VolatileDispose.java b/app/src/main/java/com/stardust/concurrent/VolatileDispose.java new file mode 100644 index 00000000..d92e6f68 --- /dev/null +++ b/app/src/main/java/com/stardust/concurrent/VolatileDispose.java @@ -0,0 +1,75 @@ +package com.stardust.concurrent; + +/** + * Created by Stardust on 2017/10/28. + */ + +public class VolatileDispose { + + private volatile T mValue; + + public T blockedGet() { + synchronized (this) { + if (mValue != null) { + return mValue; + } + try { + this.wait(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + return mValue; + } + + public T blockedGetOrThrow(Class exception) { + synchronized (this) { + if (mValue != null) { + return mValue; + } + try { + this.wait(); + } catch (InterruptedException e) { + try { + throw exception.newInstance(); + } catch (InstantiationException e1) { + throw new RuntimeException(e1); + } catch (IllegalAccessException e1) { + throw new RuntimeException(e1); + } + } + } + return mValue; + } + + public T blockedGetOrThrow(Class exception, long timeout, T defaultValue) { + synchronized (this) { + if (mValue != null) { + return mValue; + } + try { + this.wait(timeout); + } catch (InterruptedException e) { + try { + throw exception.newInstance(); + } catch (InstantiationException e1) { + throw new RuntimeException(e1); + } catch (IllegalAccessException e1) { + throw new RuntimeException(e1); + } + } + if (mValue == null) { + return defaultValue; + } + } + return mValue; + } + + public void setAndNotify(T value) { + synchronized (this) { + mValue = value; + notify(); + } + } + +} diff --git a/app/src/main/java/com/stardust/event/EventDispatcher.java b/app/src/main/java/com/stardust/event/EventDispatcher.java new file mode 100644 index 00000000..8caea547 --- /dev/null +++ b/app/src/main/java/com/stardust/event/EventDispatcher.java @@ -0,0 +1,32 @@ +package com.stardust.event; + +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Created by Stardust on 2017/8/6. + */ + +public class EventDispatcher { + + public interface Event { + void notify(Listener l); + } + + private CopyOnWriteArrayList mListeners = new CopyOnWriteArrayList<>(); + + public void addListener(Listener l) { + mListeners.add(l); + } + + + public boolean removeListener(Listener l) { + return mListeners.remove(l); + } + + public void dispatchEvent(Event event) { + for (Listener listener : mListeners) { + event.notify(listener); + } + } + +} diff --git a/app/src/main/java/com/stardust/ext/Common.kt b/app/src/main/java/com/stardust/ext/Common.kt new file mode 100644 index 00000000..b725e67c --- /dev/null +++ b/app/src/main/java/com/stardust/ext/Common.kt @@ -0,0 +1,13 @@ +package com.stardust.ext + +import kotlin.reflect.KMutableProperty0 + +fun > T.ifNull(provider: () -> R): R { + val value = this.get() + if (value != null) { + return value + } + val newValue = provider() + set(newValue) + return newValue +} \ No newline at end of file diff --git a/app/src/main/java/com/stardust/io/ByteBufferBackedInputStream.kt b/app/src/main/java/com/stardust/io/ByteBufferBackedInputStream.kt new file mode 100644 index 00000000..34b5fb74 --- /dev/null +++ b/app/src/main/java/com/stardust/io/ByteBufferBackedInputStream.kt @@ -0,0 +1,30 @@ +package com.stardust.io + +import java.io.IOException +import java.io.InputStream +import java.nio.ByteBuffer + +class ByteBufferBackedInputStream(private var buf: ByteBuffer) : InputStream() { + + @Throws(IOException::class) + override fun read(): Int { + return if (!buf.hasRemaining()) { + -1 + } else buf.get().toInt() and 0xFF + } + + @Throws(IOException::class) + override fun read(bytes: ByteArray, off: Int, len: Int): Int { + if (!buf.hasRemaining()) { + return -1 + } + val read = Math.min(len, available()) + buf.get(bytes, off, read) + buf.position(buf.position() - read) + return read + } + + override fun available(): Int { + return buf.position() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/stardust/io/ByteBufferBackedOutputStream.kt b/app/src/main/java/com/stardust/io/ByteBufferBackedOutputStream.kt new file mode 100644 index 00000000..7c3ee425 --- /dev/null +++ b/app/src/main/java/com/stardust/io/ByteBufferBackedOutputStream.kt @@ -0,0 +1,19 @@ +package com.stardust.io + +import java.io.IOException +import java.io.OutputStream +import java.nio.ByteBuffer + +class ByteBufferBackedOutputStream(private var buf: ByteBuffer) : OutputStream() { + + @Throws(IOException::class) + override fun write(b: Int) { + buf.put(b.toByte()) + } + + @Throws(IOException::class) + override fun write(bytes: ByteArray, off: Int, len: Int) { + buf.put(bytes, off, len) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/stardust/io/ConcatReader.java b/app/src/main/java/com/stardust/io/ConcatReader.java new file mode 100644 index 00000000..3e6ef327 --- /dev/null +++ b/app/src/main/java/com/stardust/io/ConcatReader.java @@ -0,0 +1,424 @@ +package com.stardust.io; + + +/* + * Copyright (C) 2004 Stephen Ostermiller + * http://ostermiller.org/contact.pl?regarding=Java+Utilities + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * See COPYING.TXT for details. + */ + +import java.io.*; +import java.util.ArrayList; +import java.util.Arrays; + +/** + * A reader which reads sequentially from multiple sources. + * More information about this class is available from ostermiller.org. + * + * @author Stephen Ostermiller http://ostermiller.org/contact.pl?regarding=Java+Utilities + * @since ostermillerutils 1.04.00 + */ +public class ConcatReader extends Reader { + + /** + * Current index to readerQueue + * + * @since ostermillerutils 1.04.01 + */ + private int readerQueueIndex = 0; + + /** + * Queue of readers that have yet to be read from. + * + * @since ostermillerutils 1.04.01 + */ + private ArrayList readerQueue = new ArrayList<>(); + + /** + * A cache of the current reader from the readerQueue + * to avoid unneeded access to the queue which must + * be synchronized. + * + * @since ostermillerutils 1.04.01 + */ + private Reader currentReader = null; + + /** + * true iff the client may add more readers. + * + * @since ostermillerutils 1.04.01 + */ + private boolean doneAddingReaders = false; + + + /** + * Causes the addReader method to throw IllegalStateException + * and read() methods to return -1 (end of stream) + * when there is no more available data. + *

+ * Calling this method when this class is no longer accepting + * more readers has no effect. + * + * @since ostermillerutils 1.04.01 + */ + public void lastReaderAdded() { + doneAddingReaders = true; + } + + /** + * Add the given reader to the queue of readers from which to + * concatenate data. + * + * @param in Reader to add to the concatenation. + * @throws IllegalStateException if more readers can't be added because lastReaderAdded() has been called, close() has been called, or a constructor with reader parameters was used. + * @since ostermillerutils 1.04.01 + */ + public void addReader(Reader in) { + synchronized (readerQueue) { + if (in == null) throw new NullPointerException(); + if (closed) throw new IllegalStateException("ConcatReader has been closed"); + if (doneAddingReaders) + throw new IllegalStateException("Cannot add more readers - the last reader has already been added."); + readerQueue.add(in); + } + } + + /** + * Add the given reader to the queue of readers from which to + * concatenate data. + * + * @param in Reader to add to the concatenation. + * @throws IllegalStateException if more readers can't be added because lastReaderAdded() has been called, close() has been called, or a constructor with reader parameters was used. + * @throws NullPointerException the array of readers, or any of the contents is null. + * @since ostermillerutils 1.04.01 + */ + public void addReaders(Reader[] in) { + for (Reader element : in) { + addReader(element); + } + } + + /** + * Gets the current reader, looking at the next + * one in the list if the current one is null. + * + * @since ostermillerutils 1.04.01 + */ + private Reader getCurrentReader() { + if (currentReader == null && readerQueueIndex < readerQueue.size()) { + synchronized (readerQueue) { + // reader queue index is advanced only by the nextReader() + // method. Don't do it here. + currentReader = readerQueue.get(readerQueueIndex); + } + } + return currentReader; + } + + /** + * Indicate that we are done with the current reader and we should + * advance to the next reader. + * + * @since ostermillerutils 1.04.01 + */ + private void advanceToNextReader() { + currentReader = null; + readerQueueIndex++; + } + + /** + * True iff this the close() method has been called on this stream. + * + * @since ostermillerutils 1.04.00 + */ + private boolean closed = false; + + /** + * Create a new reader that can dynamically accept new sources. + *

+ * New sources should be added using the addReader() method. + * When all sources have been added the lastReaderAdded() should + * be called so that read methods can return -1 (end of stream). + *

+ * Adding new sources may by interleaved with read calls. + * + * @since ostermillerutils 1.04.01 + */ + public ConcatReader() { + // Empty Constructor + } + + /** + * Create a new reader with one source. + *

+ * When using this constructor, more readers cannot + * be added later, and calling addReader() will + * throw an illegal state Exception. + * + * @param in reader to use as a source. + * @throws NullPointerException if in is null + * @since ostermillerutils 1.04.00 + */ + public ConcatReader(Reader in) { + addReader(in); + lastReaderAdded(); + } + + /** + * Create a new reader with two sources. + *

+ * When using this constructor, more readers cannot + * be added later, and calling addReader() will + * throw an illegal state Exception. + * + * @param in1 first reader to use as a source. + * @param in2 second reader to use as a source. + * @throws NullPointerException if either source is null. + * @since ostermillerutils 1.04.00 + */ + public ConcatReader(Reader in1, Reader in2) { + addReader(in1); + addReader(in2); + lastReaderAdded(); + } + + /** + * Create a new reader with an arbitrary number of sources. + *

+ * When using this constructor, more readers cannot + * be added later, and calling addReader() will + * throw an illegal state Exception. + * + * @param in readers to use as a sources. + * @throws NullPointerException if the input array on any element is null. + * @since ostermillerutils 1.04.00 + */ + public ConcatReader(Reader[] in) { + addReaders(in); + lastReaderAdded(); + } + + /** + * Read a single character. This method will block until a + * character is available, an I/O error occurs, or the end of all underlying + * streams are reached. + *

+ * If this class in not done accepting readers and the end of the last known + * stream is reached, this method will block forever unless another thread + * adds a reader or interrupts. + * + * @return The character read, as an integer in the range 0 to 65535 (0x00-0xffff), + * or -1 if the end of the stream has been reached + * @throws IOException - If an I/O error occurs + * @since ostermillerutils 1.04.00 + */ + @Override + public int read() throws IOException { + if (closed) throw new IOException("Reader closed"); + int r = -1; + while (r == -1) { + Reader in = getCurrentReader(); + if (in == null) { + if (doneAddingReaders) return -1; + try { + Thread.sleep(100); + } catch (InterruptedException iox) { + throw new IOException("Interrupted"); + } + } else { + r = in.read(); + if (r == -1) advanceToNextReader(); + } + } + return r; + } + + /** + * Read characters into an array. This method will block until some input is available, an + * I/O error occurs, or the end of all underlying + * streams are reached. + *

+ * If this class in not done accepting readers and the end of the last known + * stream is reached, this method will block forever unless another thread + * adds a reader or interrupts. + * + * @param cbuf - Destination buffer + * @return The number of characters read, or -1 if the end of the stream has been reached + * @throws IOException - If an I/O error occurs + * @throws NullPointerException - If the buffer is null. + * @since ostermillerutils 1.04.00 + */ + @Override + public int read(char[] cbuf) throws IOException { + return read(cbuf, 0, cbuf.length); + } + + /** + * Read characters into a portion of an array. This method will block until + * some input is available, an I/O error occurs, or the end of all underlying + * streams are reached. + *

+ * If this class in not done accepting readers and the end of the last known + * stream is reached, this method will block forever unless another thread + * adds a reader or interrupts. + * + * @param cbuf Destination buffer + * @param off Offset at which to start storing characters + * @param len Maximum number of characters to read + * @return The number of characters read, or -1 if the end of the stream has been reached + * @throws IOException - If an I/O error occurs + * @throws NullPointerException - If the buffer is null. + * @throws IndexOutOfBoundsException - if length or offset are not possible. + * @since ostermillerutils 1.04.00 + */ + @Override + public int read(char[] cbuf, int off, int len) throws IOException { + if (off < 0 || len < 0 || off + len > cbuf.length) throw new IndexOutOfBoundsException(); + if (closed) throw new IOException("Reader closed"); + int r = -1; + while (r == -1) { + Reader in = getCurrentReader(); + if (in == null) { + if (doneAddingReaders) return -1; + try { + Thread.sleep(100); + } catch (InterruptedException iox) { + throw new IOException("Interrupted"); + } + } else { + r = in.read(cbuf, off, len); + if (r == -1) advanceToNextReader(); + } + } + return r; + } + + /** + * Skip characters. This method will block until some characters are + * available, an I/O error occurs, or the end of the stream is reached. + *

+ * If this class in not done accepting readers and the end of the last known + * stream is reached, this method will block forever unless another thread + * adds a reader or interrupts. + * + * @param n the number of characters to skip + * @return The number of characters actually skipped + * @throws IllegalArgumentException If n is negative. + * @throws IOException If an I/O error occurs + * @since ostermillerutils 1.04.00 + */ + @Override + public long skip(long n) throws IOException { + if (closed) throw new IOException("Reader closed"); + if (n <= 0) return 0; + long s = -1; + while (s <= 0) { + Reader in = getCurrentReader(); + if (in == null) { + if (doneAddingReaders) return 0; + try { + Thread.sleep(100); + } catch (InterruptedException iox) { + throw new IOException("Interrupted"); + } + } else { + s = in.skip(n); + // When nothing was skipped it is a bit of a puzzle. + // The most common cause is that the end of the underlying + // stream was reached. In which case calling skip on it + // will always return zero. If somebody were calling skip + // until it skipped everything they needed, there would + // be an infinite loop if we were to return zero here. + // If we get zero, let us try to read one character so + // we can see if we are at the end of the stream. If so, + // we will move to the next. + if (s <= 0) { + // read() will advance to the next stream for us, so don't do it again + s = ((read() == -1) ? -1 : 1); + } + } + + } + return s; + } + + /** + * Tell whether this stream is ready to be read. + * + * @return True if the next read() is guaranteed not to block for input, + * false otherwise. Note that returning false does not guarantee that the next + * read will block. + * @throws IOException If an I/O error occurs + * @since ostermillerutils 1.04.00 + */ + @Override + public boolean ready() throws IOException { + if (closed) throw new IOException("Reader closed"); + Reader in = getCurrentReader(); + if (in == null) return false; + return in.ready(); + } + + /** + * Close the stream and any underlying streams. + * Once a stream has been closed, further read(), ready(), mark(), or reset() + * invocations will throw an IOException. Closing a previously-closed stream, + * however, has no effect. + * + * @throws IOException If an I/O error occurs + * @since ostermillerutils 1.04.00 + */ + @Override + public void close() throws IOException { + if (closed) return; + for (Reader reader : readerQueue) { + reader.close(); + } + closed = true; + } + + /** + * Mark not supported. + * + * @throws IOException because mark is not supported. + * @since ostermillerutils 1.04.00 + */ + @Override + public void mark(int readlimit) throws IOException { + throw new IOException("Mark not supported"); + } + + /** + * Reset not supported. + * + * @throws IOException because reset is not supported. + * @since ostermillerutils 1.04.00 + */ + @Override + public void reset() throws IOException { + throw new IOException("Reset not supported"); + } + + /** + * Mark not supported. + * + * @return false + * @since ostermillerutils 1.04.00 + */ + @Override + public boolean markSupported() { + return false; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/stardust/io/EFile.java b/app/src/main/java/com/stardust/io/EFile.java new file mode 100644 index 00000000..83f85a65 --- /dev/null +++ b/app/src/main/java/com/stardust/io/EFile.java @@ -0,0 +1,29 @@ +package com.stardust.io; + +import androidx.annotation.NonNull; + +import java.io.File; +import java.net.URI; + +/** + * Created by Stardust on 2017/8/19. + */ + +public class EFile extends File { + + public EFile(@NonNull String pathname) { + super(pathname); + } + + public EFile(String parent, @NonNull String child) { + super(parent, child); + } + + public EFile(File parent, @NonNull String child) { + super(parent, child); + } + + public EFile(@NonNull URI uri) { + super(uri); + } +} diff --git a/app/src/main/java/com/stardust/io/Zip.java b/app/src/main/java/com/stardust/io/Zip.java new file mode 100644 index 00000000..9e006709 --- /dev/null +++ b/app/src/main/java/com/stardust/io/Zip.java @@ -0,0 +1,49 @@ +package com.stardust.io; + +import com.stardust.pio.PFile; +import com.stardust.pio.PFiles; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import static com.stardust.pio.PFiles.closeSilently; + +public class Zip { + + public static void unzip(InputStream stream, File dir) throws IOException { + FileOutputStream fos = null; + ZipInputStream zis = null; + try { + zis = new ZipInputStream(stream); + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + File file = new File(dir, entry.getName()); + if (entry.isDirectory()) { + file.mkdirs(); + } else { + PFiles.ensureDir(file.getPath()); + fos = new FileOutputStream(file); + PFiles.write(zis, fos, false); + fos.close(); + fos = null; + zis.closeEntry(); + } + } + } finally { + closeSilently(fos); + closeSilently(stream); + closeSilently(zis); + } + } + + public static void unzip(File zipFile, File dir) throws IOException { + unzip(new FileInputStream(zipFile), dir); + } + + +} diff --git a/app/src/main/java/com/stardust/lang/ThreadCompat.java b/app/src/main/java/com/stardust/lang/ThreadCompat.java new file mode 100644 index 00000000..53916ac3 --- /dev/null +++ b/app/src/main/java/com/stardust/lang/ThreadCompat.java @@ -0,0 +1,69 @@ +package com.stardust.lang; + +import java.lang.ref.WeakReference; +import java.util.Collections; +import java.util.Hashtable; +import java.util.Set; +import java.util.WeakHashMap; + +/** + * Created by Stardust on 2017/4/30. + */ + +public class ThreadCompat extends Thread { + + // FIXME: 2017/12/29 是否需要用synchronizedMap?这里虽然线程不安全,但竞争很小 + private static final Set interruptedThreads = Collections.newSetFromMap(new WeakHashMap()); + + public ThreadCompat() { + } + + public ThreadCompat(Runnable target) { + super(target); + } + + public ThreadCompat(ThreadGroup group, Runnable target) { + super(group, target); + } + + public ThreadCompat(String name) { + super(name); + } + + public ThreadCompat(ThreadGroup group, String name) { + super(group, name); + } + + public ThreadCompat(Runnable target, String name) { + super(target, name); + } + + public ThreadCompat(ThreadGroup group, Runnable target, String name) { + super(group, target, name); + } + + public ThreadCompat(ThreadGroup group, Runnable target, String name, long stackSize) { + super(group, target, name, stackSize); + } + + @Override + public boolean isInterrupted() { + return super.isInterrupted() || interruptedThreads.contains(this); + } + + public static boolean interrupted() { + boolean interrupted = Thread.currentThread().isInterrupted(); + interruptedThreads.remove(Thread.currentThread()); + Thread.interrupted(); + return interrupted; + } + + + @Override + public void interrupt() { + super.interrupt(); + interruptedThreads.add(this); + } + + +} diff --git a/app/src/main/java/com/stardust/net/AutoHttpURLConnection.java b/app/src/main/java/com/stardust/net/AutoHttpURLConnection.java new file mode 100644 index 00000000..a3828847 --- /dev/null +++ b/app/src/main/java/com/stardust/net/AutoHttpURLConnection.java @@ -0,0 +1,62 @@ +package com.stardust.net; + +import com.stardust.pio.UncheckedIOException; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; + +/** + * Created by Stardust on 2017/4/10. + */ + +public class AutoHttpURLConnection extends HttpURLConnection implements AutoCloseable { + + private HttpURLConnection mHttpURLConnection; + private InputStream mInputStream; + private OutputStream mOutputStream; + + + public AutoHttpURLConnection(URL url) throws IOException { + super(url); + mHttpURLConnection = (HttpURLConnection) url.openConnection(); + connect(); + } + + public AutoHttpURLConnection(String url) throws IOException { + this(new URL(url)); + } + + @Override + public InputStream getInputStream() { + try { + mInputStream = super.getInputStream(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return mInputStream; + } + + @Override + public void close() { + disconnect(); + } + + @Override + public void disconnect() { + mHttpURLConnection.disconnect(); + } + + @Override + public boolean usingProxy() { + return mHttpURLConnection.usingProxy(); + } + + @Override + public void connect() throws IOException { + mHttpURLConnection.connect(); + } +} diff --git a/app/src/main/java/com/stardust/notification/Notification.kt b/app/src/main/java/com/stardust/notification/Notification.kt new file mode 100644 index 00000000..ab91ef5f --- /dev/null +++ b/app/src/main/java/com/stardust/notification/Notification.kt @@ -0,0 +1,96 @@ +package com.stardust.notification + +import android.app.PendingIntent +import android.os.Build +import android.os.Parcel +import androidx.annotation.RequiresApi + +/** + * Created by Stardust on 2017/10/30. + */ + +class Notification private constructor(val packageName: String) : android.app.Notification() { + + val text: String? + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + get() = extras.getString(EXTRA_TEXT) + + val title: String? + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + get() = extras.getString(EXTRA_TITLE) + + fun click() { + try { + this.contentIntent.send() + } catch (e: PendingIntent.CanceledException) { + throw RuntimeException(e) + } + + } + + fun delete() { + try { + this.deleteIntent.send() + } catch (e: PendingIntent.CanceledException) { + throw RuntimeException(e) + } + + } + + override fun toString(): String { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + "Notification{" + + "packageName='" + packageName + "', " + + "title='" + title + ", " + + "text='" + text + "'" + + "} " + } else super.toString() + } + + companion object { + + fun create(n: android.app.Notification, packageName: String): Notification { + val notification = Notification(packageName) + clone(n, notification) + return notification + } + + @Suppress("DEPRECATION") + fun clone(from: android.app.Notification, to: android.app.Notification) { + to.`when` = from.`when` + to.icon = from.icon + to.iconLevel = from.iconLevel + to.number = from.number + to.contentIntent = from.contentIntent + to.deleteIntent = from.deleteIntent + to.fullScreenIntent = from.fullScreenIntent + to.tickerText = from.tickerText + to.tickerView = from.tickerView + to.contentView = from.contentView + to.bigContentView = from.bigContentView + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + to.headsUpContentView = from.headsUpContentView + to.audioAttributes = from.audioAttributes + to.color = from.color + to.visibility = from.visibility + to.category = from.category + to.publicVersion = from.publicVersion + } + to.largeIcon = from.largeIcon + to.sound = from.sound + to.audioStreamType = from.audioStreamType + to.vibrate = from.vibrate + to.ledARGB = from.ledARGB + to.ledOnMS = from.ledOnMS + to.ledOffMS = from.ledOffMS + to.defaults = from.defaults + to.flags = from.flags + to.priority = from.priority + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + to.extras = from.extras + to.actions = from.actions + } + } + } + +} diff --git a/app/src/main/java/com/stardust/notification/NotificationListenerService.kt b/app/src/main/java/com/stardust/notification/NotificationListenerService.kt new file mode 100644 index 00000000..103b86cd --- /dev/null +++ b/app/src/main/java/com/stardust/notification/NotificationListenerService.kt @@ -0,0 +1,59 @@ +package com.stardust.notification + +import android.app.Notification +import android.os.Build +import android.os.Parcel +import android.service.notification.StatusBarNotification +import androidx.annotation.RequiresApi + +import com.stardust.view.accessibility.NotificationListener +import java.util.concurrent.CopyOnWriteArrayList + +/** + * Created by Stardust on 2017/10/30. + */ + +@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) +class NotificationListenerService : android.service.notification.NotificationListenerService() { + + private val mNotificationListeners = CopyOnWriteArrayList() + + override fun onCreate() { + super.onCreate() + instance = this + } + + override fun onNotificationPosted(sbn: StatusBarNotification, rankingMap: RankingMap) { + onNotificationPosted(sbn) + } + + override fun onNotificationPosted(sbn: StatusBarNotification) { + for (listener in mNotificationListeners) { + listener.onNotification(com.stardust.notification.Notification.create( + sbn.notification, sbn.packageName)) + } + } + + override fun onNotificationRemoved(sbn: StatusBarNotification) {} + + override fun onNotificationRemoved(sbn: StatusBarNotification, rankingMap: RankingMap) {} + + fun addListener(listener: NotificationListener) { + mNotificationListeners.add(listener) + } + + fun removeListener(listener: NotificationListener): Boolean { + return mNotificationListeners.remove(listener) + } + + + override fun onDestroy() { + super.onDestroy() + instance = null + } + + companion object { + var instance: NotificationListenerService? = null + private set + } +} diff --git a/app/src/main/java/com/stardust/pio/PFile.java b/app/src/main/java/com/stardust/pio/PFile.java new file mode 100644 index 00000000..97d6feab --- /dev/null +++ b/app/src/main/java/com/stardust/pio/PFile.java @@ -0,0 +1,139 @@ +package com.stardust.pio; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.File; +import java.io.FileFilter; +import java.io.FilenameFilter; +import java.net.URI; +import java.util.ArrayList; + +/** + * Created by Stardust on 2017/10/19. + */ + +public class PFile extends File { + + private String mSimplifyPath; + private String mSimplifiedName; + private String mExtension; + + public PFile(@NonNull String pathname) { + super(pathname); + init(); + } + + public PFile(String parent, @NonNull String child) { + super(parent, child); + init(); + } + + public PFile(File parent, @NonNull String child) { + super(parent, child); + init(); + } + + public PFile(@NonNull URI uri) { + super(uri); + init(); + } + + + private void init() { + if (isDirectory()) { + mSimplifiedName = getName(); + } else { + mSimplifiedName = PFiles.getNameWithoutExtension(getName()); + } + mSimplifyPath = PFiles.getSimplifiedPath(getPath()); + } + + + @NonNull + public PFile renameTo(String newName) { + PFile newFile = new PFile(getParent(), newName); + if (renameTo(newFile)) { + return newFile; + } else { + return this; + } + } + + @NonNull + public PFile renameWithoutExt(String newName) { + PFile newFile = isDirectory() ? new PFile(getParent(), newName) : + new PFile(getParent(), newName + "." + getExtension()); + if (renameTo(newFile)) { + return newFile; + } else { + return this; + } + } + + public String getExtension() { + if (mExtension == null) { + mExtension = PFiles.getExtension(getName()); + } + return mExtension; + } + + public String getSimplifiedPath() { + return mSimplifyPath; + } + + @Override + public PFile getParentFile() { + String p = this.getParent(); + if (p == null) + return null; + return new PFile(p); + } + + @Override + public PFile[] listFiles() { + String ss[] = list(); + if (ss == null) return null; + ArrayList files = new ArrayList<>(); + for (int i = 0; i < ss.length; i++) { + if (!ss[i].startsWith(".")) { + files.add(new PFile(this, ss[i])); + } + } + return files.toArray(new PFile[files.size()]); + } + + @Override + public PFile[] listFiles(FilenameFilter filter) { + String ss[] = list(); + if (ss == null) return null; + ArrayList files = new ArrayList<>(); + for (String s : ss) + if (!s.startsWith(".") && (filter == null || filter.accept(this, s))) + files.add(new PFile(this, s)); + return files.toArray(new PFile[files.size()]); + } + + @Override + public PFile[] listFiles(FileFilter filter) { + String ss[] = list(); + if (ss == null) return null; + ArrayList files = new ArrayList<>(); + for (String s : ss) { + PFile f = new PFile(this, s); + if (!f.isHidden() && (filter == null || filter.accept(f))) + files.add(f); + } + return files.toArray(new PFile[files.size()]); + + } + + public String getSimplifiedName() { + return mSimplifiedName; + } + + public boolean moveTo(PFile to) { + return renameTo(new File(to, getName())); + } + +} diff --git a/app/src/main/java/com/stardust/pio/PFileInterface.java b/app/src/main/java/com/stardust/pio/PFileInterface.java new file mode 100644 index 00000000..6fe6ba95 --- /dev/null +++ b/app/src/main/java/com/stardust/pio/PFileInterface.java @@ -0,0 +1,9 @@ +package com.stardust.pio; + +/** + * Created by Stardust on 2017/12/5. + */ + +public interface PFileInterface { + String getPath(); +} diff --git a/app/src/main/java/com/stardust/pio/PFiles.java b/app/src/main/java/com/stardust/pio/PFiles.java new file mode 100644 index 00000000..abb2fd0e --- /dev/null +++ b/app/src/main/java/com/stardust/pio/PFiles.java @@ -0,0 +1,525 @@ +package com.stardust.pio; + +import android.app.NativeActivity; +import android.content.Context; +import android.content.res.AssetManager; +import android.os.Environment; +import android.text.TextUtils; +import android.util.Log; + +import com.stardust.util.Func1; + +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.Locale; + +/** + * Created by Stardust on 2017/4/1. + */ + +public class PFiles { + + private static final String TAG = "PFiles"; + + static final int DEFAULT_BUFFER_SIZE = 8192; + static final String DEFAULT_ENCODING = Charset.defaultCharset().name(); + + public static PFileInterface open(String path, String mode, String encoding, int bufferSize) { + switch (mode) { + case "r": + return new PReadableTextFile(path, encoding, bufferSize); + case "w": + return new PWritableTextFile(path, encoding, bufferSize, false); + case "a": + return new PWritableTextFile(path, encoding, bufferSize, true); + } + return null; + } + + public static Object open(String path, String mode, String encoding) { + return open(path, mode, encoding, DEFAULT_BUFFER_SIZE); + } + + public static Object open(String path, String mode) { + return open(path, mode, DEFAULT_ENCODING, DEFAULT_BUFFER_SIZE); + } + + public static Object open(String path) { + return open(path, "r", DEFAULT_ENCODING, DEFAULT_BUFFER_SIZE); + } + + public static boolean create(String path) { + File f = new File(path); + if (path.endsWith(File.separator)) { + return f.mkdir(); + } else { + try { + return f.createNewFile(); + } catch (IOException e) { + return false; + } + } + } + + public static boolean createIfNotExists(String path) { + ensureDir(path); + File file = new File(path); + if (!file.exists()) { + try { + return file.createNewFile(); + } catch (IOException e) { + e.printStackTrace(); + } + } + return false; + } + + public static boolean createWithDirs(String path) { + return createIfNotExists(path); + } + + public static boolean exists(String path) { + return new File(path).exists(); + } + + public static boolean ensureDir(String path) { + int i = path.lastIndexOf("\\"); + if (i < 0) + i = path.lastIndexOf("/"); + if (i >= 0) { + String folder = path.substring(0, i); + File file = new File(folder); + if (file.exists()) + return true; + return file.mkdirs(); + } else { + return false; + } + } + + public static String read(String path, String encoding) { + return read(new File(path), encoding); + } + + public static String read(String path) { + return read(new File(path)); + } + + + public static String read(File file, String encoding) { + try { + return read(new FileInputStream(file), encoding); + } catch (FileNotFoundException e) { + throw new UncheckedIOException(e); + } + } + + public static String read(File file) { + return read(file, "utf-8"); + } + + public static String read(InputStream is, String encoding) { + try { + byte[] bytes = new byte[is.available()]; + is.read(bytes); + return new String(bytes, encoding); + } catch (IOException e) { + throw new UncheckedIOException(e); + } finally { + closeSilently(is); + } + } + + public static String read(InputStream inputStream) { + return read(inputStream, "utf-8"); + } + + public static byte[] readBytes(InputStream is) { + try { + byte[] bytes = new byte[is.available()]; + is.read(bytes); + return bytes; + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public static boolean copyRaw(Context context, int rawId, String path) { + InputStream is = context.getResources().openRawResource(rawId); + return copyStream(is, path); + } + + public static boolean copyStream(InputStream is, String path) { + if (!ensureDir(path)) + return false; + File file = new File(path); + try { + if (!file.exists()) + if (!file.createNewFile()) + return false; + FileOutputStream fos = new FileOutputStream(file); + write(is, fos); + return true; + } catch (IOException | UncheckedIOException e) { + e.printStackTrace(); + return false; + } + } + + public static void write(InputStream is, OutputStream os, boolean close) { + byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; + try { + while (is.available() > 0) { + int n = is.read(buffer); + if (n > 0) { + os.write(buffer, 0, n); + } + } + if (close) { + is.close(); + os.close(); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public static void write(InputStream is, OutputStream os) { + write(is, os, true); + } + + + public static void write(String path, String text) { + write(new File(path), text); + } + + public static void write(String path, String text, String encoding) { + try { + write(new FileOutputStream(path), text, encoding); + } catch (FileNotFoundException e) { + throw new UncheckedIOException(e); + } + } + + public static void write(File file, String text) { + try { + write(new FileOutputStream(file), text); + } catch (FileNotFoundException e) { + throw new UncheckedIOException(e); + } + } + + public static void write(OutputStream fileOutputStream, String text) { + write(fileOutputStream, text, "utf-8"); + } + + + public static void write(OutputStream outputStream, String text, String encoding) { + try { + outputStream.write(text.getBytes(encoding)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } finally { + closeSilently(outputStream); + } + } + + public static void append(String path, String text) { + create(path); + try { + write(new FileOutputStream(path, true), text); + } catch (FileNotFoundException e) { + throw new UncheckedIOException(e); + } + } + + + public static void append(String path, String text, String encoding) { + create(path); + try { + write(new FileOutputStream(path, true), text, encoding); + } catch (FileNotFoundException e) { + throw new UncheckedIOException(e); + } + } + + public static void writeBytes(OutputStream outputStream, byte[] bytes) { + try { + outputStream.write(bytes); + outputStream.close(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public static void appendBytes(String path, byte[] bytes) { + create(path); + try { + writeBytes(new FileOutputStream(path, true), bytes); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public static void writeBytes(String path, byte[] bytes) { + try { + writeBytes(new FileOutputStream(path), bytes); + } catch (FileNotFoundException e) { + throw new UncheckedIOException(e); + } + } + + public static boolean copy(String pathFrom, String pathTo) { + try { + return copyStream(new FileInputStream(pathFrom), pathTo); + } catch (FileNotFoundException e) { + e.printStackTrace(); + return false; + } + } + + public static boolean copyAsset(Context context, String assetFile, String path) { + try { + return copyStream(context.getAssets().open(assetFile), path); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + + + public static void copyAssetDir(AssetManager manager, String assetsDir, String toDir, String[] list) throws IOException { + new File(toDir).mkdirs(); + if (list == null) { + list = manager.list(assetsDir); + } + if (list == null) + throw new IOException("not a directory: " + assetsDir); + for (String file : list) { + if (TextUtils.isEmpty(file)) { + continue; + } + String fullAssetsPath = join(assetsDir, file); + String[] children = manager.list(fullAssetsPath); + if (children == null || children.length == 0) { + InputStream stream = null; + try { + stream = manager.open(fullAssetsPath); + copyStream(stream, join(toDir, file)); + } catch (IOException e) { + throw e; + } finally { + if (stream != null) { + stream.close(); + } + } + } else { + copyAssetDir(manager, fullAssetsPath, join(toDir, file), children); + } + } + } + + public static String renameWithoutExtensionAndReturnNewPath(String path, String newName) { + File file = new File(path); + File newFile = new File(file.getParent(), newName + "." + getExtension(file.getName())); + file.renameTo(newFile); + return newFile.getAbsolutePath(); + } + + public static boolean renameWithoutExtension(String path, String newName) { + File file = new File(path); + File newFile = new File(file.getParent(), newName + "." + getExtension(file.getName())); + return file.renameTo(newFile); + } + + public static boolean rename(String path, String newName) { + File f = new File(path); + return f.renameTo(new File(f.getParent(), newName)); + } + + public static boolean move(String path, String newPath) { + File f = new File(path); + return f.renameTo(new File(newPath)); + } + + public static String getExtension(String fileName) { + int i = fileName.lastIndexOf('.'); + if (i < 0 || i + 1 >= fileName.length() - 1) + return ""; + return fileName.substring(i + 1); + } + + public static String generateNotExistingPath(String path, String extension) { + if (!new File(path + extension).exists()) + return path + extension; + int i = 0; + while (true) { + String pathI = path + "(" + i + ")" + extension; + if (!new File(pathI).exists()) + return pathI; + i++; + } + } + + public static String getName(String filePath) { + filePath = filePath.replace('\\', '/'); + return new File(filePath).getName(); + } + + public static String getNameWithoutExtension(String filePath) { + String fileName = getName(filePath); + int b = fileName.lastIndexOf('.'); + if (b < 0) + b = fileName.length(); + fileName = fileName.substring(0, b); + return fileName; + } + + public static File copyAssetToTmpFile(Context context, String path) { + String extension = "." + getExtension(path); + String name = getNameWithoutExtension(path); + if (name.length() < 5) { + name += name.hashCode(); + } + try { + File tmpFile = File.createTempFile(name, extension, context.getCacheDir()); + copyAsset(context, path, tmpFile.getPath()); + return tmpFile; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static boolean deleteRecursively(File file) { + if (file.isFile()) + return file.delete(); + File[] children = file.listFiles(); + if (children != null) { + for (File child : children) { + if (!deleteRecursively(child)) + return false; + } + } + return file.delete(); + } + + public static boolean deleteFilesOfDir(File dir) { + if (!dir.isDirectory()) + throw new IllegalArgumentException("not a directory: " + dir); + File[] children = dir.listFiles(); + if (children != null) { + for (File child : children) { + if (!deleteRecursively(child)) + return false; + } + } + return true; + } + + public static boolean remove(String path) { + return new File(path).delete(); + } + + public static boolean removeDir(String path) { + return deleteRecursively(new File(path)); + } + + public static String getSdcardPath() { + return Environment.getExternalStorageDirectory().getPath(); + } + + public static String readAsset(AssetManager assets, String path) { + try { + return read(assets.open(path)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public static String[] listDir(String path) { + File file = new File(path); + return wrapNonNull(file.list()); + } + + private static String[] wrapNonNull(String[] list) { + if (list == null) + return new String[0]; + return list; + } + + public static String[] listDir(String path, final Func1 filter) { + final File file = new File(path); + return wrapNonNull(file.list(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return filter.call(name); + } + })); + } + + public static boolean isFile(String path) { + return new File(path).isFile(); + } + + public static boolean isDir(String path) { + return new File(path).isDirectory(); + } + + public static boolean isEmptyDir(String path) { + File file = new File(path); + return file.isDirectory() && file.list().length == 0; + } + + public static String join(String base, String... paths) { + File file = new File(base); + for (String path : paths) { + file = new File(file, path); + } + return file.getPath(); + } + + public static String getHumanReadableSize(long bytes) { + int unit = 1024; + if (bytes < unit) return bytes + " B"; + int exp = (int) (Math.log(bytes) / Math.log(unit)); + String pre = "KMGTPE".substring(exp - 1, exp); + return String.format(Locale.getDefault(), "%.1f %sB", bytes / Math.pow(unit, exp), pre); + } + + public static String getSimplifiedPath(String path) { + if (path.startsWith(Environment.getExternalStorageDirectory().getPath())) { + return path.substring(Environment.getExternalStorageDirectory().getPath().length()); + } + return path; + } + + public static byte[] readBytes(String path) { + try { + return readBytes(new FileInputStream(path)); + } catch (FileNotFoundException e) { + throw new UncheckedIOException(e); + } + } + + public static void closeSilently(Closeable closeable) { + if (closeable == null) { + return; + } + try { + closeable.close(); + } catch (IOException ignored) { + + } + } +} diff --git a/app/src/main/java/com/stardust/pio/PRandomAccessBinaryFile.java b/app/src/main/java/com/stardust/pio/PRandomAccessBinaryFile.java new file mode 100644 index 00000000..f75b232e --- /dev/null +++ b/app/src/main/java/com/stardust/pio/PRandomAccessBinaryFile.java @@ -0,0 +1,36 @@ +package com.stardust.pio; + +import java.io.Closeable; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Stardust on 2017/4/29. + */ + +public class PRandomAccessBinaryFile extends RandomAccessFile { + + + private RandomAccessFile mRandomAccessFile; + + public PRandomAccessBinaryFile(String name, String mode) throws FileNotFoundException { + super(name, mode); + } + + public PRandomAccessBinaryFile(File file, String mode) throws FileNotFoundException { + super(file, mode); + } + + public String readline() throws IOException { + return super.readLine(); + } + + + + + +} diff --git a/app/src/main/java/com/stardust/pio/PReadableBinaryFile.java b/app/src/main/java/com/stardust/pio/PReadableBinaryFile.java new file mode 100644 index 00000000..92361a41 --- /dev/null +++ b/app/src/main/java/com/stardust/pio/PReadableBinaryFile.java @@ -0,0 +1,17 @@ +package com.stardust.pio; + +import java.io.Closeable; +import java.io.IOException; + +/** + * Created by Stardust on 2017/4/6. + */ + +public class PReadableBinaryFile implements Closeable { + + + @Override + public void close() throws IOException { + + } +} diff --git a/app/src/main/java/com/stardust/pio/PReadableTextFile.java b/app/src/main/java/com/stardust/pio/PReadableTextFile.java new file mode 100644 index 00000000..935d4ac1 --- /dev/null +++ b/app/src/main/java/com/stardust/pio/PReadableTextFile.java @@ -0,0 +1,113 @@ +package com.stardust.pio; + +import java.io.*; +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Stardust on 2017/4/1. + */ + +public class PReadableTextFile implements Closeable, PFileInterface { + + private BufferedReader mBufferedReader; + private FileInputStream mFileInputStream; + private int mBufferingSize; + private String mEncoding; + private String mPath; + + public PReadableTextFile(String path) { + this(path, PFiles.DEFAULT_ENCODING); + } + + public PReadableTextFile(String path, String encoding) { + this(path, encoding, -1); + } + + public PReadableTextFile(String path, String encoding, int bufferingSize) { + mEncoding = encoding; + mBufferingSize = bufferingSize; + mPath = path; + try { + mFileInputStream = new FileInputStream(path); + } catch (FileNotFoundException e) { + throw new UncheckedIOException(e); + } + } + + + private void ensureBufferReader() { + if (mBufferedReader == null) { + try { + if (mBufferingSize == -1) + mBufferedReader = new BufferedReader(new InputStreamReader(mFileInputStream, mEncoding)); + else + mBufferedReader = new BufferedReader(new InputStreamReader(mFileInputStream, mEncoding), mBufferingSize); + } catch (UnsupportedEncodingException e) { + throw new UncheckedIOException(e); + } + + } + } + + public String read() { + try { + byte[] data = new byte[mFileInputStream.available()]; + mFileInputStream.read(data); + return new String(data, mEncoding); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public String read(int size) { + ensureBufferReader(); + try { + char[] chars = new char[size]; + int len = mBufferedReader.read(chars); + return new String(chars, 0, len); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public String readline() { + ensureBufferReader(); + try { + return mBufferedReader.readLine(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public String[] readlines() { + ensureBufferReader(); + List lines = new ArrayList<>(); + try { + while (mBufferedReader.ready()) { + lines.add(mBufferedReader.readLine()); + } + return lines.toArray(new String[lines.size()]); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public void close() { + try { + if (mBufferedReader != null) { + mBufferedReader.close(); + } else { + mFileInputStream.close(); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public String getPath() { + return mPath; + } +} diff --git a/app/src/main/java/com/stardust/pio/PWritableTextFile.java b/app/src/main/java/com/stardust/pio/PWritableTextFile.java new file mode 100644 index 00000000..6db5919e --- /dev/null +++ b/app/src/main/java/com/stardust/pio/PWritableTextFile.java @@ -0,0 +1,125 @@ +package com.stardust.pio; + +import java.io.BufferedWriter; +import java.io.Closeable; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.UnsupportedEncodingException; +import java.util.Arrays; +import java.util.List; + +import static com.stardust.pio.PFiles.DEFAULT_BUFFER_SIZE; + +/** + * Created by Stardust on 2017/4/1. + */ + +public class PWritableTextFile implements Closeable, PFileInterface { + + public static PWritableTextFile open(String path, String encoding, int bufferSize) { + return new PWritableTextFile(path, encoding, bufferSize, false); + } + + public static PWritableTextFile open(String path, String encoding) { + return new PWritableTextFile(path, encoding); + } + + public static PWritableTextFile open(String path, boolean append) { + return new PWritableTextFile(path, append); + } + + public static PWritableTextFile open(String path) { + return new PWritableTextFile(path); + } + + private BufferedWriter mBufferedWriter; + private String mPath; + + public PWritableTextFile(String path, String encoding, int bufferingSize, boolean append) { + mPath = path; + if (bufferingSize <= 0) { + bufferingSize = DEFAULT_BUFFER_SIZE; + } + try { + mBufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(path, append), encoding), bufferingSize); + } catch (UnsupportedEncodingException | FileNotFoundException e) { + throw new UncheckedIOException(e); + } + } + + public PWritableTextFile(String path) { + this(path, PFiles.DEFAULT_ENCODING, DEFAULT_BUFFER_SIZE, false); + } + + public PWritableTextFile(String path, boolean append) { + this(path, PFiles.DEFAULT_ENCODING, DEFAULT_BUFFER_SIZE, append); + } + + public PWritableTextFile(String path, int bufferSize) { + this(path, PFiles.DEFAULT_ENCODING, bufferSize, false); + } + + public PWritableTextFile(String path, String encoding) { + this(path, encoding, DEFAULT_BUFFER_SIZE, false); + } + + public PWritableTextFile(String path, String encoding, boolean append) { + this(path, encoding, DEFAULT_BUFFER_SIZE, append); + } + + public PWritableTextFile(String path, String encoding, int bufferSize) { + this(path, encoding, bufferSize, false); + } + + + public void write(String str) { + try { + mBufferedWriter.write(str); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public void writeline(String line) { + try { + mBufferedWriter.write(line); + mBufferedWriter.newLine(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public void writelines(String[] lines) { + writelines(Arrays.asList(lines)); + } + + public void writelines(List lines) { + for (String line : lines) { + writeline(line); + } + } + + public void close() { + try { + mBufferedWriter.close(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public void flush() { + try { + mBufferedWriter.flush(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + + @Override + public String getPath() { + return mPath; + } +} diff --git a/app/src/main/java/com/stardust/pio/UncheckedIOException.java b/app/src/main/java/com/stardust/pio/UncheckedIOException.java new file mode 100644 index 00000000..ca110b0f --- /dev/null +++ b/app/src/main/java/com/stardust/pio/UncheckedIOException.java @@ -0,0 +1,19 @@ +package com.stardust.pio; + +import java.io.IOException; + +/** + * Created by Stardust on 2017/4/1. + */ + +public class UncheckedIOException extends RuntimeException { + + public UncheckedIOException(IOException cause) { + super(cause); + } + + @Override + public synchronized IOException getCause() { + return (IOException) super.getCause(); + } +} diff --git a/app/src/main/java/com/stardust/util/AdvancedEncryptionStandard.kt b/app/src/main/java/com/stardust/util/AdvancedEncryptionStandard.kt new file mode 100644 index 00000000..5b8c9ee7 --- /dev/null +++ b/app/src/main/java/com/stardust/util/AdvancedEncryptionStandard.kt @@ -0,0 +1,42 @@ +package com.stardust.util + +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +class AdvancedEncryptionStandard(private val key: ByteArray, private val initVector: String) { + + /** + * Encrypts the given plain text + * + * @param plainText The plain text to encrypt + */ + @Throws(Exception::class) + fun encrypt(plainText: ByteArray): ByteArray { + val secretKey = SecretKeySpec(key, ALGORITHM) + val cipher = Cipher.getInstance(FULL_ALGORITHM) + val ivParameterSpec = IvParameterSpec(initVector.toByteArray()) + cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec) + return cipher.doFinal(plainText) + } + + /** + * Decrypts the given byte array + * + * @param cipherText The data to decrypt + */ + @Throws(Exception::class) + fun decrypt(cipherText: ByteArray, start: Int = 0, end: Int = cipherText.size): ByteArray { + val secretKey = SecretKeySpec(key, ALGORITHM) + val ivParameterSpec = IvParameterSpec(initVector.toByteArray()) + val cipher = Cipher.getInstance(FULL_ALGORITHM) + cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec) + return cipher.doFinal(cipherText, start, end - start) + } + + companion object { + + private const val ALGORITHM = "AES" + private const val FULL_ALGORITHM = "AES/CBC/PKCS5Padding" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/stardust/util/Array.kt b/app/src/main/java/com/stardust/util/Array.kt new file mode 100644 index 00000000..261579f8 --- /dev/null +++ b/app/src/main/java/com/stardust/util/Array.kt @@ -0,0 +1,9 @@ +package com.stardust.util + +import java.util.* + +inline fun sortedArrayOf(vararg elements: T): Array { + val a = arrayOf(*elements) + Arrays.sort(a) + return a +} \ No newline at end of file diff --git a/app/src/main/java/com/stardust/util/ArrayUtils.java b/app/src/main/java/com/stardust/util/ArrayUtils.java new file mode 100644 index 00000000..2c13f0d3 --- /dev/null +++ b/app/src/main/java/com/stardust/util/ArrayUtils.java @@ -0,0 +1,46 @@ +package com.stardust.util; + +import java.lang.reflect.Array; +import java.util.List; + +/** + * Created by Stardust on 2017/5/8. + */ + +public class ArrayUtils { + + + public static Integer[] box(int[] array) { + Integer[] box = new Integer[array.length]; + for (int i = 0; i < array.length; i++) { + box[i] = array[i]; + } + return box; + } + + + public static int[] unbox(Integer[] array) { + int[] unbox = new int[array.length]; + for (int i = 0; i < array.length; i++) { + unbox[i] = array[i]; + } + return unbox; + } + + public static String[] toStringArray(List list) { + int i = 0; + String[] str = new String[list.size()]; + for (Object o : list) { + str[i] = o == null ? null : o.toString(); + } + return str; + } + + @SuppressWarnings("unchecked") + public static T[] merge(T[] a1, T[] a2) { + T[] a = (T[]) Array.newInstance(a1.getClass().getComponentType(), a1.length + a2.length); + System.arraycopy(a1, 0, a, 0, a1.length); + System.arraycopy(a2, 0, a, a1.length, a2.length); + return a; + } +} diff --git a/app/src/main/java/com/stardust/util/AssetsCache.java b/app/src/main/java/com/stardust/util/AssetsCache.java new file mode 100644 index 00000000..67e6b2ba --- /dev/null +++ b/app/src/main/java/com/stardust/util/AssetsCache.java @@ -0,0 +1,30 @@ +package com.stardust.util; + +import android.app.Activity; +import android.content.res.AssetManager; + +import com.stardust.pio.PFiles; + +/** + * Created by Stardust on 2017/3/14. + */ + +public class AssetsCache { + + private static final long PERSIST_TIME = 5 * 60 * 1000; + + private static SimpleCache cache = new SimpleCache<>(PERSIST_TIME, 5, 30 * 1000); + + public static String get(final AssetManager assetManager, final String path) { + return cache.get(path, new SimpleCache.Supplier() { + @Override + public String get(String key) { + return PFiles.readAsset(assetManager, path); + } + }); + } + + public static String get(final Activity activity, final String path) { + return get(activity.getAssets(), path); + } +} diff --git a/app/src/main/java/com/stardust/util/BackPressedHandler.java b/app/src/main/java/com/stardust/util/BackPressedHandler.java new file mode 100644 index 00000000..ac5125d5 --- /dev/null +++ b/app/src/main/java/com/stardust/util/BackPressedHandler.java @@ -0,0 +1,85 @@ +package com.stardust.util; + +import android.app.Activity; +import android.os.Handler; +import android.widget.Toast; + + +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Created by Stardust on 2017/2/3. + */ +public interface BackPressedHandler { + + boolean onBackPressed(Activity activity); + + interface HostActivity { + + Observer getBackPressedObserver(); + + } + + class Observer implements BackPressedHandler { + + private CopyOnWriteArrayList mBackPressedHandlers = new CopyOnWriteArrayList<>(); + + @Override + public boolean onBackPressed(Activity activity) { + for (BackPressedHandler handler : mBackPressedHandlers) { + if (handler.onBackPressed(activity)) { + return true; + } + } + return false; + } + + public void registerHandler(BackPressedHandler handler) { + mBackPressedHandlers.add(handler); + } + + public void registerHandlerAtFront(BackPressedHandler handler) { + mBackPressedHandlers.add(0, handler); + } + + public void unregisterHandler(BackPressedHandler handler) { + mBackPressedHandlers.remove(handler); + } + } + + + class DoublePressExit implements BackPressedHandler { + + private final Activity mActivity; + private long mLastPressedMillis; + private long mDoublePressInterval = 1000; + private String mToast; + + public DoublePressExit(Activity activity, int noticeResId) { + this(activity, activity.getString(noticeResId)); + } + + public DoublePressExit(Activity activity, String toast) { + mActivity = activity; + mToast = toast; + } + + public DoublePressExit doublePressInterval(long doublePressInterval) { + mDoublePressInterval = doublePressInterval; + return this; + } + + @Override + public boolean onBackPressed(Activity activity) { + if (System.currentTimeMillis() - mLastPressedMillis < mDoublePressInterval) { + mActivity.finish(); + } else { + mLastPressedMillis = System.currentTimeMillis(); + Toast.makeText(mActivity, mToast, Toast.LENGTH_SHORT).show(); + } + return true; + } + } + + +} diff --git a/app/src/main/java/com/stardust/util/BiMap.java b/app/src/main/java/com/stardust/util/BiMap.java new file mode 100644 index 00000000..10e526a7 --- /dev/null +++ b/app/src/main/java/com/stardust/util/BiMap.java @@ -0,0 +1,16 @@ +package com.stardust.util; + +import java.util.Map; +import java.util.Set; + +public interface BiMap extends Map { + + K getKey(V value); + + Set valueSet(); + + V getOr(K key, V def); + + K getKeyOr(V value, K def); + +} diff --git a/app/src/main/java/com/stardust/util/BiMaps.java b/app/src/main/java/com/stardust/util/BiMaps.java new file mode 100644 index 00000000..c78b2f28 --- /dev/null +++ b/app/src/main/java/com/stardust/util/BiMaps.java @@ -0,0 +1,218 @@ +package com.stardust.util; + +import android.os.Build; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Function; + +public class BiMaps { + + public static BiMap make(Map keyToValue, Map valueToKey) { + return new BiMapImpl<>(keyToValue, valueToKey); + } + + public static BiMapBuilder newBuilder() { + return new BiMapBuilder<>(); + } + + public static class BiMapBuilder { + + private final BiMap mBiMap = make(new HashMap(), new HashMap()); + + public BiMapBuilder put(K key, V value) { + mBiMap.put(key, value); + return this; + } + + public BiMap build() { + return mBiMap; + } + + } + + private static class BiMapImpl implements BiMap { + + private final Map mKVMap; + private final Map mVKMap; + + private BiMapImpl(Map kvMap, Map vkMap) { + mKVMap = kvMap; + mVKMap = vkMap; + } + + @Override + public int size() { + return mKVMap.size(); + } + + @Override + public boolean isEmpty() { + return mKVMap.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return mKVMap.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return mVKMap.containsKey(value); + } + + @Override + public V get(Object key) { + return mKVMap.get(key); + } + + @Override + public V put(K key, V value) { + V put = mKVMap.put(key, value); + mVKMap.put(value, key); + return put; + } + + @Override + public V remove(Object key) { + V remove = mKVMap.remove(key); + if (remove != null) { + mVKMap.remove(key); + } + return remove; + } + + @Override + public void putAll(@NonNull Map m) { + mKVMap.putAll(m); + } + + @Override + public void clear() { + mKVMap.clear(); + mVKMap.clear(); + } + + @NonNull + @Override + public Set keySet() { + return mKVMap.keySet(); + } + + @Override + public K getKey(V value) { + return mVKMap.get(value); + } + + @Override + public Set valueSet() { + return mVKMap.keySet(); + } + + @Override + public V getOr(K key, V def) { + V v = get(key); + return v == null ? def : v; + } + + @Override + public K getKeyOr(V value, K def) { + K key = getKey(value); + return key == null ? def : key; + } + + @NonNull + @Override + public Collection values() { + return mKVMap.values(); + } + + @NonNull + @Override + public Set> entrySet() { + return mKVMap.entrySet(); + } + + @Override + public boolean equals(Object o) { + return mKVMap.equals(o); + } + + @Override + public int hashCode() { + return mKVMap.hashCode(); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + @Override + public V getOrDefault(Object key, V defaultValue) { + return mKVMap.getOrDefault(key, defaultValue); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + @Override + public void forEach(BiConsumer action) { + mKVMap.forEach(action); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + @Override + public void replaceAll(BiFunction function) { + mKVMap.replaceAll(function); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + @Override + public V putIfAbsent(K key, V value) { + return mKVMap.putIfAbsent(key, value); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + @Override + public boolean remove(Object key, Object value) { + return mKVMap.remove(key, value); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + @Override + public boolean replace(K key, V oldValue, V newValue) { + return mKVMap.replace(key, oldValue, newValue); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + @Override + public V replace(K key, V value) { + return mKVMap.replace(key, value); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + @Override + public V computeIfAbsent(K key, Function mappingFunction) { + return mKVMap.computeIfAbsent(key, mappingFunction); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + @Override + public V computeIfPresent(K key, BiFunction remappingFunction) { + return mKVMap.computeIfPresent(key, remappingFunction); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + @Override + public V compute(K key, BiFunction remappingFunction) { + return mKVMap.compute(key, remappingFunction); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + @Override + public V merge(K key, V value, BiFunction remappingFunction) { + return mKVMap.merge(key, value, remappingFunction); + } + } +} diff --git a/app/src/main/java/com/stardust/util/Callback.java b/app/src/main/java/com/stardust/util/Callback.java new file mode 100644 index 00000000..d3d3941b --- /dev/null +++ b/app/src/main/java/com/stardust/util/Callback.java @@ -0,0 +1,10 @@ +package com.stardust.util; + +/** + * Created by Stardust on 2017/4/18. + */ + +public interface Callback { + + void call(T t); +} diff --git a/app/src/main/java/com/stardust/util/ClipboardUtil.java b/app/src/main/java/com/stardust/util/ClipboardUtil.java new file mode 100644 index 00000000..2053b3dc --- /dev/null +++ b/app/src/main/java/com/stardust/util/ClipboardUtil.java @@ -0,0 +1,34 @@ +package com.stardust.util; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; + +import androidx.annotation.NonNull; + + +/** + * Created by Stardust on 2017/3/10. + */ + +public class ClipboardUtil { + + + public static void setClip(Context context, CharSequence text) { + ((ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE)).setPrimaryClip(ClipData.newPlainText("", text)); + } + + public static CharSequence getClip(Context context) { + ClipData clip = ((ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE)).getPrimaryClip(); + return (clip == null || clip.getItemCount() == 0) ? null : clip.getItemAt(0).getText(); + } + + @NonNull + public static CharSequence getClipOrEmpty(Context context) { + CharSequence clip = getClip(context); + if (clip == null) { + return ""; + } + return clip; + } +} diff --git a/app/src/main/java/com/stardust/util/Consumer.java b/app/src/main/java/com/stardust/util/Consumer.java new file mode 100644 index 00000000..2a31f183 --- /dev/null +++ b/app/src/main/java/com/stardust/util/Consumer.java @@ -0,0 +1,13 @@ +package com.stardust.util; + +import androidx.annotation.Keep; + +/** + * Created by Stardust on 2017/3/10. + */ +@Keep +public interface Consumer { + + void accept(T t); + +} diff --git a/app/src/main/java/com/stardust/util/DeveloperUtils.java b/app/src/main/java/com/stardust/util/DeveloperUtils.java new file mode 100644 index 00000000..85fb1119 --- /dev/null +++ b/app/src/main/java/com/stardust/util/DeveloperUtils.java @@ -0,0 +1,199 @@ +package com.stardust.util; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.Service; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ServiceInfo; +import android.content.pm.Signature; +import androidx.annotation.Nullable; +import android.util.Base64; + +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.security.MessageDigest; +import java.util.concurrent.ExecutorService; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** + * Created by Stardust on 2017/4/5. + */ + +public class DeveloperUtils { + + private static final String PACKAGE_NAME = "org.autojs.autojs4"; + private static final String SIGNATURE = "nPNPcy4Lk/eP6fLvZitP0VPbHdFCbKua77m59vis5fA="; + private static final String LOG_TAG = "DeveloperUtils"; + private static final ExecutorService sExecutor = UnderuseExecutors.getExecutor(); + private static final String SALT = "let\nlife\nbe\nbeautiful\nlike\nsummer\nflowers\nand\ndeath\nlike\nautumn\nleaves\n."; + + public static boolean isSelfPackage(@Nullable String runningPackage) { + return PACKAGE_NAME.equals(runningPackage); + } + + @Nullable + public static String getSignatureSHA(Context context, String packageName) { + try { + @SuppressLint("PackageManagerGetSignatures") + PackageInfo packageInfo = getPackageInfo(context, packageName, PackageManager.GET_SIGNATURES); + if (packageInfo == null) + return null; + Signature[] signatures = packageInfo.signatures; + StringBuilder builder = new StringBuilder(); + for (Signature signature : signatures) { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + md.update(signature.toByteArray()); + final String sha = Base64.encodeToString(md.digest(), Base64.NO_WRAP); + builder.append(sha); + } + return builder.toString(); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + private static PackageInfo getPackageInfo(Context context, String packageName, int flags) { + try { + return context.getPackageManager().getPackageInfo(packageName, flags); + } catch (PackageManager.NameNotFoundException e) { + return null; + } + } + + /** + * 此方法仅防止那些不会改源码直接用apk编辑器修改应用内字符串(QQ群号)等的恶意用户行为。 + * 为了开源社区的发展,请善用源码:-) + */ + public static boolean checkSignature(Context context) { + return checkSignature(context, context.getPackageName()); + } + + public static boolean checkSignature(Context context, String packageName) { + String sha = getSignatureSHA(context, packageName); + if (sha == null) + return false; + if (sha.endsWith("\n")) { + sha = sha.substring(0, sha.length() - 1); + } + return SIGNATURE.equals(sha); + } + + + public static String selfPackage() { + return PACKAGE_NAME; + } + + + public static boolean isActivityRegistered(Context context, Class c) { + try { + PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_ACTIVITIES); + ActivityInfo[] activities = packageInfo.activities; + if (activities == null) { + return false; + } + for (ActivityInfo info : activities) { + if (c.getName().equals(info.name)) { + return true; + } + } + return false; + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + + public static boolean isServiceRegistered(Context context, Class c) { + try { + PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SERVICES); + ServiceInfo[] activities = packageInfo.services; + if (activities == null) { + return false; + } + for (ServiceInfo info : activities) { + if (c.getName().equals(info.name)) { + return true; + } + } + return false; + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + + public static boolean checkDexFile(Context context, long[] crc) { + String apkPath = context.getPackageCodePath(); + try { + ZipFile zipFile = new ZipFile(apkPath); + for (int i = 0; i < crc.length; i++) { + String dexFile; + if (i == 0) { + dexFile = "classes.dex"; + } else { + dexFile = "classes" + (i + 1) + ".dex"; + } + ZipEntry dexEntry = zipFile.getEntry(dexFile); + long dexEntryCrc = dexEntry.getCrc(); + //Log.d(LOG_TAG, String.valueOf(dexEntryCrc)); + if (dexEntryCrc != crc[i]) { + return false; + } + } + return true; + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + + public static void verifyApk(Activity activity, final int crcRes) { + final WeakReference activityWeakReference = new WeakReference<>(activity); + sExecutor.execute(new Runnable() { + @Override + public void run() { + Activity a = activityWeakReference.get(); + if (a == null) + return; + if (!checkSignature(a)) { + a.finish(); + return; + } + //long[] crc = readCrc(a.getString(crcRes)); + //if (!checkDexFile(a, crc)) { + //a.finish(); + //} + + } + }); + } + + private static long[] readCrc(String crcStr) { + String[] crcStrings = crcStr.split("\n"); + StringBuilder iHash = new StringBuilder(); + long[] crc = new long[crcStrings.length - 1]; + for (int i = 0; i < crc.length; i++) { + crc[i] = Long.parseLong(crcStrings[i]); + iHash.append(iHash(crcStrings[i])); + } + if (!crcStrings[crcStrings.length - 1].equals(iHash.toString())) { + return new long[crc.length]; + } + return crc; + } + + private static String iHash(String data) { + try { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + for (int i = 0; i < 8; i++) { + data = Base64.encodeToString(md5.digest((data + SALT).getBytes()), Base64.NO_WRAP); + } + return data; + } catch (Exception e) { + return null; + } + } +} diff --git a/app/src/main/java/com/stardust/util/DrawerAutoClose.java b/app/src/main/java/com/stardust/util/DrawerAutoClose.java new file mode 100644 index 00000000..272aca56 --- /dev/null +++ b/app/src/main/java/com/stardust/util/DrawerAutoClose.java @@ -0,0 +1,28 @@ +package com.stardust.util; + +import android.app.Activity; +import androidx.drawerlayout.widget.DrawerLayout; + +/** + * Created by Stardust on 2017/6/19. + */ + +public class DrawerAutoClose implements BackPressedHandler { + + private DrawerLayout mDrawerLayout; + private int mGravity; + + public DrawerAutoClose(DrawerLayout drawerLayout, int gravity){ + mDrawerLayout = drawerLayout; + mGravity = gravity; + } + + @Override + public boolean onBackPressed(Activity activity) { + if (mDrawerLayout.isDrawerOpen(mGravity)) { + mDrawerLayout.closeDrawer(mGravity); + return true; + } + return false; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/stardust/util/FileSorter.java b/app/src/main/java/com/stardust/util/FileSorter.java new file mode 100644 index 00000000..28795764 --- /dev/null +++ b/app/src/main/java/com/stardust/util/FileSorter.java @@ -0,0 +1,86 @@ +package com.stardust.util; + +import com.stardust.pio.PFiles; + +import java.io.File; +import java.text.Collator; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * Created by Stardust on 2017/3/31. + */ + +public class FileSorter { + + public static final Comparator NAME = new Comparator() { + final Collator collator = Collator.getInstance(); + + @Override + public int compare(File o1, File o2) { + if (o1.isDirectory() != o2.isDirectory()) + return o1.isDirectory() ? Integer.MIN_VALUE : Integer.MAX_VALUE; + return -collator.compare(o1.getName(), o2.getName()); + } + }; + + public static final Comparator DATE = new Comparator() { + @Override + public int compare(File o1, File o2) { + return o1.lastModified() == o2.lastModified() ? 0 : + o1.lastModified() > o2.lastModified() ? 1 : -1; + } + }; + + public static final Comparator TYPE = new Comparator() { + @Override + public int compare(File o1, File o2) { + return -PFiles.getExtension(o1.getName()).compareTo(PFiles.getExtension(o2.getName())); + } + }; + + public static final Comparator SIZE = new Comparator() { + @Override + public int compare(File o1, File o2) { + return o1.length() == o2.length() ? 0 : + o1.length() < o2.length() ? 1 : -1; + } + }; + + public static void sort(File[] files, final Comparator comparator, boolean ascending) { + if (ascending) { + Arrays.sort(files, comparator); + } else { + Arrays.sort(files, new Comparator() { + @Override + public int compare(File o1, File o2) { + return comparator.compare(o2, o1); + } + }); + } + } + + public static void sort(File[] files, Comparator comparator) { + sort(files, comparator, true); + } + + public static void sort(List files, final Comparator comparator, boolean ascending) { + if (ascending) { + Collections.sort(files, comparator); + } else { + Collections.sort(files, new Comparator() { + @Override + public int compare(File o1, File o2) { + return comparator.compare(o2, o1); + } + }); + } + } + + public static void sort(List files, Comparator comparator) { + sort(files, comparator, true); + } + +} diff --git a/app/src/main/java/com/stardust/util/Func1.java b/app/src/main/java/com/stardust/util/Func1.java new file mode 100644 index 00000000..b7c34be0 --- /dev/null +++ b/app/src/main/java/com/stardust/util/Func1.java @@ -0,0 +1,11 @@ +package com.stardust.util; + +/** + * Created by Stardust on 2017/7/7. + */ + +public interface Func1 { + + R call(T t); + +} diff --git a/app/src/main/java/com/stardust/util/HashUtils.java b/app/src/main/java/com/stardust/util/HashUtils.java new file mode 100644 index 00000000..a280cd3e --- /dev/null +++ b/app/src/main/java/com/stardust/util/HashUtils.java @@ -0,0 +1,25 @@ +package com.stardust.util; + +import android.util.Base64; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Created by Stardust on 2017/9/26. + */ + +public class HashUtils { + + public static String md5(String text) { + MessageDigest md; + byte[] bytesOfMessage = text.getBytes(); + try { + md = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + byte[] thedigest = md.digest(bytesOfMessage); + return Base64.encodeToString(thedigest, Base64.DEFAULT); + } +} diff --git a/app/src/main/java/com/stardust/util/IntentExtras.java b/app/src/main/java/com/stardust/util/IntentExtras.java new file mode 100644 index 00000000..2d078468 --- /dev/null +++ b/app/src/main/java/com/stardust/util/IntentExtras.java @@ -0,0 +1,107 @@ +package com.stardust.util; + +import android.content.Intent; +import android.util.SparseArray; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Created by Stardust on 2017/7/11. + */ + +public class IntentExtras implements Serializable { + + public static final String EXTRA_ID = "com.stardust.util.IntentExtras.id"; + + private static AtomicInteger mMaxId = new AtomicInteger(-1); + private static SparseArray> extraStore = new SparseArray<>(); + + private Map mMap; + private int mId; + + private IntentExtras() { + mMap = new HashMap<>(); + mId = mMaxId.incrementAndGet(); + extraStore.put(mId, mMap); + } + + + private IntentExtras(int id, Map map) { + mId = id; + mMap = map; + } + + + public static IntentExtras newExtras() { + return new IntentExtras(); + } + + public static IntentExtras fromIntentAndRelease(Intent intent) { + int id = intent.getIntExtra(EXTRA_ID, -1); + if (id < 0) { + return null; + } + return fromIdAndRelease(id); + } + + public static IntentExtras fromIdAndRelease(int id) { + Map map = extraStore.get(id); + if (map == null) { + return null; + } + extraStore.remove(id); + return new IntentExtras(id, map); + } + + public static IntentExtras fromId(int id) { + Map map = extraStore.get(id); + if (map == null) { + return null; + } + return new IntentExtras(id, map); + } + + + public static IntentExtras fromIntent(Intent intent) { + int id = intent.getIntExtra(EXTRA_ID, -1); + if (id < 0) { + return null; + } + return fromId(id); + } + + + public int getId() { + return mId; + } + + @SuppressWarnings("unchecked") + public T get(String key) { + return (T) mMap.get(key); + } + + public IntentExtras put(String key, Object value) { + mMap.put(key, value); + return this; + } + + public IntentExtras putAll(IntentExtras extras) { + mMap.putAll(extras.mMap); + return this; + } + + public Intent putInIntent(Intent intent) { + intent.putExtra(EXTRA_ID, mId); + return intent; + } + + public void release() { + extraStore.remove(mId); + mId = -1; + } + + +} diff --git a/app/src/main/java/com/stardust/util/IntentUtil.java b/app/src/main/java/com/stardust/util/IntentUtil.java new file mode 100644 index 00000000..283eed55 --- /dev/null +++ b/app/src/main/java/com/stardust/util/IntentUtil.java @@ -0,0 +1,199 @@ +package com.stardust.util; + +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; + +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.core.content.FileProvider; + +import android.os.Build; +import android.widget.Toast; +import org.autojs.autojs6.R; + +import java.io.File; + + +public class IntentUtil { + + public static boolean chatWithQQ(Context context, String qq) { + try { + String url = "mqqwpa://im/chat?chat_type=wpa&uin=" + qq; + context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + return true; + } catch (Exception exception) { + exception.printStackTrace(); + return false; + } + } + + public static boolean joinQQGroup(Context context, String key) { + Intent intent = new Intent().addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setData(Uri.parse("mqqopensdkapi://bizAgent/qm/qr?url=http%3A%2F%2Fqm.qq.com%2Fcgi-bin%2Fqm%2Fqr%3Ffrom%3Dapp%26p%3Dandroid%26k%3D" + key)); + try { + context.startActivity(intent); + return true; + } catch (Exception e) { + return false; + } + } + + + public static boolean sendMailTo(Context context, String sendTo, @Nullable String title, @Nullable String content) { + try { + Uri uri = Uri.parse("mailto:" + sendTo); + String[] email = {sendTo}; + Intent intent = new Intent(Intent.ACTION_SENDTO, uri).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(Intent.EXTRA_CC, email); + if (title != null) + intent.putExtra(Intent.EXTRA_SUBJECT, title); + if (content != null) + intent.putExtra(Intent.EXTRA_TEXT, content); + context.startActivity(Intent.createChooser(intent, "")); + return true; + } catch (ActivityNotFoundException e) { + e.printStackTrace(); + return false; + } + } + + public static boolean sendMailTo(Context context, String sendTo) { + return sendMailTo(context, sendTo, null, null); + } + + public static boolean browse(Context context, String link) { + try { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(link)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + return true; + } catch (ActivityNotFoundException ignored) { + return false; + } + + } + + public static boolean shareText(Context context, String text) { + try { + context.startActivity(new Intent(Intent.ACTION_SEND) + .putExtra(Intent.EXTRA_TEXT, text) + .setType("text/plain")); + return true; + } catch (ActivityNotFoundException e) { + e.printStackTrace(); + return false; + } + } + + public static boolean goToAppDetailSettings(Context context, String packageName) { + try { + Intent i = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + i.addCategory(Intent.CATEGORY_DEFAULT); + i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + i.setData(Uri.parse("package:" + packageName)); + context.startActivity(i); + return true; + } catch (ActivityNotFoundException ignored) { + return false; + } + } + + public static boolean goToAppDetailSettings(Context context) { + return goToAppDetailSettings(context, context.getPackageName()); + } + + public static void installApk(Context context, String path, String fileProviderAuthority) throws ActivityNotFoundException { + Uri uri = getUriOfFile(context, path, fileProviderAuthority); + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(uri, "application/vnd.android.package-archive"); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + .addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + context.startActivity(intent); + } + + public static void installApkOrToast(Context context, String path, String fileProviderAuthority) { + try { + installApk(context, path, fileProviderAuthority); + } catch (ActivityNotFoundException e) { + e.printStackTrace(); + Toast.makeText(context, R.string.text_activity_not_found_for_apk_installing, Toast.LENGTH_SHORT).show(); + } + } + + public static boolean viewFile(Context context, String path, String fileProviderAuthority) { + String mimeType = MimeTypes.fromFileOr(path, "*/*"); + return viewFile(context, path, mimeType, fileProviderAuthority); + } + + public static Uri getUriOfFile(Context context, String path, String fileProviderAuthority) { + Uri uri; + if (fileProviderAuthority == null) { + uri = Uri.parse("file://" + path); + } else { + uri = FileProvider.getUriForFile(context, fileProviderAuthority, new File(path)); + } + return uri; + } + + public static boolean viewFile(Context context, Uri uri, String mimeType, String fileProviderAuthority) { + if (uri.getScheme().equals("file")) { + return viewFile(context, uri.getPath(), mimeType, fileProviderAuthority); + } else { + try { + context.startActivity(new Intent(Intent.ACTION_VIEW) + .setDataAndType(uri, mimeType) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + .addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)); + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + } + + public static boolean viewFile(Context context, String path, String mimeType, String fileProviderAuthority) { + try { + Uri uri = getUriOfFile(context, path, fileProviderAuthority); + context.startActivity(new Intent(Intent.ACTION_VIEW) + .setDataAndType(uri, mimeType) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + .addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)); + return true; + } catch (ActivityNotFoundException e) { + e.printStackTrace(); + return false; + } + } + + public static boolean editFile(Context context, String path, String fileProviderAuthority) { + try { + String mimeType = MimeTypes.fromFileOr(path, "*/*"); + Uri uri = getUriOfFile(context, path, fileProviderAuthority); + context.startActivity(new Intent(Intent.ACTION_EDIT) + .setDataAndType(uri, mimeType) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + .addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)); + return true; + } catch (ActivityNotFoundException e) { + e.printStackTrace(); + return false; + } + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public static void requestAppUsagePermission(Context context) { + Intent intent = new Intent(android.provider.Settings.ACTION_USAGE_ACCESS_SETTINGS); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + try { + context.startActivity(intent); + } catch (ActivityNotFoundException e) { + e.printStackTrace(); + } + } +} diff --git a/app/src/main/java/com/stardust/util/LimitedHashMap.java b/app/src/main/java/com/stardust/util/LimitedHashMap.java new file mode 100644 index 00000000..0026264b --- /dev/null +++ b/app/src/main/java/com/stardust/util/LimitedHashMap.java @@ -0,0 +1,24 @@ +package com.stardust.util; + +import java.util.LinkedHashMap; + +/** + * Created by Stardust on 2017/3/31. + */ + +public class LimitedHashMap extends LinkedHashMap { + + private int mMaxSize; + + public LimitedHashMap(int maxSize) { + super(4, 0.75f, true); + mMaxSize = maxSize; + } + + @Override + protected boolean removeEldestEntry(Entry eldest) { + return size() > mMaxSize; + } + + +} diff --git a/app/src/main/java/com/stardust/util/MD5.java b/app/src/main/java/com/stardust/util/MD5.java new file mode 100644 index 00000000..aaa6f69c --- /dev/null +++ b/app/src/main/java/com/stardust/util/MD5.java @@ -0,0 +1,32 @@ +package com.stardust.util; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class MD5 { + + public static byte[] md5Bytes(String message) { + try { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + md5.update(message.getBytes()); + return md5.digest(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static String md5(String message) { + byte[] bytes = md5Bytes(message); + StringBuilder hexString = new StringBuilder(32); + for (byte b : bytes) { + String hex = Integer.toHexString(0xFF & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + return hexString.toString(); + + + } +} diff --git a/app/src/main/java/com/stardust/util/MapBuilder.java b/app/src/main/java/com/stardust/util/MapBuilder.java new file mode 100644 index 00000000..3f465b81 --- /dev/null +++ b/app/src/main/java/com/stardust/util/MapBuilder.java @@ -0,0 +1,37 @@ +package com.stardust.util; + +import java.util.HashMap; +import java.util.Map; + +/** + * Created by Stardust on 2017/1/26. + */ + +public class MapBuilder { + + private Map mMap; + + public MapBuilder() { + this(new HashMap()); + } + + public MapBuilder(Map map) { + mMap = map; + } + + public MapBuilder put(K key, V value) { + mMap.put(key, value); + return this; + } + + public Map build() { + return mMap; + } + + public Map putIn(Map map) { + map.putAll(mMap); + return map; + } + + +} diff --git a/app/src/main/java/com/stardust/util/MessageEvent.java b/app/src/main/java/com/stardust/util/MessageEvent.java new file mode 100644 index 00000000..ab5f0c55 --- /dev/null +++ b/app/src/main/java/com/stardust/util/MessageEvent.java @@ -0,0 +1,22 @@ +package com.stardust.util; + +/** + * Created by Stardust on 2017/3/12. + */ + +public class MessageEvent { + + public String message; + public Object param; + + public MessageEvent(String message, Object param) { + this.message = message; + this.param = param; + } + + + public MessageEvent(String message) { + this.message = message; + } + +} diff --git a/app/src/main/java/com/stardust/util/MessageIntent.java b/app/src/main/java/com/stardust/util/MessageIntent.java new file mode 100644 index 00000000..1d4e75ef --- /dev/null +++ b/app/src/main/java/com/stardust/util/MessageIntent.java @@ -0,0 +1,51 @@ +package com.stardust.util; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; + +import java.util.HashMap; + +/** + * Created by Stardust on 2017/6/27. + */ + +public class MessageIntent extends Intent { + + private HashMap mObjectExtras; + + public MessageIntent() { + } + + public MessageIntent(Intent o) { + super(o); + } + + public MessageIntent(String action) { + super(action); + } + + public MessageIntent(String action, Uri uri) { + super(action, uri); + } + + public MessageIntent(Context packageContext, Class cls) { + super(packageContext, cls); + } + + public MessageIntent(String action, Uri uri, Context packageContext, Class cls) { + super(action, uri, packageContext, cls); + } + + public MessageIntent putExtra(String key, Object value) { + if (mObjectExtras == null) { + mObjectExtras = new HashMap<>(); + } + mObjectExtras.put(key, value); + return this; + } + + public Object getObjectExtra(String key) { + return mObjectExtras.get(key); + } +} diff --git a/app/src/main/java/com/stardust/util/MimeTypes.java b/app/src/main/java/com/stardust/util/MimeTypes.java new file mode 100644 index 00000000..debc77ef --- /dev/null +++ b/app/src/main/java/com/stardust/util/MimeTypes.java @@ -0,0 +1,26 @@ +package com.stardust.util; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import android.webkit.MimeTypeMap; + +import static com.stardust.pio.PFiles.getExtension; + +/** + * Created by Stardust on 2018/2/12. + */ + +public class MimeTypes { + + @Nullable + public static String fromFile(String path) { + String ext = getExtension(path); + return android.text.TextUtils.isEmpty(ext) ? "*/*" : MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext); + } + + @NonNull + public static String fromFileOr(String path, String defaultType) { + String mimeType = fromFile(path); + return mimeType == null ? defaultType : mimeType; + } +} diff --git a/app/src/main/java/com/stardust/util/Nath.java b/app/src/main/java/com/stardust/util/Nath.java new file mode 100644 index 00000000..b5f023d2 --- /dev/null +++ b/app/src/main/java/com/stardust/util/Nath.java @@ -0,0 +1,16 @@ +package com.stardust.util; + +/** + * Created by Stardust on 2017/11/26. + */ + +public class Nath { + + public static int min(int... ints) { + int min = ints[0]; + for (int i = 1; i < ints.length; i++) { + min = ints[i] < min ? ints[i] : min; + } + return min; + } +} diff --git a/app/src/main/java/com/stardust/util/NetworkUtils.java b/app/src/main/java/com/stardust/util/NetworkUtils.java new file mode 100644 index 00000000..2140c04b --- /dev/null +++ b/app/src/main/java/com/stardust/util/NetworkUtils.java @@ -0,0 +1,20 @@ +package com.stardust.util; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; + +/** + * Created by Stardust on 2017/4/9. + */ + +public class NetworkUtils { + + public static boolean isWifiAvailable(Context context) { + ConnectivityManager connectivityManager = (ConnectivityManager) context + .getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo activeNetInfo = connectivityManager.getActiveNetworkInfo(); + return activeNetInfo != null + && activeNetInfo.getType() == ConnectivityManager.TYPE_WIFI; + } +} diff --git a/app/src/main/java/com/stardust/util/ObjectHelper.java b/app/src/main/java/com/stardust/util/ObjectHelper.java new file mode 100644 index 00000000..97384662 --- /dev/null +++ b/app/src/main/java/com/stardust/util/ObjectHelper.java @@ -0,0 +1,18 @@ +package com.stardust.util; + +public class ObjectHelper { + + public static void requireNonNull(Object obj, String name){ + if(obj == null){ + throw new NullPointerException(name + " should not be null"); + } + } + + public static void requireNonNull(Object obj){ + if(obj == null){ + throw new NullPointerException(); + } + } + + +} diff --git a/app/src/main/java/com/stardust/util/Objects.java b/app/src/main/java/com/stardust/util/Objects.java new file mode 100644 index 00000000..17466d6a --- /dev/null +++ b/app/src/main/java/com/stardust/util/Objects.java @@ -0,0 +1,40 @@ +package com.stardust.util; + +/** + * Created by Stardust on 2017/12/31. + */ + +public class Objects { + + /** + * Returns {@code true} if the arguments are equal to each other + * and {@code false} otherwise. + * Consequently, if both arguments are {@code null}, {@code true} + * is returned and if exactly one argument is {@code null}, {@code + * false} is returned. Otherwise, equality is determined by using + * the {@link Object#equals equals} method of the first + * argument. + * + * @param a an object + * @param b an object to be compared with {@code a} for equality + * @return {@code true} if the arguments are equal to each other + * and {@code false} otherwise + * @see Object#equals(Object) + */ + public static boolean equals(Object a, Object b) { + return (a == b) || (a != null && a.equals(b)); + } + + /** + * Returns the hash code of a non-{@code null} argument and 0 for + * a {@code null} argument. + * + * @param o an object + * @return the hash code of a non-{@code null} argument and 0 for + * a {@code null} argument + * @see Object#hashCode + */ + public static int hashCode(Object o) { + return o != null ? o.hashCode() : 0; + } +} diff --git a/app/src/main/java/com/stardust/util/ResourceMonitor.java b/app/src/main/java/com/stardust/util/ResourceMonitor.java new file mode 100644 index 00000000..de2752ae --- /dev/null +++ b/app/src/main/java/com/stardust/util/ResourceMonitor.java @@ -0,0 +1,122 @@ +package com.stardust.util; + +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.util.SparseArray; +import org.autojs.autojs6.BuildConfig; + +import java.util.concurrent.ConcurrentHashMap; + +public final class ResourceMonitor { + + private static final String LOG_TAG = "ResourceMonitor"; + + private static final ConcurrentHashMap, SparseArray> mResources = new ConcurrentHashMap<>(); + private static Handler sHandler; + private static boolean sEnabled = BuildConfig.DEBUG; + private static ExceptionCreator sExceptionCreator; + private static UnclosedResourceDetectedHandler sUnclosedResourceDetectedHandler; + + public static void setExceptionCreator(ExceptionCreator exceptionCreator) { + sExceptionCreator = exceptionCreator; + } + + public static void setUnclosedResourceDetectedHandler(UnclosedResourceDetectedHandler unclosedResourceDetectedHandler) { + sUnclosedResourceDetectedHandler = unclosedResourceDetectedHandler; + } + + public static void onOpen(ResourceMonitor.Resource resource) { + if (!sEnabled) { + return; + } + SparseArray map = mResources.get(resource.getClass()); + if (map == null) { + map = new SparseArray<>(); + mResources.put(resource.getClass(), map); + } + int resourceId = resource.getResourceId(); + Exception exception; + if (sExceptionCreator == null) { + exception = new ResourceMonitor.UnclosedResourceException(resource); + exception.fillInStackTrace(); + } else { + exception = sExceptionCreator.create(resource); + } + map.put(resourceId, exception); + } + + public static void onClose(ResourceMonitor.Resource resource) { + if (!sEnabled) { + return; + } + SparseArray map = mResources.get(resource.getClass()); + if (map != null) { + map.remove(resource.getResourceId()); + } + } + + public static void onFinalize(ResourceMonitor.Resource resource) { + if (!sEnabled) { + return; + } + SparseArray map = mResources.get(resource.getClass()); + if (map != null) { + int indexOfKey = map.indexOfKey(resource.getResourceId()); + if (indexOfKey >= 0) { + final Exception exception = map.valueAt(indexOfKey); + map.removeAt(indexOfKey); + if (sHandler == null) { + sHandler = new Handler(Looper.getMainLooper()); + } + sHandler.post(new Runnable() { + public final void run() { + UnclosedResourceDetectedException detectedException = new UnclosedResourceDetectedException(exception); + detectedException.fillInStackTrace(); + Log.w(LOG_TAG, "UnclosedResourceDetected", detectedException); + if (sUnclosedResourceDetectedHandler != null) { + sUnclosedResourceDetectedHandler.onUnclosedResourceDetected(detectedException); + } else { + throw detectedException; + } + } + }); + } + } + } + + public static boolean isEnabled() { + return sEnabled; + } + + public static void setEnabled(boolean mEnabled) { + ResourceMonitor.sEnabled = mEnabled; + } + + public static final class UnclosedResourceException extends RuntimeException { + public UnclosedResourceException(Resource resource) { + super("id = " + resource.getResourceId() + ", resource = " + resource); + } + + } + + + public static final class UnclosedResourceDetectedException extends RuntimeException { + public UnclosedResourceDetectedException(Throwable cause) { + super(cause); + } + } + + public interface Resource { + int getResourceId(); + } + + public interface ExceptionCreator { + Exception create(Resource resource); + } + + public interface UnclosedResourceDetectedHandler { + + void onUnclosedResourceDetected(UnclosedResourceDetectedException detectedException); + } +} diff --git a/app/src/main/java/com/stardust/util/ScreenMetrics.java b/app/src/main/java/com/stardust/util/ScreenMetrics.java new file mode 100644 index 00000000..9072a80b --- /dev/null +++ b/app/src/main/java/com/stardust/util/ScreenMetrics.java @@ -0,0 +1,130 @@ +package com.stardust.util; + +import android.app.Activity; +import android.content.res.Configuration; +import android.graphics.Point; +import android.util.DisplayMetrics; +import android.view.Display; +import android.view.Surface; + +import static java.lang.System.out; + +/** + * Created by Stardust on 2017/4/26. + */ + +public class ScreenMetrics { + + private static int deviceScreenHeight; + private static int deviceScreenWidth; + private static boolean initialized = false; + private static int deviceScreenDensity; + private static Display display; + + public static void initIfNeeded(Activity activity) { + if (initialized) + return; + DisplayMetrics metrics = new DisplayMetrics(); + activity.getWindowManager().getDefaultDisplay().getRealMetrics(metrics); + deviceScreenHeight = metrics.heightPixels; + deviceScreenWidth = metrics.widthPixels; + deviceScreenDensity = metrics.densityDpi; + display = activity.getWindowManager().getDefaultDisplay(); + initialized = true; + } + + public static int getDeviceScreenHeight() { + return deviceScreenHeight; + } + + public static int getDeviceScreenWidth() { + return deviceScreenWidth; + } + + public static int getDeviceScreenDensity() { + return deviceScreenDensity; + } + + public static int getOrientationAwareScreenWidth(int orientation) { + if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + return getDeviceScreenHeight(); + } else { + return getDeviceScreenWidth(); + } + } + + public static int getOrientationAwareScreenHeight(int orientation) { + if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + return getDeviceScreenWidth(); + } else { + return getDeviceScreenHeight(); + } + } + + public static int scaleX(int x, int width) { + if (width == 0 || !initialized) + return x; + return x * deviceScreenWidth / width; + } + + public static int scaleY(int y, int height) { + if (height == 0 || !initialized) + return y; + return y * deviceScreenHeight / height; + } + + public static int rescaleX(int x, int width) { + if (width == 0 || !initialized) + return x; + return x * width / deviceScreenWidth; + } + + public static int rescaleY(int y, int height) { + if (height == 0 || !initialized) + return y; + return y * height / deviceScreenHeight; + } + + + private int mDesignWidth; + private int mDesignHeight; + + public ScreenMetrics(int designWidth, int designHeight) { + mDesignWidth = designWidth; + mDesignHeight = designHeight; + } + + public ScreenMetrics() { + } + + public void setDesignWidth(int designWidth) { + mDesignWidth = designWidth; + } + + public void setDesignHeight(int designHeight) { + mDesignHeight = designHeight; + } + + public int scaleX(int x) { + return scaleX(x, mDesignWidth); + } + + public int scaleY(int y) { + return scaleY(y, mDesignHeight); + } + + + public void setScreenMetrics(int width, int height) { + mDesignWidth = width; + mDesignHeight = height; + } + + public int rescaleX(int x) { + return rescaleX(x, mDesignWidth); + } + + + public int rescaleY(int y) { + return rescaleY(y, mDesignHeight); + } +} diff --git a/app/src/main/java/com/stardust/util/SdkVersionUtil.java b/app/src/main/java/com/stardust/util/SdkVersionUtil.java new file mode 100644 index 00000000..d66be0a7 --- /dev/null +++ b/app/src/main/java/com/stardust/util/SdkVersionUtil.java @@ -0,0 +1,24 @@ +package com.stardust.util; + +/** + * Created by Stardust on 2017/4/3. + */ + +public class SdkVersionUtil { + + private static final String[] SDK_VERSIONS = { + "1.0", "1.1", "1.5", "1.6", "2.0", "2.0.1", "2.1.x", "2.2.x", + "2.3", "2.3.3", "3.0.x", "3.1.x", "3.2", "4.0", "4.0.3", "4.1", + "4.2", "4.3", "4.4.2", "4.4W", "5.0", "5.1", "6.0", "7.0", "7.1", + "8.0" + }; + + public static String sdkIntToString(int i) { + if (i > 26) { + return "Unknown"; + } + return SDK_VERSIONS[i - 1]; + } + + +} diff --git a/app/src/main/java/com/stardust/util/SimpleCache.java b/app/src/main/java/com/stardust/util/SimpleCache.java new file mode 100644 index 00000000..22e76e27 --- /dev/null +++ b/app/src/main/java/com/stardust/util/SimpleCache.java @@ -0,0 +1,118 @@ +package com.stardust.util; + +import java.util.Iterator; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; + +/** + * Created by Stardust on 2017/4/5. + */ + +public class SimpleCache { + + public interface Supplier { + T get(String key); + } + + private long mPersistTime; + private LimitedHashMap> mCache; + private Timer mCacheCheckTimer; + private Supplier mSupplier; + + public SimpleCache(long persistTime, int cacheSize, long checkInterval, Supplier supplier) { + mPersistTime = persistTime; + mCache = new LimitedHashMap<>(cacheSize); + mSupplier = supplier == null ? new NullSupplier() : supplier; + mCacheCheckTimer = new Timer(); + startCacheCheck(checkInterval); + } + + public SimpleCache(long persistTime, int cacheSize, long checkInterval) { + this(persistTime, cacheSize, checkInterval, null); + } + + public synchronized void put(String key, T value) { + mCache.put(key, new Item<>(value)); + } + + public synchronized T get(String key) { + Item item = mCache.get(key); + if (item == null) { + T value = mSupplier.get(key); + if (value != null) { + put(key, value); + } + return value; + } + return item.value; + } + + public T get(String key, T defaultValue) { + T value = get(key); + if (value == null) { + value = defaultValue; + } + return value; + } + + public T get(String key, Supplier supplier) { + T value = get(key); + if (value == null) { + value = supplier.get(key); + put(key, value); + } + return value; + } + + public synchronized void destroy() { + mCacheCheckTimer.cancel(); + mCache.clear(); + } + + private void startCacheCheck(long checkInterval) { + mCacheCheckTimer.schedule(new TimerTask() { + @Override + public void run() { + checkCache(); + } + }, 0, checkInterval); + } + + + private synchronized void checkCache() { + Iterator>> iterator = mCache.entrySet().iterator(); + while (iterator.hasNext()) { + if (!iterator.next().getValue().isValid()) { + iterator.remove(); + } + } + } + + private class Item { + + T value; + private long mSaveMillis; + + + Item(T value) { + mSaveMillis = System.currentTimeMillis(); + this.value = value; + } + + boolean isValid() { + return System.currentTimeMillis() - mSaveMillis <= mPersistTime; + } + + } + + private static class NullSupplier implements Supplier { + + @Override + public T get(String key) { + return null; + } + } + + +} diff --git a/app/src/main/java/com/stardust/util/SparseArrayEntries.java b/app/src/main/java/com/stardust/util/SparseArrayEntries.java new file mode 100644 index 00000000..9b050217 --- /dev/null +++ b/app/src/main/java/com/stardust/util/SparseArrayEntries.java @@ -0,0 +1,24 @@ +package com.stardust.util; + +import androidx.annotation.NonNull; +import android.util.SparseArray; + +/** + * Created by Stardust on 2017/1/26. + */ + +public class SparseArrayEntries { + + private final SparseArray mSparseArray = new SparseArray<>(); + + public SparseArrayEntries entry(int key, E value) { + mSparseArray.put(key, value); + return this; + } + + @NonNull + public SparseArray sparseArray() { + return mSparseArray; + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/stardust/util/StateObserver.java b/app/src/main/java/com/stardust/util/StateObserver.java new file mode 100644 index 00000000..c97df231 --- /dev/null +++ b/app/src/main/java/com/stardust/util/StateObserver.java @@ -0,0 +1,93 @@ +package com.stardust.util; + +import android.content.SharedPreferences; + +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Created by Stardust on 2017/2/3. + */ + +public class StateObserver { + + public interface OnStateChangedListener { + + void onStateChanged(boolean newState); + + void initState(boolean state); + } + + + public static abstract class SimpleOnStateChangedListener implements OnStateChangedListener { + + @Override + public void initState(boolean state) { + onStateChanged(state); + } + } + + + private final Map> mKeyStateListenersMap = new HashMap<>(); + private SharedPreferences mSharedPreferences; + + public StateObserver(SharedPreferences sharedPreferences) { + mSharedPreferences = sharedPreferences; + } + + + public void register(String key, OnStateChangedListener listener) { + initState(key, listener); + synchronized (mKeyStateListenersMap) { + List listeners = getListenerListOrCreateIfNotExists(key); + listeners.add(listener); + } + } + + + private void unregister(String key, OnStateChangedListener stateChangedListener) { + synchronized (mKeyStateListenersMap) { + List listeners = mKeyStateListenersMap.get(key); + if (listeners == null) { + return; + } + listeners.remove(stateChangedListener); + } + } + + public void setState(String key, boolean state) { + synchronized (mKeyStateListenersMap) { + List listeners = mKeyStateListenersMap.get(key); + if (listeners == null || listeners.isEmpty()) + return; + mSharedPreferences.edit().putBoolean(key, state).apply(); + notifyBooleanStateChanged(listeners, state); + } + } + + private void notifyBooleanStateChanged(List listeners, boolean state) { + for (OnStateChangedListener listener : listeners) { + listener.onStateChanged(state); + } + } + + private void initState(String key, OnStateChangedListener listener) { + if (mSharedPreferences.contains(key)) { + listener.initState(mSharedPreferences.getBoolean(key, false)); + } + } + + private List getListenerListOrCreateIfNotExists(String key) { + List listeners = mKeyStateListenersMap.get(key); + if (listeners == null) { + listeners = new CopyOnWriteArrayList<>(); + mKeyStateListenersMap.put(key, listeners); + } + return listeners; + } + + +} diff --git a/app/src/main/java/com/stardust/util/Supplier.java b/app/src/main/java/com/stardust/util/Supplier.java new file mode 100644 index 00000000..07109142 --- /dev/null +++ b/app/src/main/java/com/stardust/util/Supplier.java @@ -0,0 +1,9 @@ +package com.stardust.util; + +/** + * Created by Stardust on 2017/5/1. + */ + +public interface Supplier { + T get(); +} diff --git a/app/src/main/java/com/stardust/util/TextUtils.java b/app/src/main/java/com/stardust/util/TextUtils.java new file mode 100644 index 00000000..03435990 --- /dev/null +++ b/app/src/main/java/com/stardust/util/TextUtils.java @@ -0,0 +1,149 @@ +package com.stardust.util; + +import androidx.annotation.NonNull; + +/** + * Created by Stardust on 2017/5/3. + */ + +public class TextUtils { + + public static String join(CharSequence delimiter, Object... tokens) { + return android.text.TextUtils.join(delimiter, tokens); + } + + + @NonNull + public static String toEmptyIfNull(String message) { + if (message == null) + return ""; + return message; + } + + public static int lastIndexOf(CharSequence text, char ch, int fromIndex) { + if (text instanceof String) { + return ((String) text).lastIndexOf(ch, fromIndex); + } + int i = Math.min(fromIndex, text.length() - 1); + for (; i >= 0; i--) { + if (text.charAt(i) == ch) { + return i; + } + } + return -1; + + } + + public static int indexOf(CharSequence text, char ch, int fromIndex) { + if (text instanceof String) { + return ((String) text).indexOf(ch, fromIndex); + } + final int max = text.length(); + if (fromIndex < 0) { + fromIndex = 0; + } else if (fromIndex >= max) { + // Note: fromIndex might be near -1>>>1. + return -1; + } + + // handle most cases here (ch is a BMP code point or a + // negative value (invalid code point)) + for (int i = fromIndex; i < max; i++) { + if (text.charAt(i) == ch) { + return i; + } + } + return -1; + } + + public static int indexOf(CharSequence source, + CharSequence target, + int fromIndex) { + if (source instanceof String && target instanceof String) { + return ((String) source).indexOf((String) target); + } + if (fromIndex >= source.length()) { + return (target.length() == 0 ? source.length() : -1); + } + if (fromIndex < 0) { + fromIndex = 0; + } + if (target.length() == 0) { + return fromIndex; + } + + char first = target.charAt(0); + int max = (source.length() - target.length()); + + for (int i = fromIndex; i <= max; i++) { + /* Look for first character. */ + if (source.charAt(i) != first) { + while (++i <= max && source.charAt(i) != first) ; + } + + /* Found first character, now look at the rest of v2 */ + if (i <= max) { + int j = i + 1; + int end = j + target.length() - 1; + for (int k = 1; j < end && source.charAt(j) + == target.charAt(k); j++, k++) + ; + + if (j == end) { + /* Found whole string. */ + return i; + } + } + } + return -1; + } + + public static int lastIndexOf(CharSequence source, + CharSequence target, + int fromIndex) { + if (source instanceof String && target instanceof String) { + return ((String) source).lastIndexOf((String) target); + } + /* + * Check arguments; return immediately where possible. For + * consistency, don't check for null str. + */ + int rightIndex = source.length() - target.length(); + if (fromIndex < 0) { + return -1; + } + if (fromIndex > rightIndex) { + fromIndex = rightIndex; + } + /* Empty string always matches. */ + if (target.length() == 0) { + return fromIndex; + } + + int strLastIndex = target.length() - 1; + char strLastChar = target.charAt(strLastIndex); + int min = target.length() - 1; + int i = min + fromIndex; + + startSearchForLastChar: + while (true) { + while (i >= min && source.charAt(i) != strLastChar) { + i--; + } + if (i < min) { + return -1; + } + int j = i - 1; + int start = j - (target.length() - 1); + int k = strLastIndex - 1; + + while (j > start) { + if (source.charAt(j--) != target.charAt(k--)) { + i--; + continue startSearchForLastChar; + } + } + return start + 1; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/stardust/util/UiHandler.java b/app/src/main/java/com/stardust/util/UiHandler.java new file mode 100644 index 00000000..31234a0b --- /dev/null +++ b/app/src/main/java/com/stardust/util/UiHandler.java @@ -0,0 +1,43 @@ +package com.stardust.util; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.widget.Toast; + +/** + * Created by Stardust on 2017/5/2. + */ + +public class UiHandler extends Handler { + + + private Context mContext; + + public UiHandler(Context context) { + super(Looper.getMainLooper()); + mContext = context; + } + + public Context getContext() { + return mContext; + } + + public void toast(final String message) { + post(new Runnable() { + @Override + public void run() { + Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show(); + } + }); + } + + public void toast(final int resId) { + post(new Runnable() { + @Override + public void run() { + Toast.makeText(mContext, resId, Toast.LENGTH_SHORT).show(); + } + }); + } +} diff --git a/app/src/main/java/com/stardust/util/UnderuseExecutors.java b/app/src/main/java/com/stardust/util/UnderuseExecutors.java new file mode 100644 index 00000000..ddb287c3 --- /dev/null +++ b/app/src/main/java/com/stardust/util/UnderuseExecutors.java @@ -0,0 +1,22 @@ +package com.stardust.util; + +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Created by Stardust on 2017/5/2. + */ + +public class UnderuseExecutors { + + private static ExecutorService mExecutor = Executors.newSingleThreadExecutor(); + + public static void execute(Runnable runnable) { + mExecutor.execute(runnable); + } + + public static ExecutorService getExecutor() { + return mExecutor; + } +} diff --git a/app/src/main/java/com/stardust/util/ViewUtil.java b/app/src/main/java/com/stardust/util/ViewUtil.java new file mode 100644 index 00000000..61b58b54 --- /dev/null +++ b/app/src/main/java/com/stardust/util/ViewUtil.java @@ -0,0 +1,50 @@ +package com.stardust.util; + +import android.app.Activity; +import android.content.Context; +import androidx.annotation.IdRes; +import android.util.DisplayMetrics; +import android.view.View; +import android.view.ViewGroup; + +/** + * Created by Stardust on 2017/1/24. + */ + +public class ViewUtil { + + @SuppressWarnings("unchecked") + public static V $(View view, @IdRes int resId) { + return (V) view.findViewById(resId); + } + + // FIXME: 2018/1/23 not working in some devices (https://github.com/hyb1996/Auto.js/issues/268) + public static int getStatusBarHeight(Context context) { + int result = 0; + int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android"); + if (resourceId > 0) { + result = context.getResources().getDimensionPixelSize(resourceId); + } + return result; + } + + public static int getScreenHeight(Activity activity) { + DisplayMetrics displayMetrics = new DisplayMetrics(); + activity.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); + return displayMetrics.heightPixels; + } + + public static int getScreenWidth(Activity activity) { + DisplayMetrics displayMetrics = new DisplayMetrics(); + activity.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); + return displayMetrics.widthPixels; + } + + + public static void setViewMeasure(View view, int width, int height) { + ViewGroup.LayoutParams params = view.getLayoutParams(); + params.width = width; + params.height = height; + view.setLayoutParams(params); + } +} diff --git a/app/src/main/java/com/stardust/util/ViewUtils.java b/app/src/main/java/com/stardust/util/ViewUtils.java new file mode 100644 index 00000000..8d1c8cde --- /dev/null +++ b/app/src/main/java/com/stardust/util/ViewUtils.java @@ -0,0 +1,29 @@ +package com.stardust.util; + +import android.content.Context; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewParent; + +/** + * Created by Stardust on 2017/7/2. + */ + +public class ViewUtils { + + public static View findParentById(View view, int id) { + ViewParent parent = view.getParent(); + if (parent == null || !(parent instanceof View)) + return null; + View viewParent = (View) parent; + if (viewParent.getId() == id) { + return viewParent; + } + return findParentById(viewParent, id); + } + + public static float pxToSp(Context context, float px) { + float scaledDensity = context.getResources().getDisplayMetrics().scaledDensity; + return px / scaledDensity; + } +} diff --git a/app/src/main/java/com/stardust/view/accessibility/AccessibilityDelegate.kt b/app/src/main/java/com/stardust/view/accessibility/AccessibilityDelegate.kt new file mode 100644 index 00000000..7326e77c --- /dev/null +++ b/app/src/main/java/com/stardust/view/accessibility/AccessibilityDelegate.kt @@ -0,0 +1,27 @@ +package com.stardust.view.accessibility + +import android.accessibilityservice.AccessibilityService +import android.view.accessibility.AccessibilityEvent +import android.view.accessibility.AccessibilityNodeInfo + +/** + * Created by Stardust on 2017/2/14. + */ + +interface AccessibilityDelegate { + + /** + * 返回调用onAccessibilityEvent时的EventType的集合。如果需要对所有EventType都有效,返回null。 + * + * @return + */ + val eventTypes: Set? + + fun onAccessibilityEvent(service: AccessibilityService, event: AccessibilityEvent): Boolean + + companion object { + + val ALL_EVENT_TYPES: Set? = null + } + +} diff --git a/app/src/main/java/com/stardust/view/accessibility/AccessibilityNodeInfoAllocator.kt b/app/src/main/java/com/stardust/view/accessibility/AccessibilityNodeInfoAllocator.kt new file mode 100644 index 00000000..5927881b --- /dev/null +++ b/app/src/main/java/com/stardust/view/accessibility/AccessibilityNodeInfoAllocator.kt @@ -0,0 +1,183 @@ +@file:Suppress("DEPRECATION") + +package com.stardust.view.accessibility + +import android.util.Log +import android.view.accessibility.AccessibilityNodeInfo +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat +import com.stardust.automator.BuildConfig +import java.util.* + +/** + * Created by Stardust on 2017/3/22. + */ + +open class AccessibilityNodeInfoAllocator { + + private val mAccessibilityNodeInfoList = HashMap() + + open fun getChild(parent: AccessibilityNodeInfo, i: Int): AccessibilityNodeInfo? { + return add(parent.getChild(i)) + } + + open fun getChild(parent: AccessibilityNodeInfoCompat, i: Int): AccessibilityNodeInfoCompat { + val compat = parent.getChild(i) + add(compat.info as AccessibilityNodeInfo) + return compat + } + + open fun getParent(n: AccessibilityNodeInfo): AccessibilityNodeInfo? { + return add(n.parent) + } + + + open fun getParent(n: AccessibilityNodeInfoCompat): AccessibilityNodeInfoCompat { + val compat = n.parent + add(compat.info as AccessibilityNodeInfo) + return compat + } + + open fun findAccessibilityNodeInfosByText(root: AccessibilityNodeInfo, text: String): List { + val list = root.findAccessibilityNodeInfosByText(text) + addAll(list) + return list + } + + + open fun findAccessibilityNodeInfosByText(root: AccessibilityNodeInfoCompat, text: String): List { + val list = root.findAccessibilityNodeInfosByText(text) + addAll(list) + return list + } + + open fun findAccessibilityNodeInfosByViewId(root: AccessibilityNodeInfo, id: String): List { + val list = root.findAccessibilityNodeInfosByViewId(id) + addAll(list) + return list + } + + + open fun findAccessibilityNodeInfosByViewId(root: AccessibilityNodeInfoCompat, id: String): List { + val list = root.findAccessibilityNodeInfosByViewId(id) + addAll(list) + return list + } + + open fun recycle(nodeInfo: AccessibilityNodeInfo) { + @Suppress("DEPRECATION") + nodeInfo.recycle() + mAccessibilityNodeInfoList.remove(nodeInfo) + } + + open fun recycle(nodeInfo: AccessibilityNodeInfoCompat) { + @Suppress("DEPRECATION") + recycle(nodeInfo.info as AccessibilityNodeInfo) + } + + open fun recycleAll(): Int { + var notRecycledCount = 0 + val size = mAccessibilityNodeInfoList.size + mAccessibilityNodeInfoList.forEach { (key, _) -> + try { + @Suppress("DEPRECATION") + key.recycle() + notRecycledCount++ + } catch (ignored: IllegalStateException) { + } + } + Log.v(TAG, "Total: $size Not recycled: $notRecycledCount") + return notRecycledCount + } + + fun add(nodeInfo: AccessibilityNodeInfo?): AccessibilityNodeInfo? { + val stackTrace = if (DEBUG) Arrays.toString(Thread.currentThread().stackTrace) else null + if (nodeInfo != null) + mAccessibilityNodeInfoList[nodeInfo] = stackTrace + return nodeInfo + } + + private fun addAll(nodeInfos: Collection<*>) { + val stackTrace = if (DEBUG) Arrays.toString(Thread.currentThread().stackTrace) else null + for (nodeInfo in nodeInfos) { + if (nodeInfo is AccessibilityNodeInfo) { + mAccessibilityNodeInfoList[nodeInfo] = stackTrace + } else if (nodeInfo is AccessibilityNodeInfoCompat) { + @Suppress("DEPRECATION") + mAccessibilityNodeInfoList[nodeInfo.info as AccessibilityNodeInfo] = stackTrace + } + } + } + + private class NoOpAllocator : AccessibilityNodeInfoAllocator() { + + override fun getParent(n: AccessibilityNodeInfoCompat): AccessibilityNodeInfoCompat { + return n.parent + } + + override fun getParent(n: AccessibilityNodeInfo): AccessibilityNodeInfo? { + return n.parent + } + + override fun getChild(parent: AccessibilityNodeInfoCompat, i: Int): AccessibilityNodeInfoCompat { + return parent.getChild(i) + } + + override fun getChild(parent: AccessibilityNodeInfo, i: Int): AccessibilityNodeInfo? { + return parent.getChild(i) + } + + override fun findAccessibilityNodeInfosByViewId(root: AccessibilityNodeInfoCompat, id: String): List { + return root.findAccessibilityNodeInfosByViewId(id) + } + + + override fun findAccessibilityNodeInfosByViewId(root: AccessibilityNodeInfo, id: String): List { + return root.findAccessibilityNodeInfosByViewId(id) + } + + override fun findAccessibilityNodeInfosByText(root: AccessibilityNodeInfo, text: String): List { + return root.findAccessibilityNodeInfosByText(text) + } + + override fun findAccessibilityNodeInfosByText(root: AccessibilityNodeInfoCompat, text: String): List { + return root.findAccessibilityNodeInfosByText(text) + } + + override fun recycle(nodeInfo: AccessibilityNodeInfo) { + super.recycle(nodeInfo) + } + + override fun recycle(nodeInfo: AccessibilityNodeInfoCompat) { + super.recycle(nodeInfo) + } + + override fun recycleAll(): Int { + return -1 + } + + + } + + companion object { + + val NONE: AccessibilityNodeInfoAllocator = NoOpAllocator() + + private val TAG = "AccessibilityAllocator" + private val DEBUG = BuildConfig.DEBUG + + + val global = AccessibilityNodeInfoAllocator() + + fun recycleList(root: AccessibilityNodeInfo, list: List) { + for (nodeInfo in list) { + if (nodeInfo !== root) { + //// FIXME: 2017/5/1 Issue #180 + @Suppress("DEPRECATION") + nodeInfo.recycle() + } + } + } + } + + +} diff --git a/app/src/main/java/com/stardust/view/accessibility/AccessibilityNodeInfoHelper.kt b/app/src/main/java/com/stardust/view/accessibility/AccessibilityNodeInfoHelper.kt new file mode 100644 index 00000000..bcf0dc61 --- /dev/null +++ b/app/src/main/java/com/stardust/view/accessibility/AccessibilityNodeInfoHelper.kt @@ -0,0 +1,65 @@ +package com.stardust.view.accessibility + +import android.graphics.Rect +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat +import android.view.accessibility.AccessibilityNodeInfo + +import com.stardust.automator.UiObject + +/** + * Created by Stardust on 2017/3/6. + */ + +object AccessibilityNodeInfoHelper { + + /** + * Returns the node's bounds clipped to the size of the display + * + * @param node + * @param width pixel width of the display + * @param height pixel height of the display + * @return null if node is null, else a Rect containing visible bounds + */ + fun getVisibleBoundsInScreen(node: AccessibilityNodeInfo?, width: Int, height: Int): Rect? { + if (node == null) { + return null + } + // targeted node's bounds + val nodeRect = Rect() + node.getBoundsInScreen(nodeRect) + + val displayRect = Rect() + displayRect.top = 0 + displayRect.left = 0 + displayRect.right = width + displayRect.bottom = height + val intersect = nodeRect.intersect(displayRect) + return nodeRect + } + + fun getBoundsInParent(nodeInfo: AccessibilityNodeInfo): Rect { + val rect = Rect() + @Suppress("DEPRECATION") + nodeInfo.getBoundsInParent(rect) + return rect + } + + fun getBoundsInScreen(nodeInfo: AccessibilityNodeInfo): Rect { + val rect = Rect() + nodeInfo.getBoundsInScreen(rect) + return rect + } + + fun getBoundsInScreen(nodeInfo: AccessibilityNodeInfoCompat): Rect { + val rect = Rect() + nodeInfo.getBoundsInScreen(rect) + return rect + } + + fun getBoundsInParent(nodeInfo: AccessibilityNodeInfoCompat): Rect { + val rect = Rect() + @Suppress("DEPRECATION") + nodeInfo.getBoundsInParent(rect) + return rect + } +} diff --git a/app/src/main/java/com/stardust/view/accessibility/AccessibilityNotificationObserver.kt b/app/src/main/java/com/stardust/view/accessibility/AccessibilityNotificationObserver.kt new file mode 100644 index 00000000..85925f47 --- /dev/null +++ b/app/src/main/java/com/stardust/view/accessibility/AccessibilityNotificationObserver.kt @@ -0,0 +1,118 @@ +package com.stardust.view.accessibility + +import android.accessibilityservice.* +import android.content.Context +import android.util.Log +import android.view.accessibility.AccessibilityEvent + +import com.stardust.notification.Notification + +import java.util.ArrayList +import java.util.Collections +import java.util.concurrent.CopyOnWriteArrayList + +/** + * Created by Stardust on 2017/11/3. + */ + +class AccessibilityNotificationObserver(private val mContext: Context) : NotificationListener, AccessibilityDelegate { + private val mNotificationListeners = CopyOnWriteArrayList() + private val mToastListeners = CopyOnWriteArrayList() + + override val eventTypes: Set? + get() = EVENT_TYPES + + inner class Toast(val packageName: String, texts: List) { + val texts: MutableList + + val text: String? + get() { + return if (texts.isEmpty()) { + null + } else texts[0] + } + + init { + this.texts = ArrayList(texts.size) + for (t in texts) { + this.texts.add(t.toString()) + } + } + + override fun toString(): String { + return "Toast{" + + "texts=" + texts + + ", packageName='" + packageName + '\''.toString() + + '}'.toString() + } + } + + interface ToastListener { + fun onToast(toast: Toast) + } + + + fun addNotificationListener(listener: NotificationListener) { + mNotificationListeners.add(listener) + } + + fun removeNotificationListener(listener: NotificationListener): Boolean { + return mNotificationListeners.remove(listener) + } + + + fun addToastListener(listener: ToastListener) { + mToastListeners.add(listener) + } + + fun removeToastListener(listener: ToastListener): Boolean { + return mToastListeners.remove(listener) + } + + override fun onAccessibilityEvent(service: android.accessibilityservice.AccessibilityService, event: AccessibilityEvent): Boolean { + if (event.parcelableData is Notification) { + val notification = event.parcelableData as android.app.Notification + Log.d(TAG, "onNotification: $notification; $event") + onNotification(Notification.create(notification, event.packageName.toString())) + } else { + val list = event.text + Log.d(TAG, "onNotification: $list; $event") + if (event.packageName == mContext.packageName) { + return false + } + onToast(event, Toast(event.packageName.toString(), list)) + } + + return false + } + + private fun onToast(event: AccessibilityEvent, toast: Toast) { + for (listener in mToastListeners) { + try { + listener.onToast(toast) + } catch (e: Exception) { + Log.e(TAG, "Error onNotification: $toast Listener: $listener", e) + } + + } + } + + override fun onNotification(notification: Notification) { + for (listener in mNotificationListeners) { + try { + listener.onNotification(notification) + } catch (e: Exception) { + Log.e(TAG, "Error onNotification: $notification Listener: $listener", e) + } + + } + } + + companion object { + + private val EVENT_TYPES = setOf(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED) + + private val TAG = "NotificationObserver" + private val EMPTY = arrayOfNulls(0) + } +} diff --git a/app/src/main/java/com/stardust/view/accessibility/AccessibilityService.kt b/app/src/main/java/com/stardust/view/accessibility/AccessibilityService.kt new file mode 100644 index 00000000..a580aa66 --- /dev/null +++ b/app/src/main/java/com/stardust/view/accessibility/AccessibilityService.kt @@ -0,0 +1,174 @@ +package com.stardust.view.accessibility + +import android.os.Build +import android.util.Log +import android.view.KeyEvent +import android.view.accessibility.AccessibilityEvent +import android.view.accessibility.AccessibilityNodeInfo +import com.stardust.event.EventDispatcher + +import java.util.HashSet +import java.util.TreeMap +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.locks.ReentrantLock + +/** + * Created by Stardust on 2017/5/2. + */ + + +open class AccessibilityService : android.accessibilityservice.AccessibilityService() { + + interface GestureListener { + fun onGesture(gestureId: Int) + } + + val onKeyObserver = OnKeyListener.Observer() + val keyInterrupterObserver = KeyInterceptor.Observer() + val gestureEventDispatcher = EventDispatcher() + private var mEventExecutor: ExecutorService? = null + private var mFastRootInActiveWindow: AccessibilityNodeInfo? = null + private val eventExecutor: ExecutorService + get() { + return mEventExecutor ?: run { + val executor = Executors.newSingleThreadExecutor() + mEventExecutor = executor + executor + } + } + + override fun onAccessibilityEvent(event: AccessibilityEvent) { + instance = this + // Log.v(TAG, "onAccessibilityEvent: $event"); + if (!containsAllEventTypes && !eventTypes.contains(event.eventType)) + return + val type = event.eventType + if (type == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED || type == AccessibilityEvent.TYPE_VIEW_FOCUSED) { + val root = rootInActiveWindow + if (root != null) { + mFastRootInActiveWindow = root + } + } + + for ((_, delegate) in mDelegates) { + val types = delegate.eventTypes + if (types != null && !delegate.eventTypes!!.contains(event.eventType)) + continue + //long start = System.currentTimeMillis(); + if (delegate.onAccessibilityEvent(this@AccessibilityService, event)) + break + //Log.v(TAG, "millis: " + (System.currentTimeMillis() - start) + " delegate: " + entry.getValue().getClass().getName()); + } + } + + + override fun onInterrupt() { + + } + + override fun onKeyEvent(event: KeyEvent): Boolean { + eventExecutor.execute { + stickOnKeyObserver.onKeyEvent(event.keyCode, event) + onKeyObserver.onKeyEvent(event.keyCode, event) + } + return keyInterrupterObserver.onInterceptKeyEvent(event) + } + + @Suppress("DEPRECATION") + @Deprecated("Deprecated in Java") + override fun onGesture(gestureId: Int): Boolean { + eventExecutor.execute { + gestureEventDispatcher.dispatchEvent { + onGesture(gestureId) + } + } + return false + } + + override fun getRootInActiveWindow(): AccessibilityNodeInfo? { + return try { + super.getRootInActiveWindow() + } catch (e: Exception) { + null + } + + } + + override fun onDestroy() { + Log.v(TAG, "onDestroy: $instance") + instance = null + mEventExecutor?.shutdownNow() + super.onDestroy() + } + + + override fun onServiceConnected() { + Log.v(TAG, "onServiceConnected: " + serviceInfo.toString()) + instance = this + super.onServiceConnected() + LOCK.lock() + ENABLED.signalAll() + LOCK.unlock() + // FIXME: 2017/2/12 有时在无障碍中开启服务后这里不会调用服务也不会运行,安卓的BUG??? + } + + + fun fastRootInActiveWindow(): AccessibilityNodeInfo? { + return mFastRootInActiveWindow + } + + companion object { + + private val TAG = "AccessibilityService" + + private val mDelegates = TreeMap() + private val LOCK = ReentrantLock() + private val ENABLED = LOCK.newCondition() + var instance: AccessibilityService? = null + private set + val stickOnKeyObserver = OnKeyListener.Observer() + private var containsAllEventTypes = false + private val eventTypes = HashSet() + + fun addDelegate(uniquePriority: Int, delegate: AccessibilityDelegate) { + mDelegates[uniquePriority] = delegate + val set = delegate.eventTypes + if (set == null) + containsAllEventTypes = true + else + eventTypes.addAll(set) + } + + fun disable(): Boolean { + if (instance != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + instance!!.disableSelf() + return true + } + return false + } + + fun waitForEnabled(timeOut: Long): Boolean { + if (instance != null) + return true + LOCK.lock() + try { + if (instance != null) + return true + if (timeOut == -1L) { + ENABLED.await() + return true + } + return ENABLED.await(timeOut, TimeUnit.MILLISECONDS) + } catch (e: InterruptedException) { + e.printStackTrace() + return false + } finally { + LOCK.unlock() + } + } + } + + +} diff --git a/app/src/main/java/com/stardust/view/accessibility/AccessibilityServiceUtils.kt b/app/src/main/java/com/stardust/view/accessibility/AccessibilityServiceUtils.kt new file mode 100644 index 00000000..aa3685b6 --- /dev/null +++ b/app/src/main/java/com/stardust/view/accessibility/AccessibilityServiceUtils.kt @@ -0,0 +1,44 @@ +package com.stardust.view.accessibility + +import android.accessibilityservice.AccessibilityService +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.provider.Settings +import android.text.TextUtils + +import java.util.Locale + + +/** + * Created by Stardust on 2017/1/26. + */ + +object AccessibilityServiceUtils { + + fun goToAccessibilitySetting(context: Context) { + context.startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) + } + + fun isAccessibilityServiceEnabled(context: Context, accessibilityService: Class): Boolean { + val expectedComponentName = ComponentName(context, accessibilityService) + + val enabledServicesSetting = Settings.Secure.getString(context.contentResolver, Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES) + ?: return false + + val colonSplitter = TextUtils.SimpleStringSplitter(':') + colonSplitter.setString(enabledServicesSetting) + + while (colonSplitter.hasNext()) { + val componentNameString = colonSplitter.next() + val enabledService = ComponentName.unflattenFromString(componentNameString) + + if (enabledService != null && enabledService == expectedComponentName) + return true + } + + return false + } + + +} diff --git a/app/src/main/java/com/stardust/view/accessibility/KeyInterceptor.kt b/app/src/main/java/com/stardust/view/accessibility/KeyInterceptor.kt new file mode 100644 index 00000000..235d370c --- /dev/null +++ b/app/src/main/java/com/stardust/view/accessibility/KeyInterceptor.kt @@ -0,0 +1,41 @@ +package com.stardust.view.accessibility + +import android.view.KeyEvent + +import java.util.concurrent.CopyOnWriteArrayList + +/** + * Created by Stardust on 2018/2/27. + */ + +interface KeyInterceptor { + + fun onInterceptKeyEvent(event: KeyEvent): Boolean + + class Observer : KeyInterceptor { + private val mKeyInterceptors = CopyOnWriteArrayList() + + fun addKeyInterrupter(interrupter: KeyInterceptor) { + mKeyInterceptors.add(interrupter) + } + + fun removeKeyInterrupter(interrupter: KeyInterceptor): Boolean { + return mKeyInterceptors.remove(interrupter) + } + + + override fun onInterceptKeyEvent(event: KeyEvent): Boolean { + for (interrupter in mKeyInterceptors) { + try { + if (interrupter.onInterceptKeyEvent(event)) { + return true + } + } catch (e: Exception) { + e.printStackTrace() + } + + } + return false + } + } +} diff --git a/app/src/main/java/com/stardust/view/accessibility/LayoutInspectService.kt b/app/src/main/java/com/stardust/view/accessibility/LayoutInspectService.kt new file mode 100644 index 00000000..c44358f6 --- /dev/null +++ b/app/src/main/java/com/stardust/view/accessibility/LayoutInspectService.kt @@ -0,0 +1,25 @@ +package com.stardust.view.accessibility + +import android.accessibilityservice.AccessibilityServiceInfo +import android.view.accessibility.AccessibilityNodeInfo + +/** + * Created by Stardust on 2017/7/13. + */ + +class LayoutInspectService : AccessibilityService() { + + + override fun onServiceConnected() { + val info = serviceInfo + info.flags = info.flags or AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS + serviceInfo = info + super.onServiceConnected() + } + + companion object { + + val instance: LayoutInspectService? = null + } +} + diff --git a/app/src/main/java/com/stardust/view/accessibility/LayoutInspector.kt b/app/src/main/java/com/stardust/view/accessibility/LayoutInspector.kt new file mode 100644 index 00000000..66a08dc8 --- /dev/null +++ b/app/src/main/java/com/stardust/view/accessibility/LayoutInspector.kt @@ -0,0 +1,87 @@ +package com.stardust.view.accessibility + +import android.content.Context +import android.os.Build +import androidx.annotation.RequiresApi +import android.util.Log +import android.view.accessibility.AccessibilityNodeInfo + +import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.Executor +import java.util.concurrent.Executors + +/** + * Created by Stardust on 2017/3/10. + */ + +class LayoutInspector(private val mContext: Context) { + @Volatile + var capture: NodeInfo? = null + private set + @Volatile + var isDumping = false + private set + private val mExecutor = Executors.newSingleThreadExecutor() + private val mCaptureAvailableListeners = CopyOnWriteArrayList() + + interface CaptureAvailableListener { + fun onCaptureAvailable(capture: NodeInfo?) + } + + fun captureCurrentWindow(): Boolean { + val service = AccessibilityService.instance + if (service == null) { + Log.d(LOG_TAG, "captureCurrentWindow: service = null") + capture = null + return false + } + val root = getRootInActiveWindow(service) + if (root == null) { + Log.d(LOG_TAG, "captureCurrentWindow: root = null") + capture = null + return false + } + mExecutor.execute { + isDumping = true + capture = NodeInfo.capture(mContext, root) + isDumping = false + for (l in mCaptureAvailableListeners) { + l.onCaptureAvailable(capture) + } + } + return true + } + + fun addCaptureAvailableListener(l: CaptureAvailableListener) { + mCaptureAvailableListeners.add(l) + } + + fun removeCaptureAvailableListener(l: CaptureAvailableListener): Boolean { + return mCaptureAvailableListeners.remove(l) + } + + private fun getRootInActiveWindow(service: AccessibilityService): AccessibilityNodeInfo? { + return service.rootInActiveWindow ?: return service.fastRootInActiveWindow() + + } + + @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) + private fun refreshChildList(root: AccessibilityNodeInfo?) { + if (root == null) + return + root.refresh() + val childCount = root.childCount + for (i in 0 until childCount) { + refreshChildList(root.getChild(i)) + } + } + + fun clearCapture() { + capture = null + } + + companion object { + + private val LOG_TAG = LayoutInspector::class.java.simpleName + } +} diff --git a/app/src/main/java/com/stardust/view/accessibility/NodeInfo.kt b/app/src/main/java/com/stardust/view/accessibility/NodeInfo.kt new file mode 100644 index 00000000..88bcc9e2 --- /dev/null +++ b/app/src/main/java/com/stardust/view/accessibility/NodeInfo.kt @@ -0,0 +1,188 @@ +package com.stardust.view.accessibility + +import android.annotation.SuppressLint +import android.content.Context +import android.content.pm.PackageManager +import android.content.res.Resources +import android.graphics.Rect +import androidx.annotation.Keep +import android.view.accessibility.AccessibilityNodeInfo + +import com.stardust.automator.UiObject + +import java.util.ArrayList +import java.util.HashMap + +/** + * Created by Stardust on 2017/3/10. + */ +@SuppressLint("DiscouragedApi") +@Keep +class NodeInfo(resources: Resources?, node: UiObject, var parent: NodeInfo?) { + + private val children = ArrayList() + val boundsInScreen = Rect() + val boundsInParent = Rect() + + var id: String? = null + var idHex: String? = null + var fullId: String? = null + var desc: String? = null + var className: String? = null + var packageName: String? = null + var text: String + var depth: Int = 0 + var drawingOrder: Int = 0 + var accessibilityFocused: Boolean = false + var checked: Boolean = false + var clickable: Boolean = false + var contextClickable: Boolean = false + var dismissable: Boolean = false + var editable: Boolean = false + var enabled: Boolean = false + var focusable: Boolean = false + var longClickable: Boolean = false + var row: Int = 0 + var column: Int = 0 + var rowCount: Int = 0 + var columnCount: Int = 0 + var rowSpan: Int = 0 + var columnSpan: Int = 0 + var selected: Boolean = false + var scrollable: Boolean = false + var bounds: String + var checkable: Boolean = false + var focused: Boolean = false + var visibleToUser: Boolean = false + var indexInParent: Int = 0 + + + init { + fullId = node.viewIdResourceName + id = simplifyId(fullId) + desc = node.desc() + className = node.className() + packageName = node.packageName() + text = node.text() + + depth = node.depth() + drawingOrder = node.drawingOrder + + row = node.row() + column = node.column() + rowCount = node.rowCount() + columnCount = node.columnCount() + rowSpan = node.rowSpan() + columnSpan = node.columnSpan() + + accessibilityFocused = node.isAccessibilityFocused + checked = node.isChecked + checkable = node.isCheckable + clickable = node.isClickable + contextClickable = node.isContextClickable + dismissable = node.isDismissable + enabled = node.isEnabled + editable = node.isEditable + focusable = node.isFocusable + focused = node.focused() + longClickable = node.isLongClickable + selected = node.isSelected + scrollable = node.isScrollable + visibleToUser = node.visibleToUser() + node.getBoundsInScreen(boundsInScreen) + @Suppress("DEPRECATION") + node.getBoundsInParent(boundsInParent) + bounds = boundsToString(boundsInScreen) + indexInParent = node.indexInParent() + if (resources != null && packageName != null && fullId != null) { + idHex = "0x" + Integer.toHexString(resources.getIdentifier(fullId, null, null)) + } + } + + private fun simplifyId(idResourceName: String?): String? { + if (idResourceName == null) + return null + val i = idResourceName.indexOf('/') + return idResourceName.substring(i + 1) + } + + fun getChildren(): List { + return children + } + + override fun toString(): String { + return className + "{" + + "childCount=" + children.size + + ", mBoundsInScreen=" + boundsInScreen + + ", mBoundsInParent=" + boundsInParent + + ", id='" + id + '\''.toString() + + ", desc='" + desc + '\''.toString() + + ", packageName='" + packageName + '\''.toString() + + ", text='" + text + '\''.toString() + + ", depth=" + depth + + ", drawingOrder=" + drawingOrder + + ", accessibilityFocused=" + accessibilityFocused + + ", checked=" + checked + + ", clickable=" + clickable + + ", contextClickable=" + contextClickable + + ", dismissable=" + dismissable + + ", editable=" + editable + + ", enabled=" + enabled + + ", focusable=" + focusable + + ", longClickable=" + longClickable + + ", row=" + row + + ", column=" + column + + ", rowCount=" + rowCount + + ", columnCount=" + columnCount + + ", rowSpan=" + rowSpan + + ", columnSpan=" + columnSpan + + ", selected=" + selected + + ", scrollable=" + scrollable + + ", bounds='" + bounds + '\''.toString() + + ", checkable=" + checkable + + ", focused=" + focused + + ", visibleToUser=" + visibleToUser + + ", parent=" + parent?.className + + '}'.toString() + } + + companion object { + + fun boundsToString(rect: Rect): String { + return rect.toString().replace('-', ',').replace(" ", "").substring(4) + } + + + internal fun capture(resourcesCache: HashMap, context: Context, uiObject: UiObject, parent: NodeInfo?): NodeInfo { + val pkg = uiObject.packageName() + var resources: Resources? = null + if (pkg != null) { + resources = resourcesCache[pkg] + if (resources == null) { + try { + resources = context.packageManager.getResourcesForApplication(pkg) + resourcesCache[pkg] = resources + } catch (e: PackageManager.NameNotFoundException) { + e.printStackTrace() + } + + } + } + val nodeInfo = NodeInfo(resources, uiObject, parent) + val childCount = uiObject.childCount + for (i in 0 until childCount) { + val child = uiObject.child(i) + if (child != null) { + nodeInfo.children.add(capture(resourcesCache, context, child, nodeInfo)) + } + } + return nodeInfo + } + + fun capture(context: Context, root: AccessibilityNodeInfo): NodeInfo { + val r = UiObject.createRoot(root) + val resourcesCache = HashMap() + return capture(resourcesCache, context, r, null) + } + } +} diff --git a/app/src/main/java/com/stardust/view/accessibility/NotificationListener.kt b/app/src/main/java/com/stardust/view/accessibility/NotificationListener.kt new file mode 100644 index 00000000..ee490b29 --- /dev/null +++ b/app/src/main/java/com/stardust/view/accessibility/NotificationListener.kt @@ -0,0 +1,25 @@ +package com.stardust.view.accessibility + +import android.accessibilityservice.AccessibilityService +import android.content.Context +import android.service.notification.StatusBarNotification +import android.util.Log +import android.view.accessibility.AccessibilityEvent + +import com.stardust.notification.Notification + +import java.util.ArrayList +import java.util.Collections +import java.util.concurrent.CopyOnWriteArrayList + +/** + * Created by Stardust on 2017/8/1. + */ + +interface NotificationListener { + + + fun onNotification(notification: Notification) + + +} diff --git a/app/src/main/java/com/stardust/view/accessibility/OnKeyListener.kt b/app/src/main/java/com/stardust/view/accessibility/OnKeyListener.kt new file mode 100644 index 00000000..575a34ca --- /dev/null +++ b/app/src/main/java/com/stardust/view/accessibility/OnKeyListener.kt @@ -0,0 +1,45 @@ +package com.stardust.view.accessibility + +import android.util.Log +import android.view.KeyEvent +import android.view.View + +import java.util.concurrent.CopyOnWriteArrayList + +/** + * Created by Stardust on 2017/7/18. + */ + +interface OnKeyListener { + + fun onKeyEvent(keyCode: Int, event: KeyEvent) + + class Observer : OnKeyListener { + + private val mOnKeyListeners = CopyOnWriteArrayList() + + override fun onKeyEvent(keyCode: Int, event: KeyEvent) { + for (listener in mOnKeyListeners) { + try { + listener.onKeyEvent(keyCode, event) + } catch (e: Exception) { + Log.e(TAG, "Error OnKeyEvent: $event Listener: $listener", e) + } + + } + } + + fun addListener(listener: OnKeyListener) { + mOnKeyListeners.add(listener) + } + + fun removeListener(listener: OnKeyListener): Boolean { + return mOnKeyListeners.remove(listener) + } + + companion object { + + private val TAG = "OnKeyListenerObserver" + } + } +} diff --git a/app/src/main/java/org/autojs/autojs/rhino/AndroidClassLoader.kt b/app/src/main/java/org/autojs/autojs/rhino/AndroidClassLoader.kt index 53ad0dce..96c2768a 100644 --- a/app/src/main/java/org/autojs/autojs/rhino/AndroidClassLoader.kt +++ b/app/src/main/java/org/autojs/autojs/rhino/AndroidClassLoader.kt @@ -29,7 +29,7 @@ import com.legacy.android.dx.command.dexer.Main as LegacyMain * @param parent the parent * @param cacheDir the cache directory */ -class AndroidClassLoader(private val parent: ClassLoader, private val cacheDir: File) : ClassLoader(), GeneratedClassLoader { +open class AndroidClassLoader(private val parent: ClassLoader, private val cacheDir: File) : ClassLoader(), GeneratedClassLoader { init { if (cacheDir.exists()) { diff --git a/app/src/main/java/org/autojs/autojs/runtime/api/AppUtils.kt b/app/src/main/java/org/autojs/autojs/runtime/api/AppUtils.kt index d0043a8d..e67a73db 100644 --- a/app/src/main/java/org/autojs/autojs/runtime/api/AppUtils.kt +++ b/app/src/main/java/org/autojs/autojs/runtime/api/AppUtils.kt @@ -70,7 +70,7 @@ class AppUtils(context: Context, @get:ScriptInterface val fileProviderAuthority: } @ScriptInterface - fun launchApp(appName: String) = getPackageName(appName)?.let { launchPackage(it) } ?: false + fun launchApp(appName: String) = getPackageName(appName)?.let { launchPackage(it) } == true @ScriptInterface fun getPackageName(appName: String): String? = mPackageManager.let { @@ -85,7 +85,7 @@ class AppUtils(context: Context, @get:ScriptInterface val fileProviderAuthority: @ScriptInterface fun getAppName(packageName: String?) = try { packageName?.let { "${mPackageManager.getApplicationLabel(getApplicationInfoCompat(mPackageManager, it, 0))}" } - } catch (ignore: NameNotFoundException) { + } catch (_: NameNotFoundException) { null } @@ -113,7 +113,7 @@ class AppUtils(context: Context, @get:ScriptInterface val fileProviderAuthority: @ScriptInterface fun isInstalled(packageName: String?) = try { getPackageInfoCompat(mPackageManager, packageName!!, 0) != null - } catch (e: Exception) { + } catch (_: Exception) { false } diff --git a/app/src/main/java/org/autojs/autojs/runtime/api/Plugins.kt b/app/src/main/java/org/autojs/autojs/runtime/api/Plugins.kt index a1d90c9a..64a96edd 100644 --- a/app/src/main/java/org/autojs/autojs/runtime/api/Plugins.kt +++ b/app/src/main/java/org/autojs/autojs/runtime/api/Plugins.kt @@ -80,7 +80,7 @@ class Plugins(private val context: Context, private val runtime: PluginRuntime) val applicationInfo = context.packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA) val ctx = context.createPackageContext(packageName, Context.CONTEXT_INCLUDE_CODE or Context.CONTEXT_IGNORE_SECURITY) Plugin.Package(ctx, applicationInfo, true) - } catch (e: PackageManager.NameNotFoundException) { + } catch (_: PackageManager.NameNotFoundException) { null } diff --git a/app/src/main/java/org/autojs/autojs/runtime/api/augment/automator/Auto.kt b/app/src/main/java/org/autojs/autojs/runtime/api/augment/automator/Auto.kt index 0ee6b7ab..7bfa03fe 100644 --- a/app/src/main/java/org/autojs/autojs/runtime/api/augment/automator/Auto.kt +++ b/app/src/main/java/org/autojs/autojs/runtime/api/augment/automator/Auto.kt @@ -12,6 +12,8 @@ import org.autojs.autojs.core.automator.UiObject import org.autojs.autojs.extension.AnyExtensions.isJsNullish import org.autojs.autojs.extension.ArrayExtensions.toNativeArray import org.autojs.autojs.extension.FlexibleArray +import org.autojs.autojs.extension.FlexibleArray.Companion.component1 +import org.autojs.autojs.extension.FlexibleArray.Companion.component2 import org.autojs.autojs.runtime.ScriptRuntime import org.autojs.autojs.runtime.api.augment.Augmentable import org.autojs.autojs.runtime.api.augment.Invokable diff --git a/app/src/main/java/org/autojs/autojs/runtime/api/augment/s13n/S13n.kt b/app/src/main/java/org/autojs/autojs/runtime/api/augment/s13n/S13n.kt index 4a603d3c..d7b5cbc2 100644 --- a/app/src/main/java/org/autojs/autojs/runtime/api/augment/s13n/S13n.kt +++ b/app/src/main/java/org/autojs/autojs/runtime/api/augment/s13n/S13n.kt @@ -186,7 +186,6 @@ object S13n : Augmentable() { TimeUnit.MINUTES -> FUNC_NAME_TO_MINUTES TimeUnit.HOURS -> FUNC_NAME_TO_HOURS TimeUnit.DAYS -> FUNC_NAME_TO_DAYS - else -> null } is String -> timeUnitRex.entries.find { it.value.matches(unit) }?.let { when (it.key) { diff --git a/app/src/main/java/org/autojs/autojs/runtime/api/augment/util/Inspect.kt b/app/src/main/java/org/autojs/autojs/runtime/api/augment/util/Inspect.kt index 505d6f3f..c776410f 100644 --- a/app/src/main/java/org/autojs/autojs/runtime/api/augment/util/Inspect.kt +++ b/app/src/main/java/org/autojs/autojs/runtime/api/augment/util/Inspect.kt @@ -5,6 +5,8 @@ import org.autojs.autojs.core.automator.UiObjectCollection import org.autojs.autojs.extension.AnyExtensions.isJsNonNullObject import org.autojs.autojs.extension.AnyExtensions.isJsNullish import org.autojs.autojs.extension.ArrayExtensions.toNativeObject +import org.autojs.autojs.extension.FlexibleArray.Companion.component1 +import org.autojs.autojs.extension.FlexibleArray.Companion.component2 import org.autojs.autojs.extension.NumberExtensions.string import org.autojs.autojs.extension.ScriptableExtensions.prop import org.autojs.autojs.rhino.ProxyObject.Companion.PROXY_GETTER_KEY @@ -16,8 +18,8 @@ import org.autojs.autojs.runtime.api.augment.global.Species.isErrorRhino import org.autojs.autojs.runtime.api.augment.global.Species.isJavaObjectRhino import org.autojs.autojs.runtime.api.augment.global.Species.isObjectRhino import org.autojs.autojs.runtime.api.augment.global.Species.isStringRhino -import org.autojs.autojs.runtime.exception.WrappedIllegalArgumentException import org.autojs.autojs.runtime.exception.ShouldNeverHappenException +import org.autojs.autojs.runtime.exception.WrappedIllegalArgumentException import org.autojs.autojs.util.RhinoUtils import org.autojs.autojs.util.RhinoUtils.callFunction import org.autojs.autojs.util.RhinoUtils.callPrototypeFunction @@ -258,7 +260,7 @@ object Inspect : Augmentable(), Invokable { if (value is StringReadable) return value.toStringReadable() - if (value is Scriptable && value !is NativeJavaClass) runCatching { + if (value is Scriptable && value !is NativeJavaPackage) runCatching { (value.prop(StringReadable.KEY) as? BaseFunction)?.let { f -> return Context.toString(callFunction(f, null, value, arrayOf())) } diff --git a/app/src/main/java/org/autojs/autojs/runtime/exception/WrappedRuntimeException.kt b/app/src/main/java/org/autojs/autojs/runtime/exception/WrappedRuntimeException.kt index c757fcd3..959f2af8 100644 --- a/app/src/main/java/org/autojs/autojs/runtime/exception/WrappedRuntimeException.kt +++ b/app/src/main/java/org/autojs/autojs/runtime/exception/WrappedRuntimeException.kt @@ -26,6 +26,7 @@ class WrappedRuntimeException(detailMessage: String, private val causeException: @Deprecated("Use {@link #getWrappedException()} instead.", ReplaceWith("getWrappedException()")) fun unwrap(): Throwable? = getWrappedException() + @Suppress("DEPRECATION") override fun toString() = super.toString().replace(Regex("^${WrappedRuntimeException::class.java.name}: "), "") } diff --git a/app/src/main/java/org/autojs/autojs/ui/enhancedfloaty/ResizableExpandableFloatyWindow.kt b/app/src/main/java/org/autojs/autojs/ui/enhancedfloaty/ResizableExpandableFloatyWindow.kt index 515ad304..ffc4fc7e 100644 --- a/app/src/main/java/org/autojs/autojs/ui/enhancedfloaty/ResizableExpandableFloatyWindow.kt +++ b/app/src/main/java/org/autojs/autojs/ui/enhancedfloaty/ResizableExpandableFloatyWindow.kt @@ -24,7 +24,7 @@ import java.util.concurrent.CopyOnWriteArrayList * Modified by SuperMonster003 as of Apr 29, 2023. * Transformed by SuperMonster003 on Dec 2, 2023. */ -class ResizableExpandableFloatyWindow(private var floaty: ResizableExpandableFloaty) : FloatyWindow() { +open class ResizableExpandableFloatyWindow(private var floaty: ResizableExpandableFloaty) : FloatyWindow() { private val mViewStack = ViewStack { v -> mViewSwitcher.setSecondView(v) } diff --git a/version.properties b/version.properties index 2c0ff437..eab1ad90 100644 --- a/version.properties +++ b/version.properties @@ -1,5 +1,5 @@ -#Tue Jan 14 22:03:25 CST 2025 -BUILD_TIME=1736863405650 +#Wed Jan 15 00:03:38 CST 2025 +BUILD_TIME=1736870618100 COMPILE_SDK_VERSION=34 JAVA_VERSION=23 JAVA_VERSION_MIN_RADICAL=0 @@ -17,6 +17,6 @@ RAPID_OCR_OPENCV_MOBILE_LABEL_VERSION=13 RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3 TARGET_SDK_VERSION=34 TARGET_SDK_VERSION_INRT=29 -VERSION_BUILD=2972 +VERSION_BUILD=2973 VERSION_NAME=6.6.2 Alpha2 VSCODE_EXT_REQUIRED_VERSION=1.0.8