├── .github
└── workflows
│ ├── check.yml
│ └── publish.yml
├── .gitignore
├── .idea
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── misc.xml
├── modules.xml
└── vcs.xml
├── .metadata
├── .pubignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── android
├── .gitignore
├── build.gradle
├── gradle.properties
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
│ └── main
│ ├── AndroidManifest.xml
│ └── java
│ └── com
│ └── appspector
│ └── flutter
│ ├── AppSpectorPlugin.java
│ ├── MainAppSpectorHandler.java
│ ├── RequestSender.java
│ ├── event
│ ├── EventHandler.java
│ ├── EventReceiver.java
│ ├── http
│ │ ├── FlutterHttpTracker.java
│ │ ├── HttpRequestEventHandler.java
│ │ └── HttpResponseEventHandler.java
│ └── log
│ │ └── LogEventHandler.java
│ └── screenshot
│ └── FlutterScreenshotFactory.java
├── example
├── .gitignore
├── README.md
├── android
│ ├── app
│ │ ├── build.gradle
│ │ └── src
│ │ │ └── main
│ │ │ ├── AndroidManifest.xml
│ │ │ ├── java
│ │ │ └── com
│ │ │ │ └── appspector
│ │ │ │ └── flutter
│ │ │ │ └── example
│ │ │ │ └── MainActivity.java
│ │ │ └── res
│ │ │ ├── drawable
│ │ │ └── launch_background.xml
│ │ │ ├── mipmap-hdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-mdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ └── ic_launcher.png
│ │ │ └── values
│ │ │ └── styles.xml
│ ├── build.gradle
│ ├── gradle.properties
│ ├── gradle
│ │ └── wrapper
│ │ │ └── gradle-wrapper.properties
│ └── settings.gradle
├── assets
│ ├── patch.json
│ ├── post.json
│ └── put.json
├── ios
│ ├── AppSpectorPluginTestPlan.xctestplan
│ ├── AppSpectorPluginTests
│ │ ├── ASPluginCallValidatorTests.m
│ │ ├── ASPluginEventsHandlerTests.m
│ │ ├── AppSpectorPluginTests.m
│ │ └── Info.plist
│ ├── Flutter
│ │ ├── AppFrameworkInfo.plist
│ │ ├── Debug.xcconfig
│ │ └── Release.xcconfig
│ ├── Podfile
│ ├── Runner.xcodeproj
│ │ ├── project.pbxproj
│ │ ├── project.xcworkspace
│ │ │ └── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── xcschemes
│ │ │ └── Runner.xcscheme
│ ├── Runner.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ └── WorkspaceSettings.xcsettings
│ └── Runner
│ │ ├── AppDelegate.h
│ │ ├── AppDelegate.m
│ │ ├── Assets.xcassets
│ │ ├── AppIcon.appiconset
│ │ │ ├── Contents.json
│ │ │ ├── Icon-App-1024x1024@1x.png
│ │ │ ├── Icon-App-20x20@1x.png
│ │ │ ├── Icon-App-20x20@2x.png
│ │ │ ├── Icon-App-20x20@3x.png
│ │ │ ├── Icon-App-29x29@1x.png
│ │ │ ├── Icon-App-29x29@2x.png
│ │ │ ├── Icon-App-29x29@3x.png
│ │ │ ├── Icon-App-40x40@1x.png
│ │ │ ├── Icon-App-40x40@2x.png
│ │ │ ├── Icon-App-40x40@3x.png
│ │ │ ├── Icon-App-60x60@2x.png
│ │ │ ├── Icon-App-60x60@3x.png
│ │ │ ├── Icon-App-76x76@1x.png
│ │ │ ├── Icon-App-76x76@2x.png
│ │ │ └── Icon-App-83.5x83.5@2x.png
│ │ └── LaunchImage.imageset
│ │ │ ├── Contents.json
│ │ │ ├── LaunchImage.png
│ │ │ ├── LaunchImage@2x.png
│ │ │ ├── LaunchImage@3x.png
│ │ │ └── README.md
│ │ ├── Base.lproj
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
│ │ ├── Info.plist
│ │ └── main.m
├── lib
│ ├── app_drawer.dart
│ ├── color.dart
│ ├── http
│ │ ├── app_http_client.dart
│ │ └── http_request_item.dart
│ ├── http_page.dart
│ ├── main.dart
│ ├── main_page.dart
│ ├── metadata_page.dart
│ ├── routes.dart
│ ├── sqlite
│ │ ├── record.dart
│ │ └── storage.dart
│ ├── sqlite_page.dart
│ └── utils.dart
└── pubspec.yaml
├── github-cover.png
├── ios
├── .gitignore
├── Assets
│ └── .gitkeep
├── Classes
│ ├── ASPluginCallValidator.h
│ ├── ASPluginCallValidator.m
│ ├── ASPluginEventsHandler.h
│ ├── ASPluginEventsHandler.m
│ ├── AppSpectorPlugin.h
│ └── AppSpectorPlugin.m
└── appspector.podspec
├── lib
├── appspector.dart
└── src
│ ├── appspector_plugin.dart
│ ├── event_sender.dart
│ ├── http
│ ├── client.dart
│ ├── events.dart
│ ├── http_overrides.dart
│ ├── request_wrapper.dart
│ ├── response_wrapper.dart
│ └── tracker.dart
│ ├── log
│ └── logger.dart
│ ├── monitors.dart
│ └── request_receiver.dart
├── publish.sh
├── pubspec.yaml
├── static
└── appspector_demo.gif
└── test_ios.sh
/.github/workflows/check.yml:
--------------------------------------------------------------------------------
1 | name: Check workflow
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - master
7 | - develop
8 |
9 | env:
10 | CREDENTIALS_PATH: /Users/runner/hostedtoolcache/flutter/.pub-cache
11 |
12 | jobs:
13 | check:
14 | runs-on: macos-latest
15 | steps:
16 | - uses: actions/checkout@v3
17 | - uses: actions/setup-java@v3
18 | with:
19 | distribution: 'adopt'
20 | java-version: '17'
21 | - uses: subosito/flutter-action@v2
22 | with:
23 | flutter-version: '3.10.5'
24 |
25 | - run: flutter pub get
26 | - run: mkdir -p $CREDENTIALS_PATH && echo $CREDENTIALS_JSON > $CREDENTIALS_PATH/credentials.json
27 |
28 | - run: flutter pub publish -n
29 | - run: ./test_ios.sh
30 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Release workflow
2 |
3 | on:
4 | workflow_dispatch:
5 | release:
6 | types: [published]
7 |
8 | env:
9 | CREDENTIALS_JSON: ${{secrets.CREDENTIALS_JSON}}
10 | CREDENTIALS_PATH: /Users/runner/hostedtoolcache/flutter/.pub-cache
11 |
12 | jobs:
13 | release:
14 | runs-on: macos-latest
15 | steps:
16 | - uses: actions/checkout@v3
17 | - uses: actions/setup-java@v3
18 | with:
19 | distribution: 'adopt'
20 | java-version: '17'
21 | - uses: subosito/flutter-action@v2
22 | with:
23 | flutter-version: '3.10.5'
24 |
25 | - run: flutter pub get
26 | - run: mkdir -p $CREDENTIALS_PATH && echo $CREDENTIALS_JSON > $CREDENTIALS_PATH/credentials.json
27 |
28 | - run: flutter pub publish -f
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .dart_tool/
3 |
4 | .packages
5 | .flutter-plugins
6 | .pub/
7 | pubspec.lock
8 |
9 | build/
10 | doc/
11 |
12 | *.iml
13 |
14 | /.idea/workspace.xml
15 | /.idea/markdown-*.xml
16 | /.idea/checkstyle-idea.xml
17 | /.idea/encodings.xml
18 |
19 | /.idea/libraries
20 | /.idea/dictionaries
21 | /.idea/runConfigurations
22 | /.idea/sonarlint
23 | /.idea/markdown-navigator
24 | /.idea/shelf
25 | AppSpector Flutter/.idea
26 | example/.flutter-plugins-dependencies
27 | example/ios/Flutter/Flutter.podspec
28 | .packages_
29 | lib/.idea
30 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | xmlns:android
32 |
33 | ^$
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | xmlns:.*
43 |
44 | ^$
45 |
46 |
47 | BY_NAME
48 |
49 |
50 |
51 |
52 |
53 |
54 | .*:id
55 |
56 | http://schemas.android.com/apk/res/android
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | .*:name
66 |
67 | http://schemas.android.com/apk/res/android
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | name
77 |
78 | ^$
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | style
88 |
89 | ^$
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | .*
99 |
100 | ^$
101 |
102 |
103 | BY_NAME
104 |
105 |
106 |
107 |
108 |
109 |
110 | .*
111 |
112 | http://schemas.android.com/apk/res/android
113 |
114 |
115 | ANDROID_ATTRIBUTE_ORDER
116 |
117 |
118 |
119 |
120 |
121 |
122 | .*
123 |
124 | .*
125 |
126 |
127 | BY_NAME
128 |
129 |
130 |
131 |
132 |
133 |
134 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
36 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled and should not be manually edited.
5 |
6 | version:
7 | revision: 5391447fae6209bb21a89e6a5a6583cac1af9b4b
8 | channel: beta
9 |
10 | project_type: plugin
11 |
--------------------------------------------------------------------------------
/.pubignore:
--------------------------------------------------------------------------------
1 | publish.sh
2 | test_ios.sh
3 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 0.10.0 6 Jul 2023
2 | * Update Android SDK to v1.5
3 | * Use CompileSdkVersion = 33
4 |
5 | ## 0.9.0 13 Jun 2022
6 | * Adds support for Flutter 3
7 |
8 | ## 0.8.1 20 Sep 2021
9 | * Fix the issue with incorrect User-Agent
10 |
11 | ## 0.8.0 09 Sep 2021
12 | * Support Flutter 2.5
13 |
14 | ## 0.7.0 31 Mar 2021
15 | * Support Flutter 2 and Dart null-safety
16 | * Fix issue with missing OnSessionUrlListener
17 |
18 | ## 0.6.2 30 Mar 2021
19 | * Add internal updates
20 |
21 | ## 0.6.1 14 Mar 2021
22 | * Fix for incorrect HTTP response handling on iOS
23 |
24 | ## 0.6.0 28 Feb 2021
25 | * Fix losing connection after the hot restart
26 |
27 | ## 0.5.0 16 Dec 2020
28 | * Use 1.4.+ AppSpector Android SDK
29 | * Fix issue with missing Content-Length header
30 |
31 | ## 0.4.0 6 Oct 2020
32 | * Support new Flutter version (1.22.0)
33 |
34 | ## 0.3.0 27 May 2020
35 | * Add File System monitor
36 |
37 | ## 0.2.0 16 Apr 2020
38 | * Add API to provide list of monitors to enable
39 | * Add API to provide session metadata (including the device custom name)
40 | * Add API which allows to stop and start session during the application lifetime
41 | * Add ability to listen to a session url
42 | * Use Android SDK version 1.2.1
43 |
44 | ## 0.1.0 24 Dec 2019
45 | * Fixed logging module
46 |
47 | ## 0.0.9 20 Nov 2019
48 | * Fix compatibility with DIO library
49 |
50 | ## 0.0.8 11 Sep 2019
51 | * Add WidgetsFlutterBinding.ensureInitialized(); to README.md
52 | * Fix issue about 'toImage' isn't defined for the class 'ContainerLayer
53 | * Use MethodChannel only on MainThread
54 |
55 | ## 0.0.7 - 29 Jul 2019
56 |
57 | * Fix compatibility issues with the latest flutter version
58 |
59 | ## 0.0.6 - 15 Jul 2019
60 |
61 | * Fix compatibility issues with new dart version
62 | * Bug fixes
63 |
64 | ## 0.0.4 - 6 May 2019
65 |
66 | * Update README
67 |
68 |
69 | ## 0.0.3 - 25 Apr 2019
70 |
71 | * Add Http Monitor
72 | * Add SharedPreference/UserDefaults Monitor
73 | * Add Logger to collect logs only into AppSpector Service
74 |
75 |
76 | ## 0.0.2 - 22 Jan 2019
77 |
78 | * Fix issue with black screen in Screenshot Monitor
79 |
80 |
81 | ## 0.0.1 - 20 Jan 2019
82 |
83 | * Initial release with base initialization of AppSpector
84 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2019 AppSpector
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/appspector/flutter-plugin)
2 | # 
3 |
4 | A plugin that integrate [AppSpector](https://appspector.com/?utm_source=flutter_readme) to your Flutter project.
5 |
6 | With AppSpector you can remotely debug your app running in the same room or on another continent.
7 | You can measure app performance, view database content, logs, network requests and many more in realtime.
8 | This is the instrument that you've been looking for. Don't limit yourself only to simple logs.
9 | Debugging doesn't have to be painful!
10 |
11 |
12 |
13 | * [Installation](#installation)
14 | * [Add AppSpector plugin to pubspec.yaml](#add-appspector-plugin-to-pubspecyaml)
15 | * [Initialize AppSpector plugin](#initialize-appspector-plugin)
16 | * [Build and Run](#build-and-run)
17 | * [Getting session URL](#getting-session-url)
18 | * [Correct SQLite setup for the SDK](#correct-sqlite-setup-for-the-sdk)
19 | * [Configure](#configure)
20 | * [Start/Stop data collection](#startstop-data-collection)
21 | * [Custom device name](#custom-device-name)
22 | * [Getting session URL](#getting-session-url)
23 | * [Correct SQLite setup for the SDK](#correct-sqlite-setup-for-the-sdk)
24 | * [Features](#features)
25 | * [SQLite monitor](#sqlite-monitor)
26 | * [HTTP monitor](#http-monitor)
27 | * [Logs monitor](#logs-monitor)
28 | * [Logger](#logger)
29 | * [Location monitor](#location-monitor)
30 | * [Screenshot monitor](#screenshot-monitor)
31 | * [SharedPreference/UserDefaults monitor](#sharedpreferenceuserdefaults-monitor)
32 | * [Performance monitor](#performance-monitor)
33 | * [Environment monitor](#environment-monitor)
34 | * [Notification Center monitor (only for iOS)](#notification-center-monitor-only-for-ios)
35 | * [File System Monitor](#file-system-monitor)
36 | * [Feedback](#feedback)
37 |
38 |
39 | # Installation
40 |
41 | Before using AppSpector SDK in your Flutter app you have to register it on ([https://app.appspector.com](https://app.appspector.com?utm_source=android_readme)) via web or [desktop app](https://appspector.com/download/?utm_source=android_readme).
42 | To use SDK on both platforms (iOS and Android) you have to register two separate apps for different platforms.
43 | API keys required for the SDK initialisation will be available on the Apps settings pages
44 |
45 | ## Add AppSpector plugin to pubspec.yaml
46 | ```yaml
47 | dependencies
48 | appspector: '0.10.0'
49 | ```
50 |
51 | ## Initialize AppSpector plugin
52 | ```dart
53 | import 'package:appspector/appspector.dart';
54 |
55 | void main() {
56 | WidgetsFlutterBinding.ensureInitialized();
57 | runAppSpector();
58 | runApp(MyApp());
59 | }
60 |
61 | void runAppSpector() {
62 | final config = Config()
63 | ..iosApiKey = "Your iOS API_KEY"
64 | ..androidApiKey = "Your Android API_KEY";
65 |
66 | // If you don't want to start all monitors you can specify a list of necessary ones
67 | config.monitors = [Monitors.http, Monitors.logs, Monitors.screenshot];
68 |
69 | AppSpectorPlugin.run(config);
70 | }
71 | ```
72 |
73 | ## Build and Run
74 | Build your project and see everything work! When your app is up and running you can go to [https://app.appspector.com](https://app.appspector.com/?utm_source=flutter_readme) and connect to your application session.
75 |
76 |
77 | # Configure
78 |
79 | ## Start/Stop data collection
80 |
81 | After calling the `run` method the SDKs start data collection and
82 | data transferring to the web service. From that point you can see
83 | your session in the AppSpector client.
84 |
85 | Since plugin initialization should locate in the main function we provide
86 | methods to help you control AppSpector state by calling `stop()` and `start()` methods.
87 |
88 | **You are able to use these methods only after AppSpector was initialized.**
89 |
90 | The `stop()` tells AppSpector to disable all data collection and close current session.
91 |
92 | ```dart
93 | await AppSpectorPlugin.shared().stop();
94 | ```
95 |
96 | The `start()` starts it again using config you provided at initialization.
97 |
98 | ```dart
99 | await AppSpectorPlugin.shared().start();
100 | ```
101 |
102 | **As the result new session will be created and all activity between
103 | `stop()` and `start()` calls will not be tracked.**
104 |
105 | To check AppSpector state you can use `isStarted()` method.
106 |
107 | ```dart
108 | await AppSpectorPlugin.shared().isStarted();
109 | ```
110 |
111 | ## Custom device name
112 |
113 | You can assign a custom name to your device to easily find needed sessions
114 | in the sessions list. To do this you should add the desired name as a value
115 | for `MetadataKeys.deviceName` key to the `metadata` dictionary:
116 |
117 | ```dart
118 | void runAppSpector() {
119 | var config = new Config()
120 | ..iosApiKey = "Your iOS API_KEY"
121 | ..androidApiKey = "Your Android API_KEY"
122 | ..metadata = {MetadataKeys.deviceName: "CustomName"};
123 |
124 | AppSpectorPlugin.run(config);
125 | }
126 | ```
127 |
128 | Also, the plugin allows managing the device name during application lifetime using
129 |
130 | the `setMetadataValue` method to change device name
131 |
132 | ```dart
133 | AppSpectorPlugin.shared().setMetadataValue(MetadataKeys.deviceName, "New Device Name");
134 | ```
135 |
136 | or the `removeMetadataValue` to remove your custom device name
137 |
138 | ```dart
139 | AppSpectorPlugin.shared().removeMetadataValue(MetadataKeys.deviceName);
140 | ```
141 |
142 | ## Getting session URL
143 |
144 | Sometimes you may need to get URL pointing to current session from code.
145 | Say you want link crash in your crash reporter with it, write it to logs or
146 | display in your debug UI. To get this URL you have to add a session start callback:
147 |
148 | ```dart
149 | AppSpectorPlugin.shared()?.sessionUrlListener = (sessionUrl) => {
150 | // Save url for future use...
151 | };
152 | ```
153 |
154 |
155 | ## Correct SQLite setup for the SDK
156 |
157 | The SQLite monitor on Android demands that any DB files are located at the `database` folder.
158 | So, if you're using [sqflite](https://pub.dev/packages/sqflite) the code for opening db will be looks like that:
159 |
160 | ```dart
161 | var dbPath = await getDatabasesPath() + "/my_database_name";
162 | var db = await openDatabase(dbPath, version: 1, onCreate: _onCreate);
163 | ```
164 |
165 | The `getDatabasesPath()` method is imported from `package:sqflite/sqflite.dart`.
166 |
167 | # Features
168 |
169 | AppSpector provides many monitors that are can be different for both platforms.
170 |
171 | ### SQLite monitor
172 | Provides browser for sqlite databases found in your app. Allows to track all queries, shows DB scheme and data in DB. You can issue custom SQL query on any DB and see results in browser immediately.
173 |
174 |
175 |
176 | #### HTTP monitor
177 | Shows all HTTP traffic in your app. You can examine any request, see request/response headers and body.
178 | We provide XML and JSON highliting for request/responses with formatting and folding options so even huge responses are easy to look through.
179 |
180 |
181 |
182 | ### Logs monitor
183 | Displays all logs generated by your app.
184 |
185 | #### Logger
186 | AppSpector Logger allows you to collect log message only into AppSpector
187 | service. It is useful when you log some internal data witch can be leaked
188 | through Android Logcat or similar tool for iOS.
189 |
190 | It has a very simple API to use:
191 |
192 | ```dart
193 | Logger.d("MyTAG", "It won't be printed to the app console");
194 | ```
195 |
196 | **Don't forget to import it** from `package:appspector/appspector.dart`.
197 |
198 |
199 |
200 | ### Location monitor
201 | Most of the apps are location-aware. Testing it requires changing locations yourself. In this case, location mocking is a real time saver. Just point to the location on the map and your app will change its geodata right away.
202 |
203 |
204 |
205 | ### Screenshot monitor
206 | Simply captures screenshot from the device.
207 |
208 | ### SharedPreference/UserDefaults monitor
209 | Provides browser and editor for SharedPreferences/UserDefaults.
210 |
211 | ### Performance monitor
212 | Displays real-time graphs of the CPU / Memory/ Network / Disk / Battery usage.
213 |
214 | ### Environment monitor
215 | Gathers all of the environment variables and arguments in one place, info.plist, cli arguments and much more.
216 |
217 | ### Notification Center monitor (only for iOS)
218 | Tracks all posted notifications and subscriptions. You can examine notification user info, sender/reciever objects, etc.
219 | And naturally you can post notifications to your app from the frontend.
220 |
221 | ### File System Monitor
222 | Provides access to the application internal storage on Android and sandbox and bundle on iOS.
223 | Using this monitor you're able to download, remove or upload files, create directories and just walk around your app FS.
224 |
225 | For mode details, you can visit [Android SDK](https://github.com/appspector/android-sdk/) and [iOS SDK](https://github.com/appspector/ios-sdk) pages.
226 |
227 |
228 | # Feedback
229 | Let us know what do you think or what would you like to be improved: [info@appspector.com](mailto:info@appspector.com).
230 |
231 | [Join our slack to discuss setup process and features](https://slack.appspector.com)
232 |
--------------------------------------------------------------------------------
/android/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/workspace.xml
5 | /.idea/libraries
6 | .DS_Store
7 | /build
8 | /captures
9 |
10 | **/GeneratedPluginRegistrant.java
11 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | group 'com.appspector.flutter'
2 | version '1.0'
3 |
4 | buildscript {
5 | repositories {
6 | google()
7 | mavenCentral()
8 | }
9 |
10 | dependencies {
11 | classpath 'com.android.tools.build:gradle:8.0.2'
12 | }
13 | }
14 |
15 | rootProject.allprojects {
16 | repositories {
17 | google()
18 | maven { url "https://maven.appspector.com/artifactory/android-sdk" }
19 | }
20 | }
21 |
22 | apply plugin: 'com.android.library'
23 |
24 | android {
25 | compileSdkVersion 33
26 | if (project.android.hasProperty('namespace')) {
27 | namespace "com.appspector.flutter"
28 | }
29 | defaultConfig {
30 | minSdkVersion 21
31 | }
32 |
33 | lintOptions {
34 | disable 'InvalidPackage'
35 | }
36 |
37 | compileOptions {
38 | sourceCompatibility 1.8
39 | targetCompatibility 1.8
40 | }
41 | }
42 |
43 | dependencies {
44 | implementation("com.appspector:android-sdk:1.5.+") {
45 | exclude group:"org.jetbrains.kotlin"
46 | }
47 | implementation 'androidx.annotation:annotation:1.6.0'
48 | }
49 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx1536M
2 | android.useAndroidX=true
3 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/android/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Jun 23 08:50:38 CEST 2017
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-all.zip
7 |
--------------------------------------------------------------------------------
/android/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/android/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'appspector_plugin'
2 |
--------------------------------------------------------------------------------
/android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
--------------------------------------------------------------------------------
/android/src/main/java/com/appspector/flutter/AppSpectorPlugin.java:
--------------------------------------------------------------------------------
1 | package com.appspector.flutter;
2 |
3 | import androidx.annotation.NonNull;
4 | import androidx.annotation.Nullable;
5 | import io.flutter.embedding.engine.plugins.FlutterPlugin;
6 | import io.flutter.plugin.common.PluginRegistry.Registrar;
7 |
8 | import static com.appspector.flutter.MainAppSpectorHandler.internalRegister;
9 |
10 | /**
11 | * AppSpectorPlugin
12 | */
13 | public class AppSpectorPlugin implements FlutterPlugin {
14 |
15 | @Nullable
16 | private MainAppSpectorHandler mainAppSpectorHandler;
17 |
18 | @Override
19 | public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
20 | mainAppSpectorHandler = internalRegister(binding.getApplicationContext(), binding.getBinaryMessenger());
21 | }
22 |
23 | @Override
24 | public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
25 | if (mainAppSpectorHandler != null) {
26 | mainAppSpectorHandler.unregister();
27 | mainAppSpectorHandler = null;
28 | }
29 | }
30 |
31 | /**
32 | * Plugin registration.
33 | * Deprecated: it's old plugin registration which is needed for Flutter v1
34 | */
35 | @SuppressWarnings("deprecation")
36 | @Deprecated
37 | public static void registerWith(Registrar registrar) {
38 | internalRegister(registrar.context().getApplicationContext(), registrar.messenger());
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/android/src/main/java/com/appspector/flutter/MainAppSpectorHandler.java:
--------------------------------------------------------------------------------
1 | package com.appspector.flutter;
2 |
3 | import android.app.Application;
4 | import android.content.Context;
5 | import android.os.Handler;
6 |
7 | import com.appspector.flutter.event.EventReceiver;
8 | import com.appspector.flutter.event.http.HttpRequestEventHandler;
9 | import com.appspector.flutter.event.http.HttpResponseEventHandler;
10 | import com.appspector.flutter.event.log.LogEventHandler;
11 | import com.appspector.flutter.screenshot.FlutterScreenshotFactory;
12 | import com.appspector.sdk.AppSpector;
13 | import com.appspector.sdk.Builder;
14 | import com.appspector.sdk.SessionUrlListener;
15 | import com.appspector.sdk.core.util.AppspectorLogger;
16 | import com.appspector.sdk.monitors.screenshot.ScreenshotMonitor;
17 |
18 | import java.util.Collections;
19 | import java.util.HashMap;
20 | import java.util.List;
21 | import java.util.Map;
22 |
23 | import androidx.annotation.NonNull;
24 | import androidx.annotation.Nullable;
25 | import io.flutter.plugin.common.BinaryMessenger;
26 | import io.flutter.plugin.common.MethodCall;
27 | import io.flutter.plugin.common.MethodChannel;
28 |
29 | class MainAppSpectorHandler implements MethodChannel.MethodCallHandler {
30 |
31 | private final Application application;
32 | @SuppressWarnings({"FieldCanBeLocal", "unused"})
33 | private final MethodChannel mainChannel;
34 | private final EventReceiver eventReceiver;
35 | private final RequestSender requestSender;
36 | private final SessionUrlListener sessionUrlListener;
37 | private final Map monitorInitializerMap;
38 |
39 | private MainAppSpectorHandler(Application application,
40 | MethodChannel mainChannel,
41 | SessionUrlListener sessionUrlListener,
42 | EventReceiver eventReceiver,
43 | RequestSender requestSender) {
44 | this.application = application;
45 | this.mainChannel = mainChannel;
46 | this.eventReceiver = eventReceiver;
47 | this.requestSender = requestSender;
48 | this.sessionUrlListener = sessionUrlListener;
49 | this.monitorInitializerMap = createMonitorInitializerMap();
50 | registerEvents(eventReceiver);
51 | }
52 |
53 | private void registerEvents(EventReceiver eventReceiver) {
54 | eventReceiver.registerEventHandler(new HttpRequestEventHandler());
55 | eventReceiver.registerEventHandler(new HttpResponseEventHandler());
56 | eventReceiver.registerEventHandler(new LogEventHandler());
57 | }
58 |
59 | void register() {
60 | mainChannel.setMethodCallHandler(this);
61 | }
62 |
63 | void unregister() {
64 | mainChannel.setMethodCallHandler(null);
65 | eventReceiver.unsubscribe();
66 | }
67 |
68 | @Override
69 | public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
70 | switch (call.method) {
71 | case "run":
72 | initAppSpector(result,
73 | call.argument("apiKey"),
74 | call.argument("metadata"),
75 | call.argument("enabledMonitors")
76 | );
77 | break;
78 | case "stop":
79 | stopSdk(result);
80 | break;
81 | case "start":
82 | startSdk(result);
83 | break;
84 | case "isStarted":
85 | checkSdkStarted(result);
86 | break;
87 | case "setMetadata":
88 | setMetadata(call.argument("key"), call.argument("value"), result);
89 | break;
90 | case "removeMetadata":
91 | removeMetadata(call.argument("key"), result);
92 | break;
93 | default:
94 | result.notImplemented();
95 | }
96 | }
97 |
98 | private void setMetadata(@Nullable String key, @Nullable String value, @NonNull MethodChannel.Result result) {
99 | if (key == null || value == null) {
100 | AppspectorLogger.e("AppSpectorPlugin :: key or value is null");
101 | return;
102 | }
103 | withSharedInstance(result, sharedInstance -> {
104 | sharedInstance.setMetadataValue(key, value);
105 | return null;
106 | });
107 | }
108 |
109 | private void removeMetadata(@Nullable String key, @NonNull MethodChannel.Result result) {
110 | if (key == null) {
111 | AppspectorLogger.e("AppSpectorPlugin :: key is null");
112 | return;
113 | }
114 | withSharedInstance(result, sharedInstance -> {
115 | sharedInstance.removeMetadataValue(key);
116 | return null;
117 | });
118 | }
119 |
120 | private void checkSdkStarted(@NonNull MethodChannel.Result result) {
121 | final AppSpector sharedInstance = AppSpector.shared();
122 | if (sharedInstance != null) {
123 | result.success(sharedInstance.isStarted());
124 | return;
125 | }
126 | result.success(false);
127 | }
128 |
129 | private void stopSdk(@NonNull MethodChannel.Result result) {
130 | withSharedInstance(result, sharedInstance -> {
131 | sharedInstance.stop();
132 | return null;
133 | });
134 | }
135 |
136 | private void startSdk(@NonNull MethodChannel.Result result) {
137 | withSharedInstance(result, sharedInstance -> {
138 | sharedInstance.start();
139 | return null;
140 | });
141 | }
142 |
143 | private void initAppSpector(@NonNull MethodChannel.Result result, @Nullable String apiKey, @Nullable Map metadata, @Nullable List enabledMonitors) {
144 | if (apiKey == null) {
145 | result.error("MissingAppKey", "Cannot initialize SDK without AppKey", null);
146 | return;
147 | }
148 |
149 | final Builder builder = AppSpector.build(application)
150 | .addMetadata(metadata != null ? metadata : Collections.emptyMap());
151 |
152 | addMonitors(builder, enabledMonitors);
153 |
154 | builder.run(apiKey);
155 |
156 | //noinspection ConstantConditions
157 | AppSpector.shared().setSessionUrlListener(sessionUrlListener);
158 | }
159 |
160 | private Map createMonitorInitializerMap() {
161 | return new HashMap() {{
162 | put("logs", Builder::addLogMonitor);
163 | put("screenshot", builder -> builder.addMonitor(new ScreenshotMonitor(new FlutterScreenshotFactory(requestSender))));
164 | put("environment", Builder::addEnvironmentMonitor);
165 | put("http", Builder::addHttpMonitor);
166 | put("location", Builder::addLocationMonitor);
167 | put("performance", Builder::addPerformanceMonitor);
168 | put("sqlite", Builder::addSQLMonitor);
169 | put("shared-preferences", Builder::addSharedPreferenceMonitor);
170 | put("file-system", Builder::addFileSystemMonitor);
171 | }};
172 | }
173 |
174 | private void addMonitors(@NonNull Builder builder, @Nullable List enabledMonitors) {
175 | if (enabledMonitors == null || enabledMonitors.isEmpty()) {
176 | builder
177 | .withDefaultMonitors()
178 | .addMonitor(new ScreenshotMonitor(new FlutterScreenshotFactory(requestSender)));
179 | return;
180 | }
181 |
182 | for (String monitor : enabledMonitors) {
183 | MonitorInitializer initializer = monitorInitializerMap.get(monitor);
184 | if (initializer != null) {
185 | initializer.init(builder);
186 | } else {
187 | AppspectorLogger.d("Unknown monitor: %s", monitor);
188 | }
189 | }
190 | }
191 |
192 | private void withSharedInstance(@NonNull MethodChannel.Result result, @NonNull SharedInstanceAction action) {
193 | final AppSpector sharedInstance = AppSpector.shared();
194 | if (sharedInstance != null) {
195 | result.success(action.run(sharedInstance));
196 | } else {
197 | result.error("NotInitialized", "AppSpector shared instance is null", null);
198 | }
199 | }
200 |
201 | static MainAppSpectorHandler internalRegister(Context appContext, BinaryMessenger messenger) {
202 | final Handler mainHandler = new Handler();
203 | final MethodChannel mainChannel = new MethodChannel(messenger, "appspector_plugin");
204 | final MethodChannel eventChannel = new MethodChannel(messenger, "appspector_event_channel");
205 | final MethodChannel requestChannel = new MethodChannel(messenger, "appspector_request_channel");
206 |
207 | final MainAppSpectorHandler handler = new MainAppSpectorHandler(
208 | (Application) appContext,
209 | mainChannel,
210 | new InternalAppSpectorSessionListener(mainHandler, mainChannel),
211 | new EventReceiver(eventChannel),
212 | new RequestSender(mainHandler, requestChannel)
213 | );
214 |
215 | handler.register();
216 | return handler;
217 | }
218 |
219 | private static class InternalAppSpectorSessionListener implements SessionUrlListener {
220 |
221 | private final MethodChannel sessionUrlChannel;
222 | private final Handler handler;
223 |
224 | private InternalAppSpectorSessionListener(@NonNull Handler mainHandler, @NonNull MethodChannel sessionUrlChannel) {
225 | this.handler = mainHandler;
226 | this.sessionUrlChannel = sessionUrlChannel;
227 | }
228 |
229 | @Override
230 | public void onReceived(@NonNull String sessionUrl) {
231 | handler.post(() -> sessionUrlChannel.invokeMethod("onSessionUrl", sessionUrl));
232 | }
233 | }
234 |
235 | private interface SharedInstanceAction {
236 | @Nullable
237 | Object run(@NonNull AppSpector appSpector);
238 | }
239 |
240 | private interface MonitorInitializer {
241 | void init(@NonNull Builder builder);
242 | }
243 | }
244 |
--------------------------------------------------------------------------------
/android/src/main/java/com/appspector/flutter/RequestSender.java:
--------------------------------------------------------------------------------
1 | package com.appspector.flutter;
2 |
3 | import android.os.Handler;
4 |
5 | import androidx.annotation.NonNull;
6 |
7 | import io.flutter.plugin.common.MethodChannel;
8 |
9 | public class RequestSender {
10 |
11 | private final MethodChannel methodChannel;
12 | private final Handler handler;
13 |
14 | public RequestSender(@NonNull Handler mainHandler, @NonNull MethodChannel requestMethodChannel) {
15 | this.handler = mainHandler;
16 | this.methodChannel = requestMethodChannel;
17 | }
18 |
19 | public void executeRequest(final String requestName, final Object args, final ResponseCallback callback) {
20 | handler.post(new Runnable() {
21 | @Override
22 | public void run() {
23 | methodChannel.invokeMethod(requestName, args, new MethodChannel.Result() {
24 | @Override
25 | public void success(Object o) {
26 | callback.onSuccess(o);
27 | }
28 |
29 | @Override
30 | public void error(String s, String s1, Object o) {
31 | callback.onError(s + " " + s1);
32 | }
33 |
34 | @Override
35 | public void notImplemented() {
36 | callback.onError(String.format("%s method is not implemented", requestName));
37 | }
38 | });
39 | }
40 | });
41 | }
42 |
43 | public interface ResponseCallback {
44 |
45 | void onSuccess(Object result);
46 |
47 | void onError(String message);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/android/src/main/java/com/appspector/flutter/event/EventHandler.java:
--------------------------------------------------------------------------------
1 | package com.appspector.flutter.event;
2 |
3 | import io.flutter.plugin.common.MethodCall;
4 | import io.flutter.plugin.common.MethodChannel;
5 |
6 | /**
7 | * Abstract EventHandler to handle calls from AppSpector Flutter SDK
8 | */
9 | public abstract class EventHandler {
10 |
11 | /**
12 | * Event identifier what is used in Flutter part of SDK
13 | *
14 | * @return event name. Cannot be null
15 | */
16 | public abstract String eventName();
17 |
18 | /**
19 | * Method for handling received event
20 | *
21 | * @param call contains method name and arguments
22 | */
23 | public abstract void handle(MethodCall call);
24 | }
25 |
--------------------------------------------------------------------------------
/android/src/main/java/com/appspector/flutter/event/EventReceiver.java:
--------------------------------------------------------------------------------
1 | package com.appspector.flutter.event;
2 |
3 | import com.appspector.sdk.core.util.AppspectorLogger;
4 |
5 | import java.util.HashMap;
6 | import java.util.Map;
7 |
8 | import io.flutter.plugin.common.MethodCall;
9 | import io.flutter.plugin.common.MethodChannel;
10 |
11 | /**
12 | * EventReceiver is locator of a sdk methods. It chooses the method by name from registered ones
13 | * and execute invocation on it.
14 | */
15 | public final class EventReceiver {
16 |
17 | @SuppressWarnings({"FieldCanBeLocal", "unused"})
18 | private final MethodChannel methodChannel;
19 | private final Map registeredEvents = new HashMap<>();
20 |
21 | public EventReceiver(MethodChannel eventMethodChannel) {
22 | eventMethodChannel.setMethodCallHandler(new InternalMethodCallHandler());
23 | this.methodChannel = eventMethodChannel;
24 | }
25 |
26 | public void unsubscribe() {
27 | methodChannel.setMethodCallHandler(null);
28 | }
29 |
30 | /**
31 | * Registration of Sdk Method. In case when sdk method with the same name is already
32 | * registered at current dispatcher method will throw IllegalStateException
33 | *
34 | * @param eventHandler is Sdk Method what should be registered
35 | */
36 | public void registerEventHandler(EventHandler eventHandler) {
37 | if (registeredEvents.containsKey(eventHandler.eventName())) {
38 | throw new IllegalStateException("Action with same method name (%s) is already registered");
39 | }
40 | registeredEvents.put(eventHandler.eventName(), eventHandler);
41 | }
42 |
43 | private void handleEvent(MethodCall call, MethodChannel.Result result) {
44 | final EventHandler action = registeredEvents.get(call.method);
45 | if (action != null) {
46 | action.handle(call);
47 | result.success(null);
48 | return;
49 | }
50 | AppspectorLogger.d("Can't find action for method %s", call.method);
51 | result.notImplemented();
52 | }
53 |
54 | private class InternalMethodCallHandler implements MethodChannel.MethodCallHandler {
55 |
56 | @Override
57 | public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
58 | handleEvent(methodCall, result);
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/android/src/main/java/com/appspector/flutter/event/http/FlutterHttpTracker.java:
--------------------------------------------------------------------------------
1 | package com.appspector.flutter.event.http;
2 |
3 | import com.appspector.sdk.monitors.http.HttpMonitorObserver;
4 | import com.appspector.sdk.monitors.http.HttpRequest;
5 | import com.appspector.sdk.monitors.http.HttpResponse;
6 |
7 | import java.util.Map;
8 |
9 | final class FlutterHttpTracker {
10 |
11 | private static final String TRACKER_ID = "flutter_client_io";
12 |
13 | private FlutterHttpTracker() {
14 | }
15 |
16 | @SuppressWarnings({"ConstantConditions", "unchecked"})
17 | static void trackResponse(Map response) {
18 | Object tookMs = response.get("tookMs");
19 | HttpMonitorObserver.getTracker(TRACKER_ID).track(new HttpResponse.Builder()
20 | .requestUid((String) response.get("uid"))
21 | .code((int) response.get("code"))
22 | .error((String) response.get("error"))
23 | .body((byte[]) response.get("body"))
24 | .tookMs(tookMs instanceof Long ? (Long) tookMs : ((Integer) tookMs).longValue())
25 | .addHeaders((Map) response.get("headers"))
26 | .build());
27 | }
28 |
29 | @SuppressWarnings({"ConstantConditions", "unchecked"})
30 | static void trackRequest(Map requestData) {
31 | HttpMonitorObserver.getTracker(TRACKER_ID).track(new HttpRequest.Builder()
32 | .uid((String) requestData.get("uid"))
33 | .url((String) requestData.get("url"))
34 | .addHeaders((Map) requestData.get("headers"))
35 | .method((String) requestData.get("method"), (byte[]) requestData.get("body"))
36 | .build());
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/android/src/main/java/com/appspector/flutter/event/http/HttpRequestEventHandler.java:
--------------------------------------------------------------------------------
1 | package com.appspector.flutter.event.http;
2 |
3 | import com.appspector.flutter.event.EventHandler;
4 |
5 | import java.util.Map;
6 |
7 | import io.flutter.plugin.common.MethodCall;
8 |
9 | public final class HttpRequestEventHandler extends EventHandler {
10 |
11 | @Override
12 | public String eventName() {
13 | return "http-request";
14 | }
15 |
16 | @Override
17 | @SuppressWarnings("unchecked")
18 | public void handle(MethodCall call) {
19 | final Map requestData = (Map) call.arguments;
20 | FlutterHttpTracker.trackRequest(requestData);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/android/src/main/java/com/appspector/flutter/event/http/HttpResponseEventHandler.java:
--------------------------------------------------------------------------------
1 | package com.appspector.flutter.event.http;
2 |
3 | import com.appspector.flutter.event.EventHandler;
4 |
5 | import java.util.Map;
6 |
7 | import io.flutter.plugin.common.MethodCall;
8 |
9 | public final class HttpResponseEventHandler extends EventHandler {
10 |
11 | @Override
12 | public String eventName() {
13 | return "http-response";
14 | }
15 |
16 | @Override
17 | @SuppressWarnings("unchecked")
18 | public void handle(MethodCall call) {
19 | final Map responseData = (Map) call.arguments;
20 | FlutterHttpTracker.trackResponse(responseData);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/android/src/main/java/com/appspector/flutter/event/log/LogEventHandler.java:
--------------------------------------------------------------------------------
1 | package com.appspector.flutter.event.log;
2 |
3 | import com.appspector.flutter.event.EventHandler;
4 | import com.appspector.sdk.monitors.log.Logger;
5 |
6 | import java.util.Map;
7 |
8 | import io.flutter.plugin.common.MethodCall;
9 |
10 | public class LogEventHandler extends EventHandler {
11 |
12 | @Override
13 | public String eventName() {
14 | return "log-event";
15 | }
16 |
17 | @Override
18 | @SuppressWarnings({"unchecked", "ConstantConditions"})
19 | public void handle(MethodCall call) {
20 | Map args = (Map) call.arguments;
21 | Logger.log(
22 | (Integer) args.get("level"),
23 | (String) args.get("tag"),
24 | (String) args.get("message")
25 | );
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/android/src/main/java/com/appspector/flutter/screenshot/FlutterScreenshotFactory.java:
--------------------------------------------------------------------------------
1 | package com.appspector.flutter.screenshot;
2 |
3 | import com.appspector.flutter.RequestSender;
4 | import com.appspector.sdk.monitors.screenshot.ScreenshotCallback;
5 | import com.appspector.sdk.monitors.screenshot.ScreenshotFactory;
6 |
7 | import java.util.HashMap;
8 |
9 | public class FlutterScreenshotFactory implements ScreenshotFactory {
10 |
11 | private final RequestSender requestSender;
12 |
13 | public FlutterScreenshotFactory(RequestSender requestSender) {
14 | this.requestSender = requestSender;
15 | }
16 |
17 | @Override
18 | public void takeScreenshot(int maxWidth, int quality, final ScreenshotCallback screenshotCallback) {
19 | final HashMap args = new HashMap<>();
20 | args.put("max_width", maxWidth);
21 | args.put("quality", quality);
22 | requestSender.executeRequest("take_screenshot", args, new RequestSender.ResponseCallback() {
23 | @Override
24 | public void onSuccess(Object result) {
25 | screenshotCallback.onSuccess((byte[]) result);
26 | }
27 |
28 | @Override
29 | public void onError(String message) {
30 | screenshotCallback.onError(message);
31 | }
32 | });
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.lock
4 | *.log
5 | *.pyc
6 | *.swp
7 | .DS_Store
8 | .atom/
9 | .buildlog/
10 | .history
11 | .svn/
12 |
13 | # IntelliJ related
14 | *.iml
15 | *.ipr
16 | *.iws
17 | .idea/
18 |
19 | # Visual Studio Code related
20 | .vscode/
21 |
22 | # Flutter/Dart/Pub related
23 | **/doc/api/
24 | .dart_tool/
25 | .flutter-plugins
26 | .packages
27 | .pub-cache/
28 | .pub/
29 | build/
30 |
31 | # Android related
32 | **/android/**/gradle-wrapper.jar
33 | **/android/.gradle
34 | **/android/captures/
35 | **/android/gradlew
36 | **/android/gradlew.bat
37 | **/android/local.properties
38 | **/android/**/GeneratedPluginRegistrant.java
39 |
40 | # iOS/XCode related
41 | **/ios/**/*.mode1v3
42 | **/ios/**/*.mode2v3
43 | **/ios/**/*.moved-aside
44 | **/ios/**/*.pbxuser
45 | **/ios/**/*.perspectivev3
46 | **/ios/**/*sync/
47 | **/ios/**/.sconsign.dblite
48 | **/ios/**/.tags*
49 | **/ios/**/.vagrant/
50 | **/ios/**/DerivedData/
51 | **/ios/**/Icon?
52 | **/ios/**/Pods/
53 | **/ios/**/.symlinks/
54 | /ios/.symlinks/
55 | **/ios/**/profile
56 | **/ios/**/xcuserdata
57 | **/ios/.generated/
58 | **/ios/Flutter/App.framework
59 | **/ios/Flutter/Flutter.framework
60 | **/ios/Flutter/Generated.xcconfig
61 | **/ios/Flutter/.last_build_id
62 | **/ios/Flutter/app.flx
63 | **/ios/Flutter/app.zip
64 | **/ios/Flutter/flutter_assets/
65 | **/ios/ServiceDefinitions.json
66 | **/ios/Runner/GeneratedPluginRegistrant.*
67 |
68 | # Exceptions to above rules.
69 | !**/ios/**/default.mode1v3
70 | !**/ios/**/default.mode2v3
71 | !**/ios/**/default.pbxuser
72 | !**/ios/**/default.perspectivev3
73 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
74 | flutter_export_environment.sh
75 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | # AppSpector plugin example app
2 |
3 | Demonstrates how to use the AppSpector plugin.
4 |
5 | ## Getting Started
6 |
7 | This project is a starting point for a Flutter application.
8 |
9 | A few resources to get you started if this is your first Flutter project:
10 |
11 | - [Lab: Write your first Flutter app](https://flutter.io/docs/get-started/codelab)
12 | - [Cookbook: Useful Flutter samples](https://flutter.io/docs/cookbook)
13 |
14 | For help getting started with Flutter, view our
15 | [online documentation](https://flutter.io/docs), which offers tutorials,
16 | samples, guidance on mobile development, and a full API reference.
17 |
--------------------------------------------------------------------------------
/example/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | def localProperties = new Properties()
2 | def localPropertiesFile = rootProject.file('local.properties')
3 | if (localPropertiesFile.exists()) {
4 | localPropertiesFile.withReader('UTF-8') { reader ->
5 | localProperties.load(reader)
6 | }
7 | }
8 |
9 | def flutterRoot = localProperties.getProperty('flutter.sdk')
10 | if (flutterRoot == null) {
11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
12 | }
13 |
14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
15 | if (flutterVersionCode == null) {
16 | flutterVersionCode = '1'
17 | }
18 |
19 | def flutterVersionName = localProperties.getProperty('flutter.versionName')
20 | if (flutterVersionName == null) {
21 | flutterVersionName = '1.0'
22 | }
23 |
24 | apply plugin: 'com.android.application'
25 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
26 |
27 | android {
28 | compileSdkVersion 33
29 | namespace "com.appspector.flutter.example"
30 |
31 | lintOptions {
32 | disable 'InvalidPackage'
33 | }
34 |
35 | defaultConfig {
36 | applicationId "com.appspector.flutter.example"
37 | minSdkVersion 21
38 | targetSdkVersion 33
39 |
40 | versionCode flutterVersionCode.toInteger()
41 | versionName flutterVersionName
42 | }
43 |
44 | buildTypes {
45 | release {
46 | // TODO: Add your own signing config for the release build.
47 | // Signing with the debug keys for now, so `flutter run --release` works.
48 | signingConfig signingConfigs.debug
49 | }
50 | }
51 | }
52 |
53 | configurations.all {
54 | resolutionStrategy.eachDependency { DependencyResolveDetails details ->
55 | if (details.requested.group == "org.jetbrains.kotlin") {
56 | details.useVersion "1.8.0"
57 | details.because "Use the newest kotlin version"
58 | }
59 | }
60 | }
61 |
62 | flutter {
63 | source '../..'
64 | }
65 |
66 | dependencies {
67 | }
68 |
--------------------------------------------------------------------------------
/example/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
14 |
17 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/example/android/app/src/main/java/com/appspector/flutter/example/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.appspector.flutter.example;
2 |
3 | import androidx.annotation.NonNull;
4 |
5 | import io.flutter.embedding.android.FlutterActivity;
6 | import io.flutter.embedding.engine.FlutterEngine;
7 | import io.flutter.plugins.GeneratedPluginRegistrant;
8 |
9 | public class MainActivity extends FlutterActivity {
10 |
11 | @Override
12 | public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
13 | GeneratedPluginRegistrant.registerWith(flutterEngine);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/example/android/app/src/main/res/drawable/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
--------------------------------------------------------------------------------
/example/android/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | }
6 |
7 | dependencies {
8 | classpath 'com.android.tools.build:gradle:8.0.2'
9 | }
10 | }
11 |
12 | allprojects {
13 | repositories {
14 | google()
15 | mavenCentral()
16 | }
17 | }
18 |
19 | rootProject.buildDir = '../build'
20 | subprojects {
21 | project.buildDir = "${rootProject.buildDir}/${project.name}"
22 | }
23 | subprojects {
24 | project.evaluationDependsOn(':app')
25 | }
26 |
27 | tasks.register("clean", Delete) {
28 | delete rootProject.buildDir
29 | }
30 |
--------------------------------------------------------------------------------
/example/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx1536M
2 | android.useAndroidX=true
3 |
--------------------------------------------------------------------------------
/example/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Jun 23 08:50:38 CEST 2017
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-all.zip
7 |
--------------------------------------------------------------------------------
/example/android/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
4 |
5 | def plugins = new Properties()
6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
7 | if (pluginsFile.exists()) {
8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
9 | }
10 |
11 | plugins.each { name, path ->
12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
13 | include ":$name"
14 | project(":$name").projectDir = pluginDirectory
15 | }
16 |
--------------------------------------------------------------------------------
/example/assets/patch.json:
--------------------------------------------------------------------------------
1 | {
2 | "age": 26
3 | }
4 |
--------------------------------------------------------------------------------
/example/assets/post.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Jon Snow",
3 | "age": 25,
4 | "country": "Winterfell",
5 | "gender": "male",
6 | "additional": "Jon Snow is a fictional character in the A Song of Ice and Fire series of fantasy novels by American author George R. R. Martin, and its television adaptation Game of Thrones, in which he is portrayed by English actor Kit Harington. He is a prominent point of view character in the novels, and has been called one of the author's \"finest creations\" and most popular characters by The New York Times.[1][2] Jon is a main character in the TV series, and his storyline in the 2015 season 5 finale generated a strong reaction among viewers. Speculation about the character's parentage has also been a popular topic of discussion among fans of both the books and the TV series."
7 | }
8 |
--------------------------------------------------------------------------------
/example/assets/put.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Jon Snow",
3 | "age": "26",
4 | "country": "Winterfell",
5 | "gender": "male",
6 | "additional": "Jon Snow is a fictional character in the A Song of Ice and Fire series of fantasy novels by American author George R. R. Martin, and its television adaptation Game of Thrones, in which he is portrayed by English actor Kit Harington. He is a prominent point of view character in the novels, and has been called one of the author's \"finest creations\" and most popular characters by The New York Times.[1][2] Jon is a main character in the TV series, and his storyline in the 2015 season 5 finale generated a strong reaction among viewers. Speculation about the character's parentage has also been a popular topic of discussion among fans of both the books and the TV series."
7 | }
8 |
--------------------------------------------------------------------------------
/example/ios/AppSpectorPluginTestPlan.xctestplan:
--------------------------------------------------------------------------------
1 | {
2 | "configurations" : [
3 | {
4 | "id" : "AC7AA52D-790A-4B2A-B085-2AE5B3C71E2A",
5 | "name" : "Configuration 1",
6 | "options" : {
7 | "targetForVariableExpansion" : {
8 | "containerPath" : "container:Runner.xcodeproj",
9 | "identifier" : "97C146ED1CF9000F007C117D",
10 | "name" : "Runner"
11 | }
12 | }
13 | }
14 | ],
15 | "defaultOptions" : {
16 | "codeCoverage" : {
17 | "targets" : [
18 | {
19 | "containerPath" : "container:Pods\/Pods.xcodeproj",
20 | "identifier" : "8EE4B296639698DC467B95DEA0D6285A",
21 | "name" : "appspector"
22 | }
23 | ]
24 | }
25 | },
26 | "testTargets" : [
27 | {
28 | "target" : {
29 | "containerPath" : "container:Runner.xcodeproj",
30 | "identifier" : "0EB522EC2260D6F40076C990",
31 | "name" : "AppSpectorPluginTests"
32 | }
33 | }
34 | ],
35 | "version" : 1
36 | }
37 |
--------------------------------------------------------------------------------
/example/ios/AppSpectorPluginTests/ASPluginCallValidatorTests.m:
--------------------------------------------------------------------------------
1 | //
2 | // ASPluginCallValidatorTests.m
3 | // AppSpectorPluginTests
4 | //
5 | // Created by Deszip on 22.12.2019.
6 | // Copyright © 2019 The Chromium Authors. All rights reserved.
7 | //
8 |
9 | #import
10 | #import
11 | #import
12 |
13 | #import "ASPluginCallValidator.h"
14 |
15 |
16 | @interface ASPluginCallValidatorTests : XCTestCase
17 |
18 | @property (strong, nonatomic) ASPluginCallValidator *validator;
19 |
20 | @end
21 |
22 | @implementation ASPluginCallValidatorTests
23 |
24 | - (void)setUp {
25 | self.validator = [ASPluginCallValidator new];
26 | }
27 |
28 | - (void)tearDown {
29 | self.validator = nil;
30 | }
31 |
32 | - (void)testValidatorChecksControlMethods {
33 | expect([self.validator controlMethodSupported:kRunMethodName]).to.beTruthy();
34 | expect([self.validator controlMethodSupported:@"FAKE_METHOD"]).to.beFalsy();
35 | }
36 |
37 | - (void)testValidatorChecksEventMethods {
38 | expect([self.validator eventMethodSupported:kHTTPRequestMethodName]).to.beTruthy();
39 | expect([self.validator eventMethodSupported:kHTTPResponseMethodName]).to.beTruthy();
40 | expect([self.validator eventMethodSupported:kLogEventMethodName]).to.beTruthy();
41 |
42 | expect([self.validator eventMethodSupported:@"FAKE_METHOD"]).to.beFalsy();
43 | }
44 |
45 | - (void)testRunCallParametersValidation {
46 | [self verifyValidParams:@{ kAPIKeyArgument : @"API_KEY",
47 | kEnabledMonitorsArgument : @[],
48 | kMetadataArgument : @""
49 | } forCall:kRunMethodName];
50 | [self verifyInvalidParams:@{ @"FAKE_ARG" : @"FAKE_VALUE"} forCall:kRunMethodName];
51 | }
52 |
53 | - (void)testHTTPRequestCallParametersValidation {
54 | [self verifyValidParams:@{ kUIDArgument : @"UID",
55 | kURLArgument : @"URL",
56 | kMethodArgument : @"METHOD",
57 | kBodyArgument : @"BODY",
58 | kHeadersArgument : @"HEADERS" }
59 | forCall:kHTTPRequestMethodName];
60 |
61 | [self verifyInvalidParams:@{ @"FAKE_ARG" : @"FAKE_VALUE"} forCall:kHTTPRequestMethodName];
62 | }
63 |
64 | - (void)testHTTPResponseCallParametersValidation {
65 | [self verifyValidParams:@{ kUIDArgument : @"UID",
66 | kCodeArgument : @"CODE",
67 | kBodyArgument : @"BODY",
68 | kHeadersArgument : @"BODY",
69 | kTookMSArgument : @"HEADERS" }
70 | forCall:kHTTPResponseMethodName];
71 |
72 | [self verifyInvalidParams:@{ @"FAKE_ARG" : @"FAKE_VALUE"} forCall:kHTTPResponseMethodName];
73 | }
74 |
75 | - (void)testLogCallParametersValidation {
76 | [self verifyValidParams:@{ kLevelArgument : @"UID",
77 | kTagArgument : @"CODE",
78 | kMessageArgument : @"BODY" }
79 | forCall:kLogEventMethodName];
80 |
81 | [self verifyInvalidParams:@{ @"FAKE_ARG" : @"FAKE_VALUE"} forCall:kLogEventMethodName];
82 | }
83 |
84 | #pragma mark - Validators -
85 |
86 | - (void)verifyValidParams:(ASPluginMethodArgumentsList *)args forCall:(ASPluginMethodName *)methodName {
87 | NSError *validCallError = nil;
88 | BOOL success = [self.validator argumentsValid:args call:methodName error:&validCallError];
89 | expect(success).to.beTruthy();
90 | expect(validCallError).to.beNil();
91 | }
92 |
93 | - (void)verifyInvalidParams:(ASPluginMethodArgumentsList *)args forCall:(ASPluginMethodName *)methodName {
94 | NSError *invalidCallError = nil;
95 | BOOL success = [self.validator argumentsValid:args call:methodName error:&invalidCallError];
96 | expect(success).to.beFalsy();
97 | expect(invalidCallError).toNot.beNil();
98 | expect(invalidCallError.localizedDescription).toNot.beNil();
99 | }
100 |
101 | @end
102 |
--------------------------------------------------------------------------------
/example/ios/AppSpectorPluginTests/ASPluginEventsHandlerTests.m:
--------------------------------------------------------------------------------
1 | //
2 | // ASPluginEventsHandlerTests.m
3 | // AppSpectorPluginTests
4 | //
5 | // Created by Deszip on 24.12.2019.
6 | // Copyright © 2019 The Chromium Authors. All rights reserved.
7 | //
8 |
9 | #import
10 | #import
11 | #import
12 |
13 | #import "ASPluginEventsHandler.h"
14 | #import
15 |
16 | @interface ASPluginEventsHandlerTests : XCTestCase
17 |
18 | @property (strong, nonatomic) id callValidatorMock;
19 | @property (strong, nonatomic) ASPluginEventsHandler *handler;
20 |
21 | @end
22 |
23 | @implementation ASPluginEventsHandlerTests
24 |
25 | - (void)setUp {
26 | self.callValidatorMock = OCMClassMock([ASPluginCallValidator class]);
27 | self.handler = [[ASPluginEventsHandler alloc] initWithCallValidator:self.callValidatorMock];
28 | }
29 |
30 | - (void)tearDown {
31 | self.callValidatorMock = nil;
32 | self.handler = nil;
33 | }
34 |
35 | #pragma mark - Invalid calls -
36 |
37 | - (void)testHandlerReturnsErrorForInvalidCallName {
38 | XCTestExpectation *e = [self expectationWithDescription:@""];
39 | OCMStub([self.callValidatorMock eventMethodSupported:[OCMArg any]]).andReturn(NO);
40 |
41 | FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"foo" arguments:@{}];
42 | [self.handler handleMethodCall:call result:^(id result) {
43 | expect(result).to.equal(FlutterMethodNotImplemented);
44 | [e fulfill];
45 | }];
46 |
47 | [self waitForExpectations:@[e] timeout:1.1];
48 | }
49 |
50 | - (void)testHandlerReturnsErrorForInvalidCallArgs {
51 | XCTestExpectation *e = [self expectationWithDescription:@""];
52 |
53 | OCMStub([self.callValidatorMock eventMethodSupported:[OCMArg any]]).andReturn(YES);
54 |
55 | NSString *errorDescription = @"foo_error";
56 | NSError *error = OCMClassMock([NSError class]);
57 | OCMStub([error localizedDescription]).andReturn(errorDescription);
58 | OCMStub([self.callValidatorMock argumentsValid:[OCMArg any] call:[OCMArg any] error:[OCMArg setTo:error]]).andReturn(NO);
59 |
60 | FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"foo" arguments:@{}];
61 | [self.handler handleMethodCall:call result:^(id result) {
62 | expect(result).to.equal(errorDescription);
63 | [e fulfill];
64 | }];
65 |
66 | [self waitForExpectations:@[e] timeout:1.1];
67 | }
68 |
69 | - (void)testHandlerSendsLogEvent {
70 | NSDictionary *payload = @{ @"level" : @"warning",
71 | @"message" : @"test" };
72 | FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:kLogEventMethodName arguments:payload];
73 | ASExternalEvent *expectedEvent = [[ASExternalEvent alloc] initWithMonitorID:AS_LOG_MONITOR eventID:@"log" payload:payload];
74 |
75 | [self performCall:call andValidateEvent:expectedEvent];
76 | }
77 |
78 | - (void)testHandlerSendsHTTPRequestEvent {
79 | NSData *rawData = [@"DESDBEEF" dataUsingEncoding:NSUTF8StringEncoding];
80 | FlutterStandardTypedData *flutterData = [FlutterStandardTypedData typedDataWithBytes:rawData];
81 | NSString *UUID = [NSUUID UUID].UUIDString;
82 |
83 | NSDictionary *args = @{ @"uid" : UUID,
84 | @"url" : @"http://google.com",
85 | @"method" : @"GET",
86 | @"body" : flutterData,
87 | @"headers" : @{} };
88 |
89 | NSDictionary *payload = @{ @"uuid" : UUID,
90 | @"url" : @"http://google.com",
91 | @"method" : @"GET",
92 | @"body" : rawData,
93 | @"hasLargeBody" : @(NO),
94 | @"headers" : @{} };
95 | FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:kHTTPRequestMethodName arguments:args];
96 | ASExternalEvent *expectedEvent = [[ASExternalEvent alloc] initWithMonitorID:AS_HTTP_MONITOR eventID:@"http-request" payload:payload];
97 |
98 | [self performCall:call andValidateEvent:expectedEvent];
99 | }
100 |
101 | /// If handler gets event with invalid body it sould substitute it with emty data
102 | - (void)testHandlerSendsHTTPRequestEventWithoutBody {
103 | NSString *UUID = [NSUUID UUID].UUIDString;
104 | NSDictionary *args = @{ @"uid" : UUID,
105 | @"url" : @"http://google.com",
106 | @"method" : @"GET",
107 | @"body" : [NSObject new],
108 | @"headers" : @{} };
109 |
110 | NSDictionary *payload = @{ @"uuid" : UUID,
111 | @"url" : @"http://google.com",
112 | @"method" : @"GET",
113 | @"body" : [NSData data],
114 | @"hasLargeBody" : @(NO),
115 | @"headers" : @{} };
116 | FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:kHTTPRequestMethodName arguments:args];
117 | ASExternalEvent *expectedEvent = [[ASExternalEvent alloc] initWithMonitorID:AS_HTTP_MONITOR eventID:@"http-request" payload:payload];
118 |
119 | [self performCall:call andValidateEvent:expectedEvent];
120 | }
121 |
122 | - (void)testHandlerSendsHTTPResponseEvent {
123 | NSData *rawData = [@"DESDBEEF" dataUsingEncoding:NSUTF8StringEncoding];
124 | FlutterStandardTypedData *flutterData = [FlutterStandardTypedData typedDataWithBytes:rawData];
125 | NSString *UUID = [NSUUID UUID].UUIDString;
126 |
127 | NSDictionary *args = @{ @"uid" : UUID,
128 | @"code" : @(200),
129 | @"body" : flutterData,
130 | @"headers" : @{},
131 | @"tookMs" : @(100) };
132 |
133 | NSDictionary *payload = @{ @"uuid" : UUID,
134 | @"statusCode" : @(200),
135 | @"body" : rawData,
136 | @"hasLargeBody" : @(NO),
137 | @"headers" : @{},
138 | @"responseDuration" : @(100),
139 | @"error" : @"" };
140 | FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:kHTTPResponseMethodName arguments:args];
141 | ASExternalEvent *expectedEvent = [[ASExternalEvent alloc] initWithMonitorID:AS_HTTP_MONITOR eventID:@"http-response" payload:payload];
142 |
143 | [self performCall:call andValidateEvent:expectedEvent];
144 | }
145 |
146 | /// If handler gets event with invalid body it sould substitute it with emty data
147 | - (void)testHandlerSendsHTTPResponseEventWithoutBody {
148 | NSString *UUID = [NSUUID UUID].UUIDString;
149 | NSDictionary *args = @{ @"uid" : UUID,
150 | @"code" : @(200),
151 | @"body" : [NSObject new],
152 | @"headers" : @{},
153 | @"tookMs" : @(100) };
154 |
155 | NSDictionary *payload = @{ @"uuid" : UUID,
156 | @"statusCode" : @(200),
157 | @"body" : [NSData data],
158 | @"hasLargeBody" : @(NO),
159 | @"headers" : @{},
160 | @"responseDuration" : @(100),
161 | @"error" : @"" };
162 | FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:kHTTPResponseMethodName arguments:args];
163 | ASExternalEvent *expectedEvent = [[ASExternalEvent alloc] initWithMonitorID:AS_HTTP_MONITOR eventID:@"http-response" payload:payload];
164 |
165 | [self performCall:call andValidateEvent:expectedEvent];
166 | }
167 |
168 |
169 | #pragma mark - Event sender
170 |
171 | - (void)performCall:(FlutterMethodCall *)call andValidateEvent:(ASExternalEvent *)expectedEvent {
172 | XCTestExpectation *e = [self expectationWithDescription:@""];
173 |
174 | OCMStub([self.callValidatorMock eventMethodSupported:[OCMArg any]]).andReturn(YES);
175 | OCMStub([self.callValidatorMock argumentsValid:[OCMArg any] call:[OCMArg any] error:[OCMArg anyObjectRef]]).andReturn(YES);
176 |
177 | id sdkMock = [OCMockObject mockForClass:[AppSpector class]];
178 | OCMExpect([sdkMock sendEvent:[OCMArg checkWithBlock:^BOOL(ASExternalEvent *event) {
179 | expect(event.monitorID).to.equal(expectedEvent.monitorID);
180 | expect(event.eventID).to.equal(expectedEvent.eventID);
181 | expect(event.payload).to.equal(expectedEvent.payload);
182 | return YES;
183 | }]]);
184 |
185 | [self.handler handleMethodCall:call result:^(id result) {
186 | expect(result).toNot.beNil();
187 | OCMVerifyAll(sdkMock);
188 | [e fulfill];
189 | }];
190 |
191 | [self waitForExpectations:@[e] timeout:1.1];
192 | }
193 |
194 | @end
195 |
--------------------------------------------------------------------------------
/example/ios/AppSpectorPluginTests/AppSpectorPluginTests.m:
--------------------------------------------------------------------------------
1 | //
2 | // AppSpectorPluginTests.m
3 | // AppSpectorPluginTests
4 | //
5 | // Created by Deszip on 12/04/2019.
6 | // Copyright © 2019 The Chromium Authors. All rights reserved.
7 | //
8 |
9 | #import
10 | #import
11 | #import
12 |
13 | #import
14 | #import "AppSpectorPlugin.h"
15 |
16 | @interface AppSpectorPluginTests : XCTestCase
17 |
18 | @property (strong, nonatomic) id validatorMock;
19 | @property (strong, nonatomic) AppSpectorPlugin *handler;
20 | @property (strong, nonatomic) FlutterMethodChannel *channel;
21 |
22 | @end
23 |
24 | @implementation AppSpectorPluginTests
25 |
26 | - (void)setUp {
27 | self.validatorMock = OCMClassMock([ASPluginCallValidator class]);
28 | self.channel = OCMClassMock([FlutterMethodChannel class]);
29 | self.handler = [[AppSpectorPlugin alloc] initWithCallValidator:self.validatorMock channel:self.channel];
30 | }
31 |
32 | - (void)tearDown {
33 | self.validatorMock = nil;
34 | self.handler = nil;
35 | }
36 |
37 | - (void)testHandlerSupportsRunCall {
38 | XCTestExpectation *e = [self expectationWithDescription:@""];
39 |
40 | FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"run" arguments:@{ @"apiKey" : @"DEADBEEF",
41 | @"enabledMonitors" : @[],
42 | @"metadata" : @{}
43 | }];
44 | OCMStub([self.validatorMock controlMethodSupported:[OCMArg any]]).andReturn(YES);
45 | OCMStub([self.validatorMock argumentsValid:call.arguments call:call.method error:[OCMArg anyObjectRef]]).andReturn(YES);
46 |
47 | id sdkMock = OCMClassMock([AppSpector class]);
48 | OCMExpect(ClassMethod([sdkMock runWithConfig:[OCMArg any]]));
49 |
50 | [self.handler handleMethodCall:call result:^(id result) {
51 | expect(result).equal(@"Ok");
52 | OCMVerifyAll(sdkMock);
53 | [e fulfill];
54 | }];
55 |
56 | [self waitForExpectations:@[e] timeout:0.1];
57 | }
58 |
59 | - (void)testHandlerSupportsStopCall {
60 | XCTestExpectation *e = [self expectationWithDescription:@""];
61 |
62 | FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"stop" arguments:@{}];
63 | OCMStub([self.validatorMock controlMethodSupported:[OCMArg any]]).andReturn(YES);
64 | OCMStub([self.validatorMock argumentsValid:call.arguments call:call.method error:[OCMArg anyObjectRef]]).andReturn(YES);
65 |
66 | id sdkMock = OCMClassMock([AppSpector class]);
67 | OCMExpect(ClassMethod([sdkMock stop]));
68 |
69 | [self.handler handleMethodCall:call result:^(id result) {
70 | expect(result).equal(@"Ok");
71 | OCMVerifyAll(sdkMock);
72 | [e fulfill];
73 | }];
74 |
75 | [self waitForExpectations:@[e] timeout:0.1];
76 | }
77 |
78 | - (void)testHandlerSupportsIsStartedCall {
79 | XCTestExpectation *e = [self expectationWithDescription:@""];
80 |
81 | FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"isStarted" arguments:@{}];
82 | OCMStub([self.validatorMock controlMethodSupported:[OCMArg any]]).andReturn(YES);
83 | OCMStub([self.validatorMock argumentsValid:call.arguments call:call.method error:[OCMArg anyObjectRef]]).andReturn(YES);
84 |
85 | id sdkMock = OCMClassMock([AppSpector class]);
86 | OCMStub(ClassMethod([sdkMock isRunning])).andReturn(YES);
87 |
88 | [self.handler handleMethodCall:call result:^(id result) {
89 | expect(result).beTruthy();
90 | [e fulfill];
91 | }];
92 |
93 | [self waitForExpectations:@[e] timeout:0.1];
94 | }
95 |
96 | - (void)testHandlerSupportsSetMetadataCall {
97 | XCTestExpectation *e = [self expectationWithDescription:@""];
98 |
99 | FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"setMetadata" arguments:@{ @"key" : @"userSpecifiedDeviceName",
100 | @"value" : @"device name"
101 | }];
102 | OCMStub([self.validatorMock controlMethodSupported:[OCMArg any]]).andReturn(YES);
103 | OCMStub([self.validatorMock argumentsValid:call.arguments call:call.method error:[OCMArg anyObjectRef]]).andReturn(YES);
104 |
105 | ASMetadata *expectedMetadata = @{AS_DEVICE_NAME_KEY:@"device name"};
106 |
107 | id sdkMock = OCMClassMock([AppSpector class]);
108 | OCMExpect(ClassMethod([sdkMock updateMetadata: expectedMetadata]));
109 |
110 | [self.handler handleMethodCall:call result:^(id result) {
111 | expect(result).equal(@"Ok");
112 | OCMVerifyAll(sdkMock);
113 | [e fulfill];
114 | }];
115 |
116 | [self waitForExpectations:@[e] timeout:0.1];
117 | }
118 |
119 | - (void)testHandlerSupportsRemoveMetadataCall {
120 | XCTestExpectation *e = [self expectationWithDescription:@""];
121 |
122 | FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"removeMetadata" arguments:@{ @"key" : @"userSpecifiedDeviceName"
123 | }];
124 | OCMStub([self.validatorMock controlMethodSupported:[OCMArg any]]).andReturn(YES);
125 | OCMStub([self.validatorMock argumentsValid:call.arguments call:call.method error:[OCMArg anyObjectRef]]).andReturn(YES);
126 |
127 | ASMetadata *expectedMetadata = @{};
128 |
129 | id sdkMock = OCMClassMock([AppSpector class]);
130 | OCMExpect(ClassMethod([sdkMock updateMetadata: expectedMetadata]));
131 |
132 | [self.handler handleMethodCall:call result:^(id result) {
133 | expect(result).equal(@"Ok");
134 | OCMVerifyAll(sdkMock);
135 | [e fulfill];
136 | }];
137 |
138 | [self waitForExpectations:@[e] timeout:0.1];
139 | }
140 |
141 | - (void)testHandlerValidatesCallArguments {
142 | XCTestExpectation *e = [self expectationWithDescription:@""];
143 |
144 | FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"run" arguments:@{ @"invalidArg" : @"DEADBEEF" }];
145 |
146 | OCMStub([self.validatorMock controlMethodSupported:[OCMArg any]]).andReturn(YES);
147 | OCMExpect([self.validatorMock argumentsValid:call.arguments call:call.method error:[OCMArg anyObjectRef]]).andReturn(YES);
148 |
149 | [self.handler handleMethodCall:call result:^(id result) {
150 | OCMVerifyAll(self.validatorMock);
151 | [e fulfill];
152 | }];
153 |
154 | [self waitForExpectations:@[e] timeout:0.1];
155 | }
156 |
157 | - (void)testHandlerDoesntPerformCallWithInvalidArgs {
158 | XCTestExpectation *e = [self expectationWithDescription:@""];
159 |
160 | FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"run" arguments:@{ @"invalidArg" : @"DEADBEEF" }];
161 | OCMStub([self.validatorMock controlMethodSupported:[OCMArg any]]).andReturn(YES);
162 | OCMStub([self.validatorMock argumentsValid:call.arguments call:call.method error:[OCMArg anyObjectRef]]).andReturn(NO);
163 |
164 | id sdkMock = OCMClassMock([AppSpector class]);
165 | OCMReject(ClassMethod([sdkMock runWithConfig:[OCMArg any]]));
166 |
167 | [self.handler handleMethodCall:call result:^(id result) {
168 | OCMVerifyAll(sdkMock);
169 | [e fulfill];
170 | }];
171 |
172 | [self waitForExpectations:@[e] timeout:0.1];
173 | }
174 |
175 | - (void)testHandlerRejectsUnknownCalls {
176 | XCTestExpectation *e = [self expectationWithDescription:@""];
177 |
178 | OCMExpect([self.validatorMock controlMethodSupported:[OCMArg any]]);
179 |
180 | FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"foo" arguments:@{}];
181 | [self.handler handleMethodCall:call result:^(id result) {
182 | expect(result).notTo.equal(@"Ok");
183 | OCMVerifyAll(self.validatorMock);
184 | [e fulfill];
185 | }];
186 |
187 | [self waitForExpectations:@[e] timeout:0.1];
188 | }
189 |
190 | - (void)testHandlerExtractsEnvFlagFromArgs {
191 | XCTestExpectation *e = [self expectationWithDescription:@""];
192 |
193 | FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"run" arguments:@{ @"apiKey" : @"DEADBEEF",
194 | @"enabledMonitors" : @[],
195 | @"metadata" : @{ @"APPSPECTOR_CHECK_STORE_ENVIRONMENT" : @"false" }
196 | }];
197 | OCMStub([self.validatorMock controlMethodSupported:[OCMArg any]]).andReturn(YES);
198 | OCMStub([self.validatorMock argumentsValid:call.arguments call:call.method error:[OCMArg anyObjectRef]]).andReturn(YES);
199 |
200 | id sdkMock = OCMClassMock([AppSpector class]);
201 | OCMExpect(ClassMethod([sdkMock runWithConfig:[OCMArg checkWithBlock:^BOOL(AppSpectorConfig *config) {
202 | expect([config valueForKey:@"disableProductionCheck"]).to.beTruthy();
203 | return YES;
204 | }]]));
205 |
206 | [self.handler handleMethodCall:call result:^(id result) {
207 | expect(result).equal(@"Ok");
208 | OCMVerifyAll(sdkMock);
209 | [e fulfill];
210 | }];
211 |
212 | [self waitForExpectations:@[e] timeout:0.1];
213 | }
214 |
215 | @end
216 |
--------------------------------------------------------------------------------
/example/ios/AppSpectorPluginTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/example/ios/Flutter/AppFrameworkInfo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | App
9 | CFBundleIdentifier
10 | io.flutter.flutter.app
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | App
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1.0
23 | MinimumOSVersion
24 | 9.0
25 |
26 |
27 |
--------------------------------------------------------------------------------
/example/ios/Flutter/Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/example/ios/Flutter/Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/example/ios/Podfile:
--------------------------------------------------------------------------------
1 | # Uncomment this line to define a global platform for your project
2 | # platform :ios, '9.0'
3 |
4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true'
6 |
7 | project 'Runner', {
8 | 'Debug' => :debug,
9 | 'Profile' => :release,
10 | 'Release' => :release,
11 | }
12 |
13 | def flutter_root
14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
15 | unless File.exist?(generated_xcode_build_settings_path)
16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
17 | end
18 |
19 | File.foreach(generated_xcode_build_settings_path) do |line|
20 | matches = line.match(/FLUTTER_ROOT\=(.*)/)
21 | return matches[1].strip if matches
22 | end
23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
24 | end
25 |
26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
27 |
28 | flutter_ios_podfile_setup
29 |
30 | target 'Runner' do
31 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
32 | target 'AppSpectorPluginTests' do
33 | pod 'Expecta', '~> 1.0.5'
34 | pod 'OCMock', '~> 3.4'
35 | end
36 | end
37 |
38 | post_install do |installer|
39 | installer.pods_project.targets.each do |target|
40 | flutter_additional_ios_build_settings(target)
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
46 |
47 |
53 |
54 |
55 |
56 |
62 |
63 |
64 |
65 |
68 |
69 |
70 |
71 |
81 |
83 |
89 |
90 |
91 |
92 |
98 |
100 |
106 |
107 |
108 |
109 |
111 |
112 |
115 |
116 |
117 |
--------------------------------------------------------------------------------
/example/ios/Runner.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/example/ios/Runner/AppDelegate.h:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 |
4 | @interface AppDelegate : FlutterAppDelegate
5 |
6 | @end
7 |
--------------------------------------------------------------------------------
/example/ios/Runner/AppDelegate.m:
--------------------------------------------------------------------------------
1 | #include "AppDelegate.h"
2 | #include "GeneratedPluginRegistrant.h"
3 |
4 | #import "AppSpectorPlugin.h"
5 |
6 | @implementation AppDelegate
7 |
8 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
9 | [GeneratedPluginRegistrant registerWithRegistry:self];
10 | // Override point for customization after application launch.
11 | return [super application:application didFinishLaunchingWithOptions:launchOptions];
12 | }
13 |
14 | @end
15 |
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "20x20",
5 | "idiom" : "iphone",
6 | "filename" : "Icon-App-20x20@2x.png",
7 | "scale" : "2x"
8 | },
9 | {
10 | "size" : "20x20",
11 | "idiom" : "iphone",
12 | "filename" : "Icon-App-20x20@3x.png",
13 | "scale" : "3x"
14 | },
15 | {
16 | "size" : "29x29",
17 | "idiom" : "iphone",
18 | "filename" : "Icon-App-29x29@1x.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "29x29",
23 | "idiom" : "iphone",
24 | "filename" : "Icon-App-29x29@2x.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "29x29",
29 | "idiom" : "iphone",
30 | "filename" : "Icon-App-29x29@3x.png",
31 | "scale" : "3x"
32 | },
33 | {
34 | "size" : "40x40",
35 | "idiom" : "iphone",
36 | "filename" : "Icon-App-40x40@2x.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "40x40",
41 | "idiom" : "iphone",
42 | "filename" : "Icon-App-40x40@3x.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "60x60",
47 | "idiom" : "iphone",
48 | "filename" : "Icon-App-60x60@2x.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "60x60",
53 | "idiom" : "iphone",
54 | "filename" : "Icon-App-60x60@3x.png",
55 | "scale" : "3x"
56 | },
57 | {
58 | "size" : "20x20",
59 | "idiom" : "ipad",
60 | "filename" : "Icon-App-20x20@1x.png",
61 | "scale" : "1x"
62 | },
63 | {
64 | "size" : "20x20",
65 | "idiom" : "ipad",
66 | "filename" : "Icon-App-20x20@2x.png",
67 | "scale" : "2x"
68 | },
69 | {
70 | "size" : "29x29",
71 | "idiom" : "ipad",
72 | "filename" : "Icon-App-29x29@1x.png",
73 | "scale" : "1x"
74 | },
75 | {
76 | "size" : "29x29",
77 | "idiom" : "ipad",
78 | "filename" : "Icon-App-29x29@2x.png",
79 | "scale" : "2x"
80 | },
81 | {
82 | "size" : "40x40",
83 | "idiom" : "ipad",
84 | "filename" : "Icon-App-40x40@1x.png",
85 | "scale" : "1x"
86 | },
87 | {
88 | "size" : "40x40",
89 | "idiom" : "ipad",
90 | "filename" : "Icon-App-40x40@2x.png",
91 | "scale" : "2x"
92 | },
93 | {
94 | "size" : "76x76",
95 | "idiom" : "ipad",
96 | "filename" : "Icon-App-76x76@1x.png",
97 | "scale" : "1x"
98 | },
99 | {
100 | "size" : "76x76",
101 | "idiom" : "ipad",
102 | "filename" : "Icon-App-76x76@2x.png",
103 | "scale" : "2x"
104 | },
105 | {
106 | "size" : "83.5x83.5",
107 | "idiom" : "ipad",
108 | "filename" : "Icon-App-83.5x83.5@2x.png",
109 | "scale" : "2x"
110 | },
111 | {
112 | "size" : "1024x1024",
113 | "idiom" : "ios-marketing",
114 | "filename" : "Icon-App-1024x1024@1x.png",
115 | "scale" : "1x"
116 | }
117 | ],
118 | "info" : {
119 | "version" : 1,
120 | "author" : "xcode"
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "LaunchImage.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "LaunchImage@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "LaunchImage@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md:
--------------------------------------------------------------------------------
1 | # Launch Screen Assets
2 |
3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory.
4 |
5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
--------------------------------------------------------------------------------
/example/ios/Runner/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/example/ios/Runner/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/example/ios/Runner/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | appspector_plugin_example
15 | CFBundlePackageType
16 | APPL
17 |
18 | CFBundleShortVersionString
19 | $(FLUTTER_BUILD_NAME)
20 |
21 | CFBundleSignature
22 | AppSpector
23 |
24 | CFBundleVersion
25 | $(FLUTTER_BUILD_NUMBER)
26 |
27 | LSRequiresIPhoneOS
28 |
29 | UILaunchStoryboardName
30 | LaunchScreen
31 | UIMainStoryboardFile
32 | Main
33 | UISupportedInterfaceOrientations
34 |
35 | UIInterfaceOrientationPortrait
36 | UIInterfaceOrientationLandscapeLeft
37 | UIInterfaceOrientationLandscapeRight
38 |
39 | UISupportedInterfaceOrientations~ipad
40 |
41 | UIInterfaceOrientationPortrait
42 | UIInterfaceOrientationPortraitUpsideDown
43 | UIInterfaceOrientationLandscapeLeft
44 | UIInterfaceOrientationLandscapeRight
45 |
46 | UIViewControllerBasedStatusBarAppearance
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/example/ios/Runner/main.m:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 | #import "AppDelegate.h"
4 |
5 | int main(int argc, char* argv[]) {
6 | @autoreleasepool {
7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/example/lib/app_drawer.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'routes.dart';
3 |
4 | class SampleAppDrawer extends StatelessWidget {
5 | @override
6 | Widget build(BuildContext context) {
7 | return Drawer(
8 | child: ListView(
9 | padding: EdgeInsets.zero,
10 | children: [
11 | DrawerHeader(
12 | child: Text(""),
13 | decoration: BoxDecoration(color: Theme.of(context).primaryColor),
14 | ),
15 | ListTile(
16 | title: Text("Main Screen"),
17 | onTap: () => Navigator.pushReplacementNamed(context, Routes.MainPage),
18 | ),
19 | ListTile(
20 | title: Text("SQLite Monitor"),
21 | onTap: () =>
22 | Navigator.pushReplacementNamed(context, Routes.SQLiteMonitorPage),
23 | ),
24 | ListTile(
25 | title: Text("Http Monitor"),
26 | onTap: () =>
27 | Navigator.pushReplacementNamed(context, Routes.HttpMonitorPage),
28 | ),
29 | ListTile(
30 | title: Text("Edit Metadata"),
31 | onTap: () =>
32 | Navigator.pushReplacementNamed(context, Routes.MetadataPage),
33 | ),
34 | ],
35 | ));
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/example/lib/color.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | const _primaryColor = 0xFF424250;
4 | const _accentColor = 0xFFF6637D;
5 |
6 | const MaterialAccentColor appSpectorAccent = MaterialAccentColor(
7 | _accentColor,
8 | {},
9 | );
10 |
11 | const appSpectorPrimary = MaterialColor(_primaryColor, {
12 | 50: Color(_primaryColor),
13 | 100: Color(_primaryColor),
14 | 200: Color(_primaryColor),
15 | 300: Color(_primaryColor),
16 | 400: Color(_primaryColor),
17 | 500: Color(_primaryColor),
18 | 600: Color(_primaryColor),
19 | 700: Color(_primaryColor),
20 | 800: Color(_primaryColor),
21 | 900: Color(_primaryColor),
22 | });
23 |
--------------------------------------------------------------------------------
/example/lib/http/app_http_client.dart:
--------------------------------------------------------------------------------
1 |
2 | import 'dart:convert';
3 | import 'dart:io' show HttpClient, HttpClientRequest, HttpHeaders;
4 |
5 | import 'package:dio/dio.dart';
6 | import 'package:flutter/services.dart' show rootBundle;
7 | import 'package:http/http.dart' as http;
8 |
9 | abstract class AppHttpClient {
10 | Future executeGet(String url);
11 |
12 | Future executeGetImage();
13 |
14 | Future executePost(String url);
15 |
16 | Future executeDelete(String url);
17 |
18 | Future executePut(String url);
19 |
20 | Future executePatch(String url);
21 |
22 | Future executeHead(String url);
23 |
24 | Future executeTrace(String url);
25 |
26 | Future executeOptions(String url);
27 | }
28 |
29 | class FlutterHttpClient extends AppHttpClient {
30 | @override
31 | Future executeDelete(String url) {
32 | return http.delete(Uri.parse(url)).then((response) {
33 | return response.statusCode;
34 | });
35 | }
36 |
37 | @override
38 | Future executeGet(String url) {
39 | return http.get(Uri.parse(url)).then((response) {
40 | return response.statusCode;
41 | });
42 | }
43 |
44 | @override
45 | Future executeGetImage() {
46 | return http
47 | .get(
48 | Uri.parse("https://raw.githubusercontent.com/appspector/android-sdk/master/images/github-cover.png"))
49 | .then((response) {
50 | return response.statusCode;
51 | });
52 | }
53 |
54 | @override
55 | Future executeHead(String url) {
56 | return http.head(Uri.parse(url)).then((response) {
57 | return response.statusCode;
58 | });
59 | }
60 |
61 | @override
62 | Future executeOptions(String url) {
63 | throw Exception("OPTION request is not supported by current client");
64 | }
65 |
66 | @override
67 | Future executePatch(String url) async {
68 | final body = await rootBundle.loadString("assets/patch.json");
69 | return http.patch(Uri.parse(url), body: body).then((response) {
70 | return response.statusCode;
71 | });
72 | }
73 |
74 | @override
75 | Future executePost(String url) async {
76 | // final body = await rootBundle.loadString("assets/post.json");
77 | final data = """{
78 | "eventId": 1,
79 | "companyId": 201,
80 | "jobRoleId": "3",
81 | "expressBadge": false,
82 | "fcmToken": "svsdfvdsvf"
83 | }""";
84 | final headers = {"Content-Type": "application/json; charset=utf-8"};
85 | return http.post(Uri.parse(url), headers: headers, body: data).then((response) {
86 | return response.statusCode;
87 | });
88 | }
89 |
90 | @override
91 | Future executePut(String url) async {
92 | final body = await rootBundle.loadString("assets/put.json");
93 | return http.put(Uri.parse(url), body: body).then((response) {
94 | return response.statusCode;
95 | });
96 | }
97 |
98 | @override
99 | Future executeTrace(String url) {
100 | throw Exception("TRACE request is not supported by current client");
101 | }
102 | }
103 |
104 | class IOHttpClient extends AppHttpClient {
105 | final client = HttpClient();
106 |
107 | @override
108 | Future executeDelete(String url) {
109 | return _executeRequest(client.deleteUrl(Uri.parse(url)));
110 | }
111 |
112 | @override
113 | Future executeGet(String url) {
114 | return _executeRequest(client.getUrl(Uri.parse(url)));
115 | }
116 |
117 | @override
118 | Future executeGetImage() {
119 | return _executeRequest(client.getUrl(Uri.parse(
120 | "https://raw.githubusercontent.com/appspector/android-sdk/master/images/github-cover.png")));
121 | }
122 |
123 | @override
124 | Future executeHead(String url) {
125 | return _executeRequest(client.headUrl(Uri.parse(url)));
126 | }
127 |
128 | @override
129 | Future executeOptions(String url) {
130 | return _executeRequest(client.openUrl("option", Uri.parse(url)));
131 | }
132 |
133 | @override
134 | Future executePatch(String url) {
135 | return _executeRequestWithBody(
136 | client.patchUrl(Uri.parse(url)), "assets/patch.json");
137 | }
138 |
139 | @override
140 | Future executePost(String url) {
141 | return _executeRequestWithBody(
142 | client.postUrl(Uri.parse(url)), "assets/post.json");
143 | }
144 |
145 | @override
146 | Future executePut(String url) {
147 | return _executeRequestWithBody(
148 | client.putUrl(Uri.parse(url)), "assets/put.json");
149 | }
150 |
151 | @override
152 | Future executeTrace(String url) {
153 | return _executeRequest(client.openUrl("trace", Uri.parse(url)));
154 | }
155 |
156 | Future _executeRequest(Future requestFuture) {
157 | return requestFuture.then((request) {
158 | return request.close();
159 | }).then((response) {
160 | Utf8Decoder().bind(response).listen((data) {
161 | print("Client IO has received: $data");
162 | });
163 | return response.statusCode;
164 | });
165 | }
166 |
167 | Future _executeRequestWithBody(
168 | Future requestFuture, String bodyAssetName) async {
169 | final body = await rootBundle.load(bodyAssetName);
170 | return requestFuture.then((request) {
171 | request.headers.add(HttpHeaders.contentTypeHeader, "application/json");
172 | request.add(body.buffer.asUint8List());
173 | return request.close();
174 | }).then((response) {
175 | Utf8Decoder().bind(response).listen((data) {
176 | print("Client IO has received: $data");
177 | });
178 | return response.statusCode;
179 | });
180 | }
181 | }
182 |
183 | class DioHttpClient extends AppHttpClient {
184 |
185 | final Dio dio = new Dio();
186 |
187 | @override
188 | Future executeDelete(String url) {
189 | return dio.delete(url).then((response) {
190 | return response.statusCode ?? 0;
191 | });
192 | }
193 |
194 | @override
195 | Future executeGet(String url) {
196 | return dio.get(url).then((response) {
197 | return response.statusCode ?? 0;
198 | });
199 | }
200 |
201 | @override
202 | Future executeGetImage() {
203 | return dio.get("https://raw.githubusercontent.com/appspector/android-sdk/master/images/github-cover.png").then((response) {
204 | return response.statusCode ?? 0;
205 | });
206 | }
207 |
208 | @override
209 | Future executeHead(String url) {
210 | return dio.head(url).then((response) {
211 | return response.statusCode ?? 0;
212 | });
213 | }
214 |
215 | @override
216 | Future executeOptions(String url) {
217 | throw Exception("OPTION request is not supported by current client");
218 | }
219 |
220 | @override
221 | Future executePatch(String url) {
222 | return dio.patch(url).then((response) {
223 | return response.statusCode ?? 0;
224 | });
225 | }
226 |
227 | @override
228 | Future executePost(String url) {
229 | final data = {
230 | 'eventId': 1,
231 | 'companyId': 201,
232 | 'jobRoleId': '3',
233 | 'expressBadge': false,
234 | 'fcmToken': 'svsdfvdsvf'
235 | };
236 | return dio.post(url, data: data).then((response) {
237 | return response.statusCode ?? 0;
238 | });
239 | }
240 |
241 | @override
242 | Future executePut(String url) {
243 | return dio.put(url).then((response) {
244 | return response.statusCode ?? 0;
245 | });
246 | }
247 |
248 | @override
249 | Future executeTrace(String url) {
250 | throw Exception("TRACE request is not supported by current client");
251 | }
252 | }
253 |
--------------------------------------------------------------------------------
/example/lib/http/http_request_item.dart:
--------------------------------------------------------------------------------
1 | import 'app_http_client.dart';
2 |
3 | class HttpRequestItems {
4 | final String title;
5 | final Future Function(AppHttpClient, String) action;
6 |
7 | HttpRequestItems(this.title, this.action);
8 | }
9 |
--------------------------------------------------------------------------------
/example/lib/http_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:dio/dio.dart';
2 | import 'package:flutter/material.dart';
3 |
4 | import 'app_drawer.dart';
5 | import 'http/app_http_client.dart';
6 | import 'http/http_request_item.dart';
7 |
8 | class HttpMonitorPage extends StatefulWidget {
9 | @override
10 | State createState() => HttpMonitorPageState();
11 | }
12 |
13 | class HttpMonitorPageState extends State {
14 | static const _CLIENT_HTTP_LIB = 0;
15 | static const _CLIENT_IO = 1;
16 | static const _CLIENT_DIO = 2;
17 |
18 | final _url = "https://google.com";
19 | final _flutterHttpClient = FlutterHttpClient();
20 | final _ioHttpClient = IOHttpClient();
21 | final _dioHttpClient = DioHttpClient();
22 |
23 | final List requestMethods = [
24 | new HttpRequestItems("GET Request", (httpClient, url) {
25 | return httpClient.executeGet(url);
26 | }),
27 | new HttpRequestItems("GET Image Request", (httpClient, _) {
28 | return httpClient.executeGetImage();
29 | }),
30 | new HttpRequestItems("POST Request", (httpClient, url) {
31 | return httpClient.executePost(url);
32 | }),
33 | new HttpRequestItems("DELETE Request", (httpClient, url) {
34 | return httpClient.executeDelete(url);
35 | }),
36 | new HttpRequestItems("PUT Request", (httpClient, url) {
37 | return httpClient.executePut(url);
38 | }),
39 | new HttpRequestItems("PATCH Request", (httpClient, url) {
40 | return httpClient.executePatch(url);
41 | }),
42 | new HttpRequestItems("HEAD Request", (httpClient, url) {
43 | return httpClient.executeHead(url);
44 | }),
45 | new HttpRequestItems("TRACE Request", (httpClient, url) {
46 | return httpClient.executeTrace(url);
47 | }),
48 | new HttpRequestItems("OPTIONS Request", (httpClient, url) {
49 | return httpClient.executeOptions(url);
50 | })
51 | ];
52 |
53 | int _selectedClient = _CLIENT_HTTP_LIB;
54 | int? _statusCode;
55 | String? _error;
56 | int? _requestDuration;
57 |
58 | @override
59 | Widget build(BuildContext context) {
60 | return Scaffold(
61 | appBar: AppBar(
62 | title: Text("HTTP Monitor"),
63 | ),
64 | drawer: SampleAppDrawer(),
65 | body: SingleChildScrollView(
66 | child: Padding(
67 | padding: EdgeInsets.symmetric(horizontal: 15.0),
68 | child: Column(children: [
69 | const Text(
70 | "Choose HTTP client:",
71 | style: TextStyle(
72 | fontWeight: FontWeight.bold,
73 | fontSize: 20.0,
74 | height: 2,
75 | ),
76 | ),
77 | Row(children: [
78 | Expanded(
79 | child: RadioListTile(
80 | value: _CLIENT_HTTP_LIB,
81 | groupValue: _selectedClient,
82 | title: const Text("HTTP Lib"),
83 | onChanged: _onClientSelectChanged)),
84 | Expanded(
85 | child: RadioListTile(
86 | value: _CLIENT_IO,
87 | title: const Text("IO"),
88 | groupValue: _selectedClient,
89 | onChanged: _onClientSelectChanged)),
90 | Expanded(
91 | child: RadioListTile(
92 | value: _CLIENT_DIO,
93 | title: const Text("DIO"),
94 | groupValue: _selectedClient,
95 | onChanged: _onClientSelectChanged))
96 | ]),
97 | Container(
98 | margin: EdgeInsets.only(top: 24.0),
99 | child:
100 | Text.rich(TextSpan(children: _createResultedText()))),
101 | GridView.count(
102 | shrinkWrap: true,
103 | crossAxisCount: 2,
104 | primary: false,
105 | childAspectRatio: 3,
106 | children: createRequestMethodWidgetList(),
107 | )
108 | ]))),
109 | );
110 | }
111 |
112 | List createRequestMethodWidgetList() {
113 | return requestMethods.map((item) {
114 | return Container(
115 | alignment: Alignment.center,
116 | // margin: EdgeInsets.only(top: 24.0),
117 | child: ElevatedButton(
118 | child: Text(item.title),
119 | onPressed: () {
120 | Stopwatch stopwatch = Stopwatch()..start();
121 | item.action(_provideClient(), _url).then((responseCode) {
122 | _onHttpResponse(responseCode, stopwatch.elapsedMilliseconds);
123 | }).onError((error, stackTrace) => _onHttpError(
124 | error as Exception, stopwatch.elapsedMilliseconds));
125 | }));
126 | }).toList();
127 | }
128 |
129 | AppHttpClient _provideClient() {
130 | switch (_selectedClient) {
131 | case _CLIENT_HTTP_LIB:
132 | return _flutterHttpClient;
133 | case _CLIENT_IO:
134 | return _ioHttpClient;
135 | case _CLIENT_DIO:
136 | return _dioHttpClient;
137 | }
138 | throw Exception("Unknown client id");
139 | }
140 |
141 | _onHttpResponse(int statusCode, int requestDuration) {
142 | setState(() {
143 | _statusCode = statusCode;
144 | _requestDuration = requestDuration;
145 | _error = null;
146 | });
147 | }
148 |
149 | _onHttpError(Exception e, int requestDuration) {
150 | setState(() {
151 | _statusCode = null;
152 | _requestDuration = requestDuration;
153 | _error = e is DioException
154 | ? "${e.message} (${requestDuration}ms)"
155 | : e.toString();
156 | });
157 | }
158 |
159 | List _createResultedText() {
160 | if (_error != null) {
161 | return [TextSpan(text: _error)];
162 | }
163 | if (_statusCode != null) {
164 | List lines = [];
165 | lines
166 | .add(TextSpan(text: "Request finished with code: $_statusCode \n\n"));
167 | lines.add(TextSpan(text: "$_requestDuration ms"));
168 | return lines;
169 | }
170 | return [TextSpan(text: "Click any button")];
171 | }
172 |
173 | _onClientSelectChanged(int? newValue) {
174 | setState(() {
175 | _selectedClient = newValue ?? _CLIENT_HTTP_LIB;
176 | });
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/example/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'package:appspector/appspector.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:flutter_appspector_example/metadata_page.dart';
4 | import 'package:flutter_appspector_example/utils.dart';
5 | import 'package:logging/logging.dart' as logger;
6 |
7 | import 'color.dart';
8 | import 'http_page.dart';
9 | import 'main_page.dart';
10 | import 'routes.dart';
11 | import 'sqlite_page.dart';
12 |
13 | void main() {
14 | WidgetsFlutterBinding.ensureInitialized();
15 | var globalSessionUrlObserver = DataObservable();
16 | runAppSpector(globalSessionUrlObserver);
17 | runApp(MyApp(globalSessionUrlObserver));
18 |
19 | logger.Logger.root.level = logger.Level.ALL;
20 | logger.Logger.root.onRecord.listen((logger.LogRecord rec) {
21 | Logger.log(
22 | LogLevel.DEBUG, rec.loggerName, "(${rec.level.name}) ${rec.message}");
23 | print('${rec.level.name}: ${rec.time}: ${rec.message}');
24 | });
25 | }
26 |
27 | void runAppSpector(DataObservable sessionObserver) {
28 | final config = Config()
29 | ..iosApiKey = "YjU1NDVkZGEtN2U3Zi00MDM3LTk5ZGQtNzdkNzY3YmUzZGY2"
30 | ..androidApiKey = "MWM1YTZlOTItMmU4OS00NGI2LWJiNGQtYjdhZDljNjBhYjcz"
31 | ..monitors = [
32 | Monitors.http,
33 | Monitors.logs,
34 | Monitors.fileSystem,
35 | Monitors.screenshot,
36 | Monitors.environment,
37 | Monitors.location,
38 | Monitors.performance,
39 | Monitors.sqLite,
40 | Monitors.sharedPreferences,
41 | Monitors.analytics,
42 | Monitors.notification,
43 | Monitors.userDefaults,
44 | Monitors.coreData
45 | ];
46 |
47 | AppSpectorPlugin.run(config);
48 | AppSpectorPlugin.shared().sessionUrlListener =
49 | (sessionUrl) => {sessionObserver.setValue(sessionUrl)};
50 | }
51 |
52 | class MyApp extends StatelessWidget {
53 | final DataObservable _sessionUrlObserver;
54 |
55 | MyApp(this._sessionUrlObserver);
56 |
57 | @override
58 | Widget build(BuildContext context) {
59 | return MaterialApp(
60 | title: 'Flutter Demo',
61 | theme: ThemeData(
62 | // This is the theme of your application.
63 | //
64 | // Try running your application with "flutter run". You'll see the
65 | // application has a blue toolbar. Then, without quitting the app, try
66 | // changing the primarySwatch below to Colors.green and then invoke
67 | // "hot reload" (press "r" in the console where you ran "flutter run",
68 | // or press Run > Flutter Hot Reload in IntelliJ). Notice that the
69 | // counter didn't reset back to zero; the application is not restarted.
70 | primarySwatch: appSpectorPrimary,
71 | highlightColor: appSpectorAccent),
72 | home: MyHomePage(_sessionUrlObserver, title: 'Flutter Demo Home Page'),
73 | routes: {
74 | Routes.SQLiteMonitorPage: (BuildContext context) => SQLitePage(),
75 | Routes.HttpMonitorPage: (BuildContext context) => HttpMonitorPage(),
76 | Routes.MetadataPage: (BuildContext context) => MetadataPage(),
77 | },
78 | );
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/example/lib/main_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:appspector/appspector.dart' show Logger, AppSpectorPlugin;
2 | import 'package:flutter/material.dart';
3 | import 'package:logging/logging.dart' as logger;
4 |
5 | import 'app_drawer.dart';
6 | import 'utils.dart';
7 |
8 | class MyHomePage extends StatefulWidget {
9 | final DataObservable _sessionUrlObserver;
10 |
11 | MyHomePage(this._sessionUrlObserver, {Key? key, required this.title})
12 | : super(key: key);
13 |
14 | // This widget is the home page of your application. It is stateful, meaning
15 | // that it has a State object (defined below) that contains fields that affect
16 | // how it looks.
17 |
18 | // This class is the configuration for the state. It holds the values (in this
19 | // case the title) provided by the parent (in this case the App widget) and
20 | // used by the build method of the State. Fields in a Widget subclass are
21 | // always marked "final".
22 |
23 | final String title;
24 |
25 | @override
26 | _MyHomePageState createState() => _MyHomePageState(_sessionUrlObserver);
27 | }
28 |
29 | class _MyHomePageState extends State {
30 | final logger.Logger log = new logger.Logger('MyHomePageState');
31 |
32 | int _counter = 0;
33 |
34 | bool isSdkRunning = true;
35 | late String _sessionUrl;
36 |
37 | _MyHomePageState(DataObservable sessionUrlObserver) {
38 | sessionUrlObserver.observer = (sessionUrl) => {
39 | setState(() {
40 | _sessionUrl = sessionUrl;
41 | })
42 | };
43 | _sessionUrl = sessionUrlObserver.getValue() ?? "Unknown";
44 | }
45 |
46 | void _incrementCounter() {
47 | setState(() {
48 | // This call to setState tells the Flutter framework that something has
49 | // changed in this State, which causes it to rerun the build method below
50 | // so that the display can reflect the updated values. If we changed
51 | // _counter without calling setState(), then the build method would not be
52 | // called again, and so nothing would appear to happen.
53 | _counter++;
54 | });
55 |
56 | debugPrint("Button IncrementCounter was clicked");
57 | log.fine("Button IncrementCounter was clicked");
58 | }
59 |
60 | void _clickLogErrorButton() {
61 | try {
62 | _throwError();
63 | } catch (error, stackTrace) {
64 | log.finer("TAG _clickLogErrorButton log.finer", error, stackTrace);
65 | Logger.d("TAG", "_clickLogErrorButton Logger.d", error, stackTrace);
66 | }
67 | }
68 |
69 | void _throwError() {
70 | throw Error();
71 | }
72 |
73 | void _stopSdk() async {
74 | await AppSpectorPlugin.shared().stop();
75 | setState(() {
76 | isSdkRunning = false;
77 | });
78 | }
79 |
80 | void _startSdk() async {
81 | await AppSpectorPlugin.shared().start();
82 | setState(() {
83 | isSdkRunning = true;
84 | });
85 | }
86 |
87 | void _checkSdkState() async {
88 | var isStarted = await AppSpectorPlugin.shared().isStarted();
89 |
90 | setState(() {
91 | isSdkRunning = isStarted;
92 | });
93 |
94 | ScaffoldMessenger.of(context).showSnackBar(SnackBar(
95 | content: Text(isStarted ? "SDK is started" : "SDK is stopped"),
96 | ));
97 | }
98 |
99 | @override
100 | Widget build(BuildContext context) {
101 | // This method is rerun every time setState is called, for instance as done
102 | // by the _incrementCounter method above.
103 | //
104 | // The Flutter framework has been optimized to make rerunning build methods
105 | // fast, so that you can just rebuild anything that needs updating rather
106 | // than having to individually change instances of widgets.
107 | return Scaffold(
108 | drawer: SampleAppDrawer(),
109 | appBar: AppBar(
110 | // Here we take the value from the MyHomePage object that was created by
111 | // the App.build method, and use it to set our appbar title.
112 | title: Text(widget.title),
113 | ),
114 | body: Center(
115 | // Center is a layout widget. It takes a single child and positions it
116 | // in the middle of the parent.
117 | child: Column(
118 | // Column is also layout widget. It takes a list of children and
119 | // arranges them vertically. By default, it sizes itself to fit its
120 | // children horizontally, and tries to be as tall as its parent.
121 | //
122 | // Invoke "debug paint" (press "p" in the console where you ran
123 | // "flutter run", or select "Toggle Debug Paint" from the Flutter tool
124 | // window in IntelliJ) to see the wireframe for each widget.
125 | //
126 | // Column has various properties to control how it sizes itself and
127 | // how it positions its children. Here we use mainAxisAlignment to
128 | // center the children vertically; the main axis here is the vertical
129 | // axis because Columns are vertical (the cross axis would be
130 | // horizontal).
131 | mainAxisAlignment: MainAxisAlignment.start,
132 | children: [
133 | Text(_sessionUrl),
134 | const SizedBox(height: 124),
135 | _createSwitchSdkStateButton(),
136 | const SizedBox(height: 124),
137 | Text(
138 | 'You have pushed the button this many times:',
139 | ),
140 | Text(
141 | '$_counter',
142 | style: Theme.of(context).textTheme.headline3,
143 | ),
144 | TextButton(
145 | child: Text('Click here to log error'),
146 | onPressed: _clickLogErrorButton,
147 | ),
148 | ElevatedButton(
149 | child: Text('Check SDK state'),
150 | onPressed: _checkSdkState,
151 | )
152 | ],
153 | ),
154 | ),
155 | floatingActionButton: FloatingActionButton(
156 | onPressed: _incrementCounter,
157 | tooltip: 'Increment',
158 | child: Icon(Icons.add),
159 | ), // This trailing comma makes auto-formatting nicer for build methods.
160 | );
161 | }
162 |
163 | Widget _createSwitchSdkStateButton() {
164 | if (isSdkRunning) {
165 | return _buildSwitchSdkStateButton("Pause", Colors.redAccent, _stopSdk);
166 | } else {
167 | return _buildSwitchSdkStateButton("Resume", Colors.green, _startSdk);
168 | }
169 | }
170 |
171 | Widget _buildSwitchSdkStateButton(
172 | String text, Color color, Function() onPressed) {
173 | return ButtonTheme(
174 | minWidth: 120,
175 | height: 120,
176 | child: ElevatedButton(
177 | child: Text(
178 | text,
179 | style: TextStyle(
180 | color: Colors.white, fontWeight: FontWeight.bold, fontSize: 24),
181 | ),
182 | style: ElevatedButton.styleFrom(
183 | primary: color,
184 | shape: RoundedRectangleBorder(
185 | borderRadius: new BorderRadius.circular(60),
186 | side: BorderSide(color: Colors.transparent)),
187 | ),
188 | onPressed: onPressed,
189 | ));
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/example/lib/metadata_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:appspector/appspector.dart';
2 | import 'package:flutter/material.dart';
3 |
4 | import 'app_drawer.dart';
5 |
6 | class MetadataPage extends StatefulWidget {
7 | @override
8 | State createState() => MetadataPageState();
9 | }
10 |
11 | class MetadataPageState extends State {
12 |
13 | var _deviceNameController = TextEditingController();
14 |
15 |
16 | @override
17 | Widget build(BuildContext context) {
18 | return Scaffold(
19 | appBar: AppBar(
20 | title: Text("Edit Metadata"),
21 | ),
22 | drawer: SampleAppDrawer(),
23 | body: Padding(
24 | padding: EdgeInsets.symmetric(horizontal: 15.0),
25 | child: Column(
26 | children: [
27 | TextField(
28 | controller: _deviceNameController,
29 | decoration: InputDecoration(labelText: "Device Name"),
30 | keyboardType: TextInputType.text,
31 | maxLength: 50,
32 | onEditingComplete: _deviceNameChanged,
33 | autofocus: true,
34 | )
35 | ]
36 | )
37 | )
38 | );
39 | }
40 |
41 | void _deviceNameChanged() {
42 | var newDeviceName = _deviceNameController.text;
43 | if (newDeviceName.isNotEmpty) {
44 | AppSpectorPlugin.shared().setMetadataValue(MetadataKeys.deviceName, newDeviceName);
45 | } else {
46 | AppSpectorPlugin.shared().removeMetadataValue(MetadataKeys.deviceName);
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/example/lib/routes.dart:
--------------------------------------------------------------------------------
1 | class Routes {
2 | static const MainPage = "/";
3 | static const SQLiteMonitorPage = "/sqlite_monitor_page";
4 | static const HttpMonitorPage = "/http_monitor_page";
5 | static const MetadataPage = "/metadata_page";
6 | }
7 |
8 |
--------------------------------------------------------------------------------
/example/lib/sqlite/record.dart:
--------------------------------------------------------------------------------
1 | class Record {
2 | final String name;
3 | final String address;
4 | final String phone;
5 |
6 | Record(this.name, this.address, this.phone);
7 |
8 | @override
9 | String toString() {
10 | return 'Record({"name": "$name", "address": "$address", "phone": "$phone"})';
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/example/lib/sqlite/storage.dart:
--------------------------------------------------------------------------------
1 |
2 | import 'dart:async';
3 | import 'package:flutter/foundation.dart';
4 | import 'package:sqflite/sqflite.dart';
5 |
6 | import 'record.dart';
7 |
8 | abstract class RecordStorage {
9 | void save(Record record);
10 | Future getRecordsCount();
11 | }
12 |
13 | class RecordStorageImpl implements RecordStorage {
14 | static Database? _db;
15 | static const _tableName = "records";
16 | static const _columnId = "id";
17 | static const _columnName = "name";
18 | static const _columnAddress = "address";
19 | static const _columnPhone = "phone";
20 |
21 | Future get db async {
22 | return _db ?? await initDb();
23 | }
24 |
25 | //Creating a database with name test.db in your directory
26 | initDb() async {
27 | var dbPath = await getDatabasesPath() + "/test.db";
28 | var db = await openDatabase(dbPath, version: 1, onCreate: _onCreate);
29 | _db = db;
30 | return db;
31 | }
32 |
33 | // Creating a table name Employee with fields
34 | void _onCreate(Database db, int version) async {
35 | // When creating the db, create the table
36 | await db.execute(
37 | "CREATE TABLE $_tableName ($_columnId INTEGER PRIMARY KEY, $_columnName TEXT, $_columnAddress TEXT, $_columnPhone TEXT)");
38 | debugPrint("Created tables");
39 | }
40 |
41 | @override
42 | void save(Record record) async {
43 | var dbClient = await db;
44 | await dbClient.transaction((txn) async {
45 | return txn.insert(_tableName, _recordToMap(record));
46 | });
47 | debugPrint("Record is written: $record");
48 | }
49 |
50 | _recordToMap(Record record) {
51 | final dict = Map();
52 | dict[_columnName] = record.name;
53 | dict[_columnAddress] = record.address;
54 | dict[_columnPhone] = record.phone;
55 | return dict;
56 | }
57 |
58 | @override
59 | Future getRecordsCount() async {
60 | var dbClient = await db;
61 | var records = await dbClient.query(_tableName);
62 | return records.length;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/example/lib/sqlite_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'app_drawer.dart';
3 | import 'sqlite/storage.dart';
4 | import 'sqlite/record.dart';
5 |
6 | class SQLitePage extends StatefulWidget {
7 | @override
8 | State createState() => SQLitePageState();
9 | }
10 |
11 | class SQLitePageState extends State {
12 | final _nameController = TextEditingController();
13 | final _addressController = TextEditingController();
14 | final _phoneController = TextEditingController();
15 | final _storage = RecordStorageImpl();
16 |
17 | var _nameIsValid = false;
18 | var _addressIsValid = false;
19 | var _phoneIsValid = false;
20 | var _saveButtonEnable = false;
21 |
22 | @override
23 | Widget build(BuildContext context) {
24 | return Scaffold(
25 | appBar: AppBar(
26 | title: Text("SQLite Monitor"),
27 | ),
28 | drawer: SampleAppDrawer(),
29 | body: Padding(
30 | padding: EdgeInsets.symmetric(horizontal: 15.0),
31 | child: Column(children: [
32 | TextField(
33 | controller: _nameController,
34 | decoration: InputDecoration(labelText: "Name"),
35 | keyboardType: TextInputType.text,
36 | onChanged: _onNameChanged,
37 | autofocus: true,
38 | ),
39 | TextField(
40 | controller: _addressController,
41 | decoration: InputDecoration(labelText: "Address"),
42 | keyboardType: TextInputType.text,
43 | onChanged: _onAddressChanged,
44 | ),
45 | TextField(
46 | controller: _phoneController,
47 | decoration: InputDecoration(labelText: "Phone"),
48 | keyboardType: TextInputType.phone,
49 | onChanged: _onPhoneChanged,
50 | ),
51 | Container(
52 | margin: EdgeInsets.only(top: 24.0),
53 | child: ElevatedButton(
54 | child: Text("Add"),
55 | onPressed: _saveButtonEnable ? _onSave : null)),
56 | ])));
57 | }
58 |
59 | _onNameChanged(String text) {
60 | _nameIsValid = text.isNotEmpty;
61 | _changeButtonState();
62 | }
63 |
64 | _onAddressChanged(String text) {
65 | _addressIsValid = text.isNotEmpty;
66 | _changeButtonState();
67 | }
68 |
69 | _onPhoneChanged(String text) {
70 | _phoneIsValid = text.isNotEmpty;
71 | _changeButtonState();
72 | }
73 |
74 | _changeButtonState() {
75 | setState(() {
76 | _saveButtonEnable = _nameIsValid && _addressIsValid && _phoneIsValid;
77 | });
78 | }
79 |
80 | _onSave() {
81 | final name = _nameController.text;
82 | final address = _addressController.text;
83 | final phone = _phoneController.text;
84 | _storage.save(Record(name, address, phone));
85 | setState(() {
86 | _nameController.clear();
87 | _addressController.clear();
88 | _phoneController.clear();
89 | });
90 | debugPrint("Saving record");
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/example/lib/utils.dart:
--------------------------------------------------------------------------------
1 | class DataObservable {
2 |
3 | T? _value;
4 |
5 | Function(T)? observer;
6 |
7 | void setValue(T value) {
8 | this._value = value;
9 | if (observer != null) {
10 | observer!(value);
11 | }
12 | }
13 |
14 | T? getValue() => _value;
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/example/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: flutter_appspector_example
2 | description: A sample app which demonstrates AppSpector initialization and features.
3 | publish_to: none
4 |
5 | environment:
6 | sdk: '>=2.14.0 <3.0.0'
7 |
8 | dependencies:
9 | flutter:
10 | sdk: flutter
11 | appspector:
12 | path: ../
13 | http: ^0.13.6
14 | sqflite: ^2.2.8+4
15 | logging: ^1.2.0
16 | dio: ^5.2.1+1
17 |
18 | flutter:
19 | uses-material-design: true
20 | assets:
21 | - assets/
22 |
--------------------------------------------------------------------------------
/github-cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/github-cover.png
--------------------------------------------------------------------------------
/ios/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .vagrant/
3 | .sconsign.dblite
4 | .svn/
5 |
6 | .DS_Store
7 | *.swp
8 | profile
9 |
10 | DerivedData/
11 | build/
12 | GeneratedPluginRegistrant.h
13 | GeneratedPluginRegistrant.m
14 |
15 | .generated/
16 |
17 | *.pbxuser
18 | *.mode1v3
19 | *.mode2v3
20 | *.perspectivev3
21 |
22 | !default.pbxuser
23 | !default.mode1v3
24 | !default.mode2v3
25 | !default.perspectivev3
26 |
27 | xcuserdata
28 |
29 | *.moved-aside
30 |
31 | *.pyc
32 | *sync/
33 | Icon?
34 | .tags*
35 |
36 | /Flutter/Generated.xcconfig
37 |
--------------------------------------------------------------------------------
/ios/Assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/ios/Assets/.gitkeep
--------------------------------------------------------------------------------
/ios/Classes/ASPluginCallValidator.h:
--------------------------------------------------------------------------------
1 | //
2 | // ASPluginCallValidator.h
3 | // appspector
4 | //
5 | // Created by Deszip on 10/04/2019.
6 | //
7 |
8 | #import
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | typedef NSString ASPluginMethodName;
13 | typedef NSString ASPluginMethodArgumentName;
14 | typedef NSDictionary ASPluginMethodArgumentsList;
15 |
16 | extern ASPluginMethodName * const kRunMethodName;
17 | extern ASPluginMethodName * const kStopMethodName;
18 | extern ASPluginMethodName * const kStartMethodName;
19 | extern ASPluginMethodName * const kIsStartedMethodName;
20 | extern ASPluginMethodName * const kSetMetadataMethodName;
21 | extern ASPluginMethodName * const kRemoveMetadataMethodName;
22 | extern ASPluginMethodName * const kHTTPRequestMethodName;
23 | extern ASPluginMethodName * const kHTTPResponseMethodName;
24 | extern ASPluginMethodName * const kLogEventMethodName;
25 |
26 | extern ASPluginMethodArgumentName * const kAPIKeyArgument;
27 | extern ASPluginMethodArgumentName * const kEnabledMonitorsArgument;
28 | extern ASPluginMethodArgumentName * const kMetadataArgument;
29 |
30 | extern ASPluginMethodArgumentName * const kUIDArgument;
31 | extern ASPluginMethodArgumentName * const kURLArgument;
32 | extern ASPluginMethodArgumentName * const kMethodArgument;
33 | extern ASPluginMethodArgumentName * const kBodyArgument;
34 | extern ASPluginMethodArgumentName * const kHeadersArgument;
35 | extern ASPluginMethodArgumentName * const kCodeArgument;
36 | extern ASPluginMethodArgumentName * const kTookMSArgument;
37 | extern ASPluginMethodArgumentName * const kLevelArgument;
38 | extern ASPluginMethodArgumentName * const kTagArgument;
39 | extern ASPluginMethodArgumentName * const kMessageArgument;
40 |
41 |
42 | @interface ASPluginCallValidator : NSObject
43 |
44 | - (BOOL)controlMethodSupported:(ASPluginMethodName *)methodName;
45 | - (BOOL)eventMethodSupported:(ASPluginMethodName *)methodName;
46 |
47 | - (BOOL)argumentsValid:(ASPluginMethodArgumentsList *)arguments
48 | call:(ASPluginMethodName *)methodName
49 | error:(__autoreleasing NSError *_Nonnull*_Nullable)error;
50 |
51 |
52 |
53 | @end
54 |
55 | NS_ASSUME_NONNULL_END
56 |
--------------------------------------------------------------------------------
/ios/Classes/ASPluginCallValidator.m:
--------------------------------------------------------------------------------
1 | //
2 | // ASPluginCallValidator.m
3 | // appspector
4 | //
5 | // Created by Deszip on 10/04/2019.
6 | //
7 |
8 | #import "ASPluginCallValidator.h"
9 |
10 | ASPluginMethodName * const kRunMethodName = @"run";
11 | ASPluginMethodName * const kStopMethodName = @"stop";
12 | ASPluginMethodName * const kStartMethodName = @"start";
13 | ASPluginMethodName * const kIsStartedMethodName = @"isStarted";
14 | ASPluginMethodName * const kSetMetadataMethodName = @"setMetadata";
15 | ASPluginMethodName * const kRemoveMetadataMethodName = @"removeMetadata";
16 | ASPluginMethodName * const kHTTPRequestMethodName = @"http-request";
17 | ASPluginMethodName * const kHTTPResponseMethodName = @"http-response";
18 | ASPluginMethodName * const kLogEventMethodName = @"log-event";
19 |
20 | ASPluginMethodArgumentName * const kAPIKeyArgument = @"apiKey";
21 | ASPluginMethodArgumentName * const kEnabledMonitorsArgument = @"enabledMonitors";
22 | ASPluginMethodArgumentName * const kMetadataArgument = @"metadata";
23 |
24 | ASPluginMethodArgumentName * const kUIDArgument = @"uid";
25 | ASPluginMethodArgumentName * const kURLArgument = @"url";
26 | ASPluginMethodArgumentName * const kMethodArgument = @"method";
27 | ASPluginMethodArgumentName * const kBodyArgument = @"body";
28 | ASPluginMethodArgumentName * const kHeadersArgument = @"headers";
29 | ASPluginMethodArgumentName * const kCodeArgument = @"code";
30 | ASPluginMethodArgumentName * const kTookMSArgument = @"tookMs";
31 | ASPluginMethodArgumentName * const kLevelArgument = @"level";
32 | ASPluginMethodArgumentName * const kTagArgument = @"tag";
33 | ASPluginMethodArgumentName * const kMessageArgument = @"message";
34 |
35 | @interface ASPluginCallValidator ()
36 |
37 | @property (strong, nonatomic) NSArray *controlMethodNames;
38 | @property (strong, nonatomic) NSArray *eventMethodNames;
39 | @property (strong, nonatomic) NSDictionary *> *methodParameters;
40 |
41 | @end
42 |
43 | @implementation ASPluginCallValidator
44 |
45 | - (instancetype)init {
46 | if (self = [super init]) {
47 | _controlMethodNames = @[kRunMethodName,
48 | kStopMethodName,
49 | kStartMethodName,
50 | kIsStartedMethodName,
51 | kSetMetadataMethodName,
52 | kRemoveMetadataMethodName];
53 | _eventMethodNames = @[kHTTPRequestMethodName, kHTTPResponseMethodName, kLogEventMethodName];
54 | _methodParameters = @{ kHTTPRequestMethodName : @[ @"uid",
55 | @"url",
56 | @"method",
57 | @"body",
58 | @"headers" ],
59 | kHTTPResponseMethodName : @[ @"uid",
60 | @"code",
61 | @"body",
62 | @"headers",
63 | @"tookMs" ],
64 | kLogEventMethodName : @[ @"level",
65 | @"tag",
66 | @"message" ],
67 | kRunMethodName : @[ @"apiKey",
68 | @"enabledMonitors",
69 | @"metadata"],
70 | kSetMetadataMethodName : @[@"key",
71 | @"value"],
72 | kRemoveMetadataMethodName : @[@"key"] };
73 | }
74 |
75 | return self;
76 | }
77 |
78 | #pragma mark - Validation API -
79 |
80 | - (BOOL)controlMethodSupported:(ASPluginMethodName *)methodName {
81 | return [self.controlMethodNames containsObject:methodName];
82 | }
83 |
84 | - (BOOL)eventMethodSupported:(ASPluginMethodName *)methodName {
85 | return [self.eventMethodNames containsObject:methodName];
86 | }
87 |
88 | - (BOOL)argumentsValid:(ASPluginMethodArgumentsList *)arguments
89 | call:(ASPluginMethodName *)methodName
90 | error:(NSError **)error {
91 | __block BOOL isValid = YES;
92 | __block NSError *strongError = nil;
93 | [self.methodParameters[methodName] enumerateObjectsUsingBlock:^(ASPluginMethodArgumentName *argName, NSUInteger idx, BOOL *stop) {
94 | if (![arguments.allKeys containsObject:argName]) {
95 | isValid = NO;
96 | strongError = [self errorForMissingArgument:argName inCall:methodName];
97 | *stop = YES;
98 | }
99 | }];
100 |
101 | *error = strongError;
102 |
103 | return isValid;
104 | }
105 |
106 | #pragma mark - Private API -
107 |
108 | - (NSError *)errorForMissingArgument:(ASPluginMethodArgumentName *)argName inCall:(ASPluginMethodName *)methodName {
109 | NSString *errorMessage = [NSString stringWithFormat:@"%@ call: '%@' argument is missing", methodName, argName];
110 | NSError *error = [NSError errorWithDomain:@"" code:0 userInfo:@{ NSLocalizedDescriptionKey : errorMessage }];
111 |
112 | return error;
113 | }
114 |
115 | @end
116 |
--------------------------------------------------------------------------------
/ios/Classes/ASPluginEventsHandler.h:
--------------------------------------------------------------------------------
1 | //
2 | // ASPluginEventsHandler.h
3 | // appspector
4 | //
5 | // Created by Deszip on 10/04/2019.
6 | //
7 |
8 | #import
9 |
10 | #import
11 | #import "ASPluginCallValidator.h"
12 |
13 | NS_ASSUME_NONNULL_BEGIN
14 |
15 | @interface ASPluginEventsHandler : NSObject
16 |
17 | - (instancetype)new NS_UNAVAILABLE;
18 | - (instancetype)init NS_UNAVAILABLE;
19 |
20 | - (instancetype)initWithCallValidator:(ASPluginCallValidator *)validator NS_DESIGNATED_INITIALIZER;
21 |
22 | @end
23 |
24 | NS_ASSUME_NONNULL_END
25 |
--------------------------------------------------------------------------------
/ios/Classes/ASPluginEventsHandler.m:
--------------------------------------------------------------------------------
1 | //
2 | // ASPluginEventsHandler.m
3 | // appspector
4 | //
5 | // Created by Deszip on 10/04/2019.
6 | //
7 |
8 | #import "ASPluginEventsHandler.h"
9 |
10 | #import
11 |
12 | @interface ASPluginEventsHandler ()
13 |
14 | @property (strong, nonatomic) ASPluginCallValidator *callValidator;
15 |
16 | @end
17 |
18 | @implementation ASPluginEventsHandler
19 |
20 | - (instancetype)initWithCallValidator:(ASPluginCallValidator *)validator {
21 | if (self = [super init]) {
22 | _callValidator = validator;
23 | }
24 |
25 | return self;
26 | }
27 |
28 | #pragma mark - FlutterPlugin -
29 |
30 | + (void)registerWithRegistrar:(nonnull NSObject *)registrar { }
31 |
32 | - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result {
33 | if ([self.callValidator eventMethodSupported:call.method]) {
34 | // Validate arguments
35 | NSError *validationError = nil;
36 | if (![self.callValidator argumentsValid:call.arguments
37 | call:call.method
38 | error:&validationError]) {
39 | result(validationError.localizedDescription);
40 | return;
41 | }
42 |
43 | // Handle call
44 | // HTTP monitor
45 | if ([call.method isEqualToString:kHTTPRequestMethodName]) {
46 | [self handleHTTPRequestCall:call.arguments result:result];
47 | }
48 |
49 | if ([call.method isEqualToString:kHTTPResponseMethodName]) {
50 | [self handleHTTPResponseCall:call.arguments result:result];
51 | }
52 |
53 | // Logs monitor
54 | if ([call.method isEqualToString:kLogEventMethodName]) {
55 | [self handleLogEventCall:call.arguments result:result];
56 | }
57 | } else {
58 | result(FlutterMethodNotImplemented);
59 | }
60 | }
61 |
62 | #pragma mark - Call handlers -
63 | #pragma mark - HTTP monitor
64 |
65 | - (void)handleHTTPRequestCall:(ASPluginMethodArgumentsList *)arguments result:(FlutterResult)result {
66 | // Build event payload
67 | NSDictionary *payload = @{ @"uuid" : arguments[@"uid"],
68 | @"url" : arguments[@"url"],
69 | @"method" : arguments[@"method"],
70 | @"body" : [self unwrapData:arguments[@"body"]],
71 | @"hasLargeBody" : @(NO),
72 | @"headers" : arguments[@"headers"] };
73 |
74 | // Send event
75 | ASExternalEvent *event = [[ASExternalEvent alloc] initWithMonitorID:AS_HTTP_MONITOR eventID:@"http-request" payload:payload];
76 | [AppSpector sendEvent:event];
77 |
78 | result(@"Ok");
79 | }
80 |
81 | - (void)handleHTTPResponseCall:(ASPluginMethodArgumentsList *)arguments result:(FlutterResult)result {
82 | // Build event payload
83 | NSDictionary *payload = @{ @"uuid" : arguments[@"uid"],
84 | @"statusCode" : arguments[@"code"],
85 | @"body" : [self unwrapData:arguments[@"body"]],
86 | @"hasLargeBody" : @(NO),
87 | @"headers" : arguments[@"headers"],
88 | @"responseDuration" : arguments[@"tookMs"],
89 | @"error" : @"" };
90 |
91 | // Send event
92 | ASExternalEvent *event = [[ASExternalEvent alloc] initWithMonitorID:AS_HTTP_MONITOR eventID:@"http-response" payload:payload];
93 | [AppSpector sendEvent:event];
94 |
95 | result(@"Ok");
96 | }
97 |
98 | #pragma mark - Logs monitor
99 |
100 | - (void)handleLogEventCall:(ASPluginMethodArgumentsList *)arguments result:(FlutterResult)result {
101 | // Build event payload
102 | NSDictionary *payload = @{ @"level" : arguments[@"level"],
103 | @"message" : arguments[@"message"] };
104 |
105 | // Send event
106 | ASExternalEvent *event = [[ASExternalEvent alloc] initWithMonitorID:AS_LOG_MONITOR eventID:@"log" payload:payload];
107 | [AppSpector sendEvent:event];
108 |
109 | result(@"Ok");
110 | }
111 |
112 | #pragma mark - Tools
113 |
114 | - (NSData *)unwrapData:(id)flutterData {
115 | NSData *unwrappedData = [NSData data];
116 | if ([flutterData isKindOfClass:[FlutterStandardTypedData class]]) {
117 | unwrappedData = [(FlutterStandardTypedData *)flutterData data];
118 | }
119 |
120 | return unwrappedData;
121 | }
122 |
123 | @end
124 |
--------------------------------------------------------------------------------
/ios/Classes/AppSpectorPlugin.h:
--------------------------------------------------------------------------------
1 | //
2 | // AppSpectorPlugin.h
3 | // appspector
4 | //
5 | // Created by Deszip on 10/04/2019.
6 | //
7 |
8 | #import
9 |
10 | #import "ASPluginCallValidator.h"
11 |
12 | NS_ASSUME_NONNULL_BEGIN
13 |
14 | @interface AppSpectorPlugin : NSObject
15 |
16 | - (instancetype)new NS_UNAVAILABLE;
17 | - (instancetype)init NS_UNAVAILABLE;
18 |
19 | - (instancetype)initWithCallValidator:(ASPluginCallValidator *)validator
20 | channel:(FlutterMethodChannel *)controlChannel NS_DESIGNATED_INITIALIZER;
21 |
22 | @end
23 |
24 | NS_ASSUME_NONNULL_END
25 |
--------------------------------------------------------------------------------
/ios/Classes/AppSpectorPlugin.m:
--------------------------------------------------------------------------------
1 | //
2 | // AppSpectorPlugin.m
3 | // appspector
4 | //
5 | // Created by Deszip on 10/04/2019.
6 | //
7 |
8 | #import "AppSpectorPlugin.h"
9 | #import
10 |
11 | #import "ASPluginEventsHandler.h"
12 |
13 | static NSString * const kControlChannelName = @"appspector_plugin";
14 | static NSString * const kEventChannelName = @"appspector_event_channel";
15 |
16 | static NSString * const kEnvCheckOptionKey = @"APPSPECTOR_CHECK_STORE_ENVIRONMENT";
17 |
18 | @interface AppSpectorPlugin ()
19 |
20 | @property (strong, nonatomic) ASPluginCallValidator *callValidator;
21 | @property (strong, nonatomic) ASPluginEventsHandler *eventsHandler;
22 | @property (strong, nonatomic) FlutterMethodChannel *controlChannel;
23 |
24 | @end
25 |
26 | @implementation AppSpectorPlugin
27 |
28 | + (instancetype)rootPluginWithChannel:(FlutterMethodChannel *)controlChannel {
29 | static AppSpectorPlugin *rootPlugin = nil;
30 | static dispatch_once_t onceToken;
31 | dispatch_once(&onceToken, ^{
32 | rootPlugin = [[[self class] alloc] initWithCallValidator:[ASPluginCallValidator new]
33 | channel:controlChannel];
34 | });
35 | return rootPlugin;
36 | }
37 |
38 | - (instancetype)initWithCallValidator:(ASPluginCallValidator *)validator
39 | channel:(FlutterMethodChannel *)controlChannel {
40 | if (self = [super init]) {
41 | _callValidator = validator;
42 | _eventsHandler = [[ASPluginEventsHandler alloc] initWithCallValidator:validator];
43 | _controlChannel = controlChannel;
44 | }
45 |
46 | return self;
47 | }
48 |
49 | + (void)registerWithRegistrar:(NSObject *)registrar {
50 | FlutterMethodChannel *controlChannel = [FlutterMethodChannel methodChannelWithName:kControlChannelName binaryMessenger:[registrar messenger]];
51 | FlutterMethodChannel *eventChannel = [FlutterMethodChannel methodChannelWithName:kEventChannelName binaryMessenger:[registrar messenger]];
52 |
53 | AppSpectorPlugin *plugin = [AppSpectorPlugin rootPluginWithChannel:controlChannel];
54 |
55 | [registrar addMethodCallDelegate:plugin channel:controlChannel];
56 | [registrar addMethodCallDelegate:plugin.eventsHandler channel:eventChannel];
57 | }
58 |
59 | #pragma mark - Call handlers -
60 |
61 | - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result {
62 | if ([self.callValidator controlMethodSupported:call.method]) {
63 | // Validate arguments
64 | NSError *validationError = nil;
65 | if (![self.callValidator argumentsValid:call.arguments
66 | call:call.method
67 | error:&validationError]) {
68 | result(validationError.localizedDescription);
69 | return;
70 | }
71 |
72 | // Handle calls
73 | if ([call.method isEqualToString:kRunMethodName]) {
74 | [self handleRunCall:call.arguments result:result];
75 | }
76 | if ([call.method isEqualToString:kStopMethodName]) {
77 | [self handleStopCallWithResult:result];
78 | }
79 | if ([call.method isEqualToString:kStartMethodName]) {
80 | [self handleStartCallWithResult:result];
81 | }
82 | if ([call.method isEqualToString:kIsStartedMethodName]) {
83 | [self handleIsStartedCallWithResult:result];
84 | }
85 | if ([call.method isEqualToString:kSetMetadataMethodName]) {
86 | [self handleSetMetadataCall:call.arguments result:result];
87 | }
88 | if ([call.method isEqualToString:kRemoveMetadataMethodName]) {
89 | [self handleRemoveMetadataCall:call.arguments result:result];
90 | }
91 | } else {
92 | result(FlutterMethodNotImplemented);
93 | }
94 | }
95 |
96 | - (void)handleRunCall:(ASPluginMethodArgumentsList *)arguments result:(FlutterResult)result {
97 | NSString *apiKey = arguments[@"apiKey"];
98 | NSSet *monitorIds = [self validateAndMapRawMonitorIds:arguments[@"enabledMonitors"]];
99 |
100 | AppSpectorConfig *config = [AppSpectorConfig configWithAPIKey:apiKey monitorIDs:monitorIds];
101 |
102 | // Handle special case when private SDK options are transferred via metadata
103 | if (arguments[@"metadata"] != [NSNull null] && [arguments[@"metadata"][kEnvCheckOptionKey] isKindOfClass:[NSString class]]) {
104 | NSString *checkOption = arguments[@"metadata"][kEnvCheckOptionKey];
105 | NSNumber *productionCheck = [checkOption isEqualToString:@"true"] ? @(NO) : @(YES);
106 | [config setValue:productionCheck forKey:@"disableProductionCheck"];
107 |
108 | // Drop flag to not include in session metadata
109 | NSMutableDictionary *mutableArgs = [arguments mutableCopy];
110 | [[arguments mutableCopy] removeObjectForKey:kEnvCheckOptionKey];
111 | arguments = [mutableArgs copy];
112 | }
113 |
114 | config.metadata = [self validateAndMapRawMeatdata:arguments[@"metadata"]];
115 |
116 | __weak __auto_type weakSelf = self;
117 | config.startCallback = ^(NSURL * _Nonnull sessionURL) {
118 | [weakSelf.controlChannel invokeMethod:@"onSessionUrl" arguments:sessionURL.absoluteString];
119 | };
120 |
121 | [AppSpector runWithConfig:config];
122 |
123 | result(@"Ok");
124 | }
125 |
126 | - (void)handleStopCallWithResult:(FlutterResult)result {
127 | [AppSpector stop];
128 | result(@"Ok");
129 | }
130 |
131 | - (void)handleStartCallWithResult:(FlutterResult)result {
132 | [AppSpector start];
133 | result(@"Ok");
134 | }
135 |
136 | - (void)handleIsStartedCallWithResult:(FlutterResult)result {
137 | BOOL isStarted = [AppSpector isRunning];
138 | result(@(isStarted));
139 | }
140 |
141 | - (void)handleSetMetadataCall:(ASPluginMethodArgumentsList *)arguments result:(FlutterResult)result {
142 | NSString *key = arguments[@"key"];
143 | NSString *value = arguments[@"value"];
144 |
145 | if (key != nil && (id)key != NSNull.null && value != nil && (id)value != NSNull.null) {
146 | ASMetadata *metadata = @{key : value};
147 | [AppSpector updateMetadata:metadata];
148 | }
149 |
150 | result(@"Ok");
151 | }
152 |
153 | - (void)handleRemoveMetadataCall:(ASPluginMethodArgumentsList *)arguments result:(FlutterResult)result {
154 | ASMetadata *emptyMetadata = @{};
155 | [AppSpector updateMetadata:emptyMetadata];
156 | result(@"Ok");
157 | }
158 |
159 | #pragma mark - Validators -
160 |
161 | - (ASMetadata *)validateAndMapRawMeatdata:(NSDictionary *)rawMetadata {
162 | if (rawMetadata == nil || (id)rawMetadata == NSNull.null) {
163 | return @{};
164 | }
165 |
166 | __block BOOL isValid = YES;
167 | [rawMetadata enumerateKeysAndObjectsWithOptions:NSEnumerationConcurrent
168 | usingBlock:^(id key, id object, BOOL *stop) {
169 | if (key == NSNull.null || object == NSNull.null) {
170 | isValid = NO;
171 | }
172 | }];
173 |
174 | if (isValid) {
175 | return rawMetadata;
176 | } else {
177 | NSString *metadataString = rawMetadata.description;
178 | NSString *message = @"It looks like AppSpector iOS plugin initialized with invalid metadata: \n %@ \n Please review AppSpectorPlugin initialization code. If the problem persists, please contact us at https://slack.appspector.com/";
179 | NSLog(message, metadataString);
180 | return @{};
181 | }
182 | }
183 |
184 | - (NSSet *)validateAndMapRawMonitorIds:(NSArray *)rawMonitorIds {
185 | NSSet *allMonitors = [NSSet setWithObjects:
186 | AS_SCREENSHOT_MONITOR,
187 | AS_SQLITE_MONITOR,
188 | AS_HTTP_MONITOR,
189 | AS_COREDATA_MONITOR,
190 | AS_PERFORMANCE_MONITOR,
191 | AS_LOG_MONITOR,
192 | AS_LOCATION_MONITOR,
193 | AS_ENVIRONMENT_MONITOR ,
194 | AS_DEFAULTS_MONITOR,
195 | AS_NOTIFICATION_MONITOR,
196 | AS_ANALYTICS_MONITOR,
197 | AS_COMMANDS_MONITOR,
198 | AS_FS_MONITOR,
199 | nil];
200 |
201 | NSMutableSet *selectedMonotirIds = [NSMutableSet new];
202 | NSMutableSet *invalidMonitorIds = [NSMutableSet new];
203 |
204 | for (NSString *monitorId in rawMonitorIds) {
205 | if ([allMonitors containsObject:monitorId]) {
206 | [selectedMonotirIds addObject:monitorId];
207 | } else {
208 | [invalidMonitorIds addObject:monitorId];
209 | }
210 | }
211 |
212 | if (invalidMonitorIds.count > 0) {
213 | NSString *monitors = [[invalidMonitorIds allObjects] componentsJoinedByString:@"\n - "];
214 | NSString *message = @"It looks like AppSpector iOS plugin initialized with invalid monitors: \n - %@ \n Please review AppSpectorPlugin initialization code. If the problem persists, please contact us at https://slack.appspector.com/";
215 | NSLog(message, monitors);
216 | }
217 |
218 | return selectedMonotirIds;
219 | }
220 |
221 | @end
222 |
--------------------------------------------------------------------------------
/ios/appspector.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 | s.name = 'appspector'
3 | s.version = '0.0.1'
4 | s.summary = 'AppSpector SDK'
5 | s.description = <<-DESC
6 | AppSpector SDK
7 | DESC
8 | s.homepage = 'https://appspector.com'
9 | s.license = { :file => '../LICENSE' }
10 | s.author = { 'AppSpector' => 'info@appspector.com' }
11 | s.source = { :path => '.' }
12 | s.source_files = 'Classes/**/*'
13 | s.public_header_files = 'Classes/**/*.h'
14 | s.dependency 'Flutter'
15 | s.dependency 'AppSpectorSDK'
16 |
17 | s.ios.deployment_target = '10.0'
18 | end
19 |
20 |
--------------------------------------------------------------------------------
/lib/appspector.dart:
--------------------------------------------------------------------------------
1 | library appspector;
2 |
3 | export 'src/appspector_plugin.dart';
4 | export 'src/monitors.dart';
5 | export 'src/http/http_overrides.dart';
6 | export 'src/log/logger.dart';
7 |
--------------------------------------------------------------------------------
/lib/src/appspector_plugin.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io' show Platform, HttpOverrides;
2 |
3 | import 'package:appspector/src/http/http_overrides.dart';
4 | import 'package:appspector/src/monitors.dart';
5 | import 'package:appspector/src/request_receiver.dart';
6 | import 'package:flutter/services.dart' show MethodCall, MethodChannel;
7 |
8 | /// This class needed to aggregate AppSpector properties and arguments.
9 | class Config {
10 | /// API_KEY of your iOS Application.
11 | ///
12 | /// Property is optional if your app don't support iOS.
13 | /// Your can find API_KEY on settings page of your Application
14 | ///
15 | /// If you don't specify it and try to launch the app on iOS
16 | /// SDK will throw ArgumentError
17 | String? iosApiKey;
18 |
19 | /// API_KEY of your Android Application.
20 | ///
21 | /// Property is optional if your app don't support Android.
22 | /// Your can find API_KEY on settings page of your Application
23 | ///
24 | /// If you don't specify it and try to launch the app on Android
25 | /// SDK will throw ArgumentError
26 | String? androidApiKey;
27 |
28 | /// Collection of metadata information
29 | ///
30 | /// Property is optional. It allows to attach some additional
31 | /// information to session. For example, you can specify device name by
32 | /// putting it with MetaDataKeys.deviceName key
33 | Map? metadata;
34 |
35 | /// List of monitor which will be enabled
36 | ///
37 | /// Property is optional. By default all available monitors will be enabled.
38 | /// E.g. to enable necessary monitors you need to provide list
39 | /// like [Monitors.http, Monitors.screenshot]
40 | List? monitors;
41 | }
42 |
43 | /// This is the main class for using AppSpector. AppSpector captures various
44 | /// types of data to assist in debugging, analyzing application state and
45 | /// understanding user behavior.
46 | ///
47 | ///
Here is an example of how AppSpector is used:
48 | ///
49 | /// void main() {
50 | /// runAppSpector();
51 | /// runApp(MyApp());
52 | /// }
53 | ///
54 | /// void runAppSpector() {
55 | /// var config = new Config();
56 | /// config.iosApiKey = "Your iOS API_KEY";
57 | /// config.androidApiKey = "Your Android API_KEY";
58 | /// AppSpectorPlugin.run(config);
59 | /// }
60 | ///
61 | /// For more information visit the AppSpector Page.
62 | class AppSpectorPlugin {
63 | static AppSpectorPlugin _appSpectorPlugin =
64 | AppSpectorPlugin._privateConstructor();
65 |
66 | final MethodChannel _channel = const MethodChannel('appspector_plugin');
67 | final RequestReceiver _requestReceiver = new RequestReceiver();
68 | Function(String)? _sessionUrlListener;
69 |
70 | set sessionUrlListener(Function(String)? listener) {
71 | _sessionUrlListener = listener;
72 | }
73 |
74 | AppSpectorPlugin._privateConstructor();
75 |
76 | AppSpectorPlugin._withConfig(
77 | Config config, Function(String)? sessionUrlListener) {
78 | HttpOverrides.global = AppSpectorHttpOverrides();
79 | _requestReceiver.observeChannel();
80 | _channel.setMethodCallHandler(_handlePluginCalls);
81 | _sessionUrlListener = sessionUrlListener;
82 | _appSpectorPlugin = this;
83 | }
84 |
85 | Future _init(Config config) {
86 | final monitors = config.monitors ?? Monitors.all();
87 | if (Platform.isAndroid) {
88 | ArgumentError.checkNotNull(config.androidApiKey, "androidApiKey");
89 | return _initAppSpector(
90 | config.androidApiKey,
91 | _filterByPlatform(monitors, SupportedPlatform.android),
92 | config.metadata);
93 | } else if (Platform.isIOS) {
94 | ArgumentError.checkNotNull(config.iosApiKey, "iosApiKey");
95 | return _initAppSpector(config.iosApiKey,
96 | _filterByPlatform(monitors, SupportedPlatform.ios), config.metadata);
97 | } else {
98 | return Future.error("AppSpector doesn't support current platform");
99 | }
100 | }
101 |
102 | Future _handlePluginCalls(MethodCall methodCall) async {
103 | if (methodCall.method == "onSessionUrl") {
104 | _sessionUrlListener?.call(methodCall.arguments);
105 | }
106 | }
107 |
108 | /// Returns shared instance of SDK plugin
109 | static AppSpectorPlugin shared() => _appSpectorPlugin;
110 |
111 | /// Method for starting AppSpector with supplied configs
112 | static Future run(Config config) async {
113 | final sharedInstance = shared();
114 | final isStarted = await sharedInstance.isStarted();
115 | if (!isStarted) {
116 | return new AppSpectorPlugin._withConfig(
117 | config, sharedInstance._sessionUrlListener)
118 | ._init(config);
119 | }
120 | }
121 |
122 | _initAppSpector(String? apiKey, Iterable monitors,
123 | Map? metadata) =>
124 | _channel.invokeMethod("run", {
125 | "apiKey": apiKey,
126 | "enabledMonitors": monitors.map((m) => m.id).toList(),
127 | "metadata": metadata
128 | });
129 |
130 | /// Stop all monitors and events sending
131 | Future stop() => _channel.invokeMethod("stop");
132 |
133 | /// Resume sending events and work of all monitors
134 | Future start() => _channel.invokeMethod("start");
135 |
136 | /// Returns true if sdk is started
137 | Future isStarted() =>
138 | _channel.invokeMethod("isStarted").then((value) => value ?? false);
139 |
140 | /// Set metadata value
141 | Future setMetadataValue(String key, String value) =>
142 | _channel.invokeMethod("setMetadata", {"key": key, "value": value});
143 |
144 | /// Remove metadata value
145 | Future removeMetadataValue(String key) =>
146 | _channel.invokeMethod("removeMetadata", {"key": key});
147 |
148 | Iterable _filterByPlatform(
149 | List monitors, SupportedPlatform platform) {
150 | return monitors.where((m) => m.platforms.contains(platform));
151 | }
152 | }
153 |
154 | /// Identifiers for supported metadata keys
155 | /// Sdk provides opportunity to send additional session information
156 | ///
157 | /// For more information see Config.metadata method
158 | class MetadataKeys {
159 | MetadataKeys._();
160 |
161 | /// Supported key to change device name
162 | static const deviceName = "userSpecifiedDeviceName";
163 | }
164 |
--------------------------------------------------------------------------------
/lib/src/event_sender.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/services.dart' show MethodChannel;
2 |
3 | class EventSender {
4 | static const MethodChannel _channel =
5 | const MethodChannel('appspector_event_channel');
6 |
7 | static sendEvent(Event event) async {
8 | await _channel.invokeMethod(event.name, event.arguments);
9 | }
10 | }
11 |
12 | abstract class Event {
13 | String get name;
14 |
15 | Map get arguments;
16 | }
17 |
--------------------------------------------------------------------------------
/lib/src/http/client.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async' show Future;
2 | import 'dart:io';
3 |
4 | import 'package:appspector/src/http/request_wrapper.dart';
5 | import 'package:appspector/src/http/tracker.dart';
6 |
7 | class AppSpectorHttpClient implements HttpClient {
8 | final HttpClient _httpClient;
9 | final String Function() _uidGenerator;
10 |
11 | AppSpectorHttpClient(this._httpClient, this._uidGenerator);
12 |
13 | @override
14 | get autoUncompress => _httpClient.autoUncompress;
15 |
16 | @override
17 | set autoUncompress(value) {
18 | _httpClient.autoUncompress = value;
19 | }
20 |
21 | @override
22 | get connectionTimeout => _httpClient.connectionTimeout;
23 |
24 | @override
25 | set connectionTimeout(value) {
26 | _httpClient.connectionTimeout = value;
27 | }
28 |
29 | @override
30 | get idleTimeout => _httpClient.idleTimeout;
31 |
32 | @override
33 | set idleTimeout(value) {
34 | _httpClient.idleTimeout = value;
35 | }
36 |
37 | @override
38 | get maxConnectionsPerHost => _httpClient.maxConnectionsPerHost;
39 |
40 | @override
41 | set maxConnectionsPerHost(value) {
42 | _httpClient.maxConnectionsPerHost = value;
43 | }
44 |
45 | @override
46 | get userAgent => _httpClient.userAgent;
47 |
48 | @override
49 | set userAgent(value) {
50 | _httpClient.userAgent = value;
51 | }
52 |
53 | @override
54 | void addCredentials(
55 | Uri url, String realm, HttpClientCredentials credentials) {
56 | return _httpClient.addCredentials(url, realm, credentials);
57 | }
58 |
59 | @override
60 | void addProxyCredentials(
61 | String host, int port, String realm, HttpClientCredentials credentials) {
62 | return _httpClient.addProxyCredentials(host, port, realm, credentials);
63 | }
64 |
65 | @override
66 | set authenticate(value) {
67 | _httpClient.authenticate = value;
68 | }
69 |
70 | @override
71 | set authenticateProxy(value) {
72 | _httpClient.authenticateProxy = value;
73 | }
74 |
75 | @override
76 | set badCertificateCallback(value) {
77 | _httpClient.badCertificateCallback = value;
78 | }
79 |
80 | @override
81 | set findProxy(String Function(Uri url)? f) {
82 | _httpClient.findProxy = f;
83 | }
84 |
85 | @override
86 | void close({bool force = false}) {
87 | return _httpClient.close(force: force);
88 | }
89 |
90 | @override
91 | Future delete(String host, int port, String path) =>
92 | open("delete", host, port, path);
93 |
94 | @override
95 | Future deleteUrl(Uri url) => openUrl("delete", url);
96 |
97 | @override
98 | Future get(String host, int port, String path) =>
99 | open("get", host, port, path);
100 |
101 | @override
102 | Future getUrl(Uri url) => openUrl("get", url);
103 |
104 | @override
105 | Future head(String host, int port, String path) =>
106 | open("head", host, port, path);
107 |
108 | @override
109 | Future headUrl(Uri url) => openUrl("head", url);
110 |
111 | @override
112 | Future patch(String host, int port, String path) =>
113 | open("patch", host, port, path);
114 |
115 | @override
116 | Future patchUrl(Uri url) => openUrl("patch", url);
117 |
118 | @override
119 | Future post(String host, int port, String path) =>
120 | open("post", host, port, path);
121 |
122 | @override
123 | Future postUrl(Uri url) => openUrl("post", url);
124 |
125 | @override
126 | Future put(String host, int port, String path) =>
127 | open("put", host, port, path);
128 |
129 | @override
130 | Future putUrl(Uri url) => openUrl("put", url);
131 |
132 | @override
133 | Future open(
134 | String method, String host, int port, String path) {
135 | final tracker =
136 | HttpEventTracker.fromHost(method, _uidGenerator(), host, port, path);
137 | return _httpClient.open(method, host, port, path).then((request) {
138 | return HttpRequestWrapper(request, tracker);
139 | }).onError((Exception error, stackTrace) {
140 | tracker.onError(error);
141 | return Future.error(error, stackTrace);
142 | });
143 | }
144 |
145 | @override
146 | Future openUrl(String method, Uri url) async {
147 | final tracker = HttpEventTracker.fromUri(method, _uidGenerator(), url);
148 | return _httpClient.openUrl(method, url).then((request) {
149 | return HttpRequestWrapper(request, tracker);
150 | }).onError((Exception error, stackTrace) {
151 | tracker.onError(error);
152 | return Future.error(error, stackTrace);
153 | });
154 | }
155 |
156 | set connectionFactory(
157 | Future> Function(
158 | Uri url, String? proxyHost, int? proxyPort)?
159 | f) =>
160 | _httpClient.connectionFactory = f;
161 |
162 | set keyLog(Function(String line)? callback) => _httpClient.keyLog = callback;
163 | }
164 |
--------------------------------------------------------------------------------
/lib/src/http/events.dart:
--------------------------------------------------------------------------------
1 | import 'dart:typed_data';
2 |
3 | import 'package:appspector/src/event_sender.dart' show Event;
4 |
5 | class HttpResponseEvent extends Event {
6 | final String uid;
7 | final int timeMs;
8 | final int code;
9 | final Map headers;
10 | final String? error;
11 | final Uint8List? body;
12 |
13 | HttpResponseEvent(
14 | this.uid, this.timeMs, this.code, this.headers, this.error, this.body);
15 |
16 | @override
17 | String get name => "http-response";
18 |
19 | @override
20 | Map get arguments => {
21 | "uid": uid,
22 | "code": code,
23 | "headers": headers,
24 | "error": error,
25 | "body": body,
26 | "tookMs": timeMs
27 | };
28 | }
29 |
30 | class HttpRequestEvent extends Event {
31 | final String uid;
32 | final String url;
33 | final String method;
34 | final Map headers;
35 | final Uint8List? body;
36 |
37 | @override
38 | String get name => "http-request";
39 |
40 | HttpRequestEvent(this.uid, this.url, this.method, this.headers, this.body);
41 |
42 | @override
43 | Map get arguments => {
44 | "uid": uid,
45 | "url": url,
46 | "method": method,
47 | "headers": headers,
48 | "body": body
49 | };
50 | }
51 |
--------------------------------------------------------------------------------
/lib/src/http/http_overrides.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io' show HttpClient, HttpOverrides, SecurityContext;
2 | import 'dart:math' show Random;
3 |
4 | import 'package:appspector/src/http/client.dart';
5 |
6 | class AppSpectorHttpOverrides extends HttpOverrides {
7 | final Random _random = new Random();
8 |
9 | @override
10 | HttpClient createHttpClient(SecurityContext? context) {
11 | return AppSpectorHttpClient(super.createHttpClient(context), _generateUuid);
12 | }
13 |
14 | String _generateUuid() {
15 | // Generate xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx / 8-4-4-4-12.
16 | final int special = 8 + _random.nextInt(4);
17 |
18 | return '${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}-'
19 | '${_bitsDigits(16, 4)}-'
20 | '4${_bitsDigits(12, 3)}-'
21 | '${_printDigits(special, 1)}${_bitsDigits(12, 3)}-'
22 | '${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}';
23 | }
24 |
25 | String _bitsDigits(int bitCount, int digitCount) =>
26 | _printDigits(_generateBits(bitCount), digitCount);
27 |
28 | int _generateBits(int bitCount) => _random.nextInt(1 << bitCount);
29 |
30 | String _printDigits(int value, int count) =>
31 | value.toRadixString(16).padLeft(count, '0');
32 | }
33 |
--------------------------------------------------------------------------------
/lib/src/http/request_wrapper.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:convert';
3 | import 'dart:io';
4 |
5 | import 'package:appspector/src/http/response_wrapper.dart';
6 | import 'package:appspector/src/http/tracker.dart';
7 |
8 | class HttpRequestWrapper extends HttpClientRequest {
9 | final HttpClientRequest _httpClientRequest;
10 | final HttpEventTracker _httpEventTracker;
11 |
12 | @override
13 | Encoding get encoding => _httpClientRequest.encoding;
14 |
15 | @override
16 | set encoding(Encoding value) => _httpClientRequest.encoding = value;
17 |
18 | @override
19 | int get contentLength => _httpClientRequest.contentLength;
20 |
21 | @override
22 | set contentLength(int value) => _httpClientRequest.contentLength = value;
23 |
24 | @override
25 | bool get bufferOutput => _httpClientRequest.bufferOutput;
26 |
27 | @override
28 | set bufferOutput(bool value) => _httpClientRequest.bufferOutput = value;
29 |
30 | @override
31 | bool get followRedirects => _httpClientRequest.followRedirects;
32 |
33 | @override
34 | set followRedirects(bool value) => _httpClientRequest.followRedirects = value;
35 |
36 | @override
37 | bool get persistentConnection => _httpClientRequest.persistentConnection;
38 |
39 | @override
40 | set persistentConnection(bool value) =>
41 | _httpClientRequest.persistentConnection = value;
42 |
43 | @override
44 | int get maxRedirects => _httpClientRequest.maxRedirects;
45 |
46 | @override
47 | set maxRedirects(int value) => _httpClientRequest.maxRedirects = value;
48 |
49 | HttpRequestWrapper(this._httpClientRequest, this._httpEventTracker);
50 |
51 | @override
52 | void add(List data) {
53 | _httpEventTracker.addData(data);
54 | _httpClientRequest.add(data);
55 | }
56 |
57 | @override
58 | void addError(Object error, [StackTrace? stackTrace]) {
59 | _httpClientRequest.addError(error, stackTrace);
60 | }
61 |
62 | @override
63 | Future addStream(Stream> stream) {
64 | final List body = [];
65 | final StreamTransformer, List> streamTransformer =
66 | StreamTransformer.fromHandlers(
67 | handleData: (List data, EventSink> sink) {
68 | sink.add(data);
69 | body.addAll(data);
70 | }, handleDone: (sink) {
71 | _httpEventTracker.addData(body);
72 | sink.close();
73 | });
74 | final Stream> resultedStream = streamTransformer.bind(stream);
75 | return _httpClientRequest.addStream(resultedStream);
76 | }
77 |
78 | @override
79 | Future close() async {
80 | _httpEventTracker.sendRequestEvent(headers);
81 |
82 | final List body = [];
83 | final HttpClientResponse response = await _httpClientRequest.close();
84 | return new HttpResponseWrapper(
85 | response,
86 | response.transform(StreamTransformer.fromHandlers(
87 | handleData: (List data, EventSink> sink) {
88 | sink.add(data);
89 | body.addAll(data);
90 | }, handleError: (error, stackTrace, sink) {
91 | print("HttpRequestWrapper :: ERROR RESPONSE $error $stackTrace");
92 | }, handleDone: (sink) {
93 | _httpEventTracker.sendSuccessResponse(
94 | response.statusCode, response.headers, body);
95 | sink.close();
96 | })));
97 | }
98 |
99 | @override
100 | HttpConnectionInfo? get connectionInfo => _httpClientRequest.connectionInfo;
101 |
102 | @override
103 | List get cookies => _httpClientRequest.cookies;
104 |
105 | @override
106 | Future get done => _httpClientRequest.done;
107 |
108 | @override
109 | Future flush() => _httpClientRequest.flush();
110 |
111 | @override
112 | HttpHeaders get headers => _httpClientRequest.headers;
113 |
114 | @override
115 | String get method => _httpClientRequest.method;
116 |
117 | @override
118 | Uri get uri => _httpClientRequest.uri;
119 |
120 | @override
121 | void write(Object? obj) {
122 | _httpClientRequest.write(obj);
123 | }
124 |
125 | @override
126 | void writeAll(Iterable objects, [String separator = ""]) {
127 | _httpClientRequest.writeAll(objects, separator);
128 | }
129 |
130 | @override
131 | void writeCharCode(int charCode) {
132 | _httpClientRequest.writeCharCode(charCode);
133 | }
134 |
135 | @override
136 | void writeln([Object? obj = ""]) {
137 | _httpClientRequest.writeln(obj);
138 | }
139 |
140 | @override
141 | void abort([Object? exception, StackTrace? stackTrace]) {
142 | _httpClientRequest.abort(exception, stackTrace);
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/lib/src/http/response_wrapper.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async' show StreamView;
2 | import 'dart:io';
3 |
4 | class HttpResponseWrapper extends StreamView>
5 | implements HttpClientResponse {
6 | final HttpClientResponse _httpClientResponse;
7 |
8 | HttpResponseWrapper(this._httpClientResponse, Stream> stream)
9 | : super(stream);
10 |
11 | @override
12 | X509Certificate? get certificate => _httpClientResponse.certificate;
13 |
14 | @override
15 | HttpConnectionInfo? get connectionInfo => _httpClientResponse.connectionInfo;
16 |
17 | @override
18 | int get contentLength => _httpClientResponse.contentLength;
19 |
20 | @override
21 | List get cookies => _httpClientResponse.cookies;
22 |
23 | @override
24 | Future detachSocket() => _httpClientResponse.detachSocket();
25 |
26 | @override
27 | HttpHeaders get headers => _httpClientResponse.headers;
28 |
29 | @override
30 | bool get isRedirect => _httpClientResponse.isRedirect;
31 |
32 | @override
33 | bool get persistentConnection => _httpClientResponse.persistentConnection;
34 |
35 | @override
36 | String get reasonPhrase => _httpClientResponse.reasonPhrase;
37 |
38 | @override
39 | Future redirect(
40 | [String? method, Uri? url, bool? followLoops]) {
41 | return _httpClientResponse.redirect(method, url, followLoops);
42 | }
43 |
44 | @override
45 | List get redirects => _httpClientResponse.redirects;
46 |
47 | @override
48 | int get statusCode => _httpClientResponse.statusCode;
49 |
50 | @override
51 | HttpClientResponseCompressionState get compressionState =>
52 | _httpClientResponse.compressionState;
53 | }
54 |
--------------------------------------------------------------------------------
/lib/src/http/tracker.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io' show HttpHeaders;
2 | import 'dart:typed_data' show Uint8List;
3 |
4 | import 'package:appspector/src/event_sender.dart' show EventSender;
5 | import 'package:appspector/src/http/events.dart';
6 |
7 | class HttpEventTracker {
8 | final String _url;
9 | final String _uid;
10 | final String _method;
11 | final int _startTime;
12 | Uint8List? data;
13 |
14 | HttpEventTracker.fromUri(this._method, this._uid, Uri uri)
15 | : this._url = uri.toString(),
16 | this._startTime = DateTime.now().millisecondsSinceEpoch;
17 |
18 | HttpEventTracker.fromHost(
19 | this._method, this._uid, String host, int port, String path)
20 | : this._url =
21 | Uri(scheme: "http", host: host, port: port, path: path).toString(),
22 | this._startTime = DateTime.now().millisecondsSinceEpoch;
23 |
24 | void onError(Exception e) {
25 | _sendRequestEvent({});
26 |
27 | EventSender.sendEvent(new HttpResponseEvent(
28 | _uid, _calcDurationTime(), 0, {}, e.toString(), null));
29 | }
30 |
31 | void addData(List bytes) {
32 | this.data = Uint8List.fromList(bytes);
33 | }
34 |
35 | void sendRequestEvent(HttpHeaders headers) =>
36 | _sendRequestEvent(_headersToMap(headers));
37 |
38 | void _sendRequestEvent(Map headers) {
39 | EventSender.sendEvent(
40 | new HttpRequestEvent(_uid, _url, _method, headers, data));
41 | }
42 |
43 | void sendSuccessResponse(
44 | int statusCode, HttpHeaders headers, List data) {
45 | EventSender.sendEvent(new HttpResponseEvent(_uid, _calcDurationTime(),
46 | statusCode, _headersToMap(headers), null, Uint8List.fromList(data)));
47 | }
48 |
49 | int _calcDurationTime() {
50 | return DateTime.now().millisecondsSinceEpoch - _startTime;
51 | }
52 |
53 | Map _headersToMap(HttpHeaders httpHeaders) {
54 | final Map headers = {};
55 |
56 | httpHeaders.forEach((header, values) {
57 | headers[header] = values.first;
58 | });
59 |
60 | return headers;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/lib/src/log/logger.dart:
--------------------------------------------------------------------------------
1 | import 'package:appspector/src/event_sender.dart';
2 |
3 | class Logger {
4 | static void v(String tag, String message,
5 | [Object? error, StackTrace? stackTrace]) {
6 | log(LogLevel.VERBOSE, tag, message, error, stackTrace);
7 | }
8 |
9 | static void d(String tag, String message,
10 | [Object? error, StackTrace? stackTrace]) {
11 | log(LogLevel.DEBUG, tag, message, error, stackTrace);
12 | }
13 |
14 | static void i(String tag, String message,
15 | [Object? error, StackTrace? stackTrace]) {
16 | log(LogLevel.INFO, tag, message, error, stackTrace);
17 | }
18 |
19 | static void w(String tag, String message,
20 | [Object? error, StackTrace? stackTrace]) {
21 | log(LogLevel.WARN, tag, message, error, stackTrace);
22 | }
23 |
24 | static void e(String tag, String message,
25 | [Object? error, StackTrace? stackTrace]) {
26 | log(LogLevel.ERROR, tag, message, error, stackTrace);
27 | }
28 |
29 | static void wtf(String tag, String message,
30 | [Object? error, StackTrace? stackTrace]) {
31 | log(LogLevel.ASSERT, tag, message, error, stackTrace);
32 | }
33 |
34 | static void log(LogLevel level, String tag, String message,
35 | [Object? error, StackTrace? stackTrace]) {
36 | if (error != null) {
37 | message = "$message\n$error";
38 | }
39 | if (stackTrace != null) {
40 | message = "$message\n$stackTrace";
41 | }
42 | EventSender.sendEvent(new _LogEvent(level, tag, message));
43 | }
44 | }
45 |
46 | class _LogEvent extends Event {
47 | final LogLevel _level;
48 | final String _tag;
49 | final String _message;
50 |
51 | _LogEvent(this._level, this._tag, this._message);
52 |
53 | @override
54 | Map get arguments =>
55 | {"level": _level.value, "tag": _tag, "message": _message};
56 |
57 | @override
58 | String get name => "log-event";
59 | }
60 |
61 | class LogLevel {
62 | final int value;
63 |
64 | const LogLevel._internal(this.value);
65 |
66 | toString() => 'LogLevel.$value';
67 |
68 | static const VERBOSE = const LogLevel._internal(2);
69 | static const DEBUG = const LogLevel._internal(3);
70 | static const INFO = const LogLevel._internal(4);
71 | static const WARN = const LogLevel._internal(5);
72 | static const ERROR = const LogLevel._internal(6);
73 | static const ASSERT = const LogLevel._internal(6);
74 | }
75 |
--------------------------------------------------------------------------------
/lib/src/monitors.dart:
--------------------------------------------------------------------------------
1 | enum SupportedPlatform { android, ios }
2 |
3 | /// Class which contains description of monitor
4 | class Monitor {
5 | /// Monitor identifier
6 | final String id;
7 |
8 | /// List of platforms which support this monitor
9 | final List platforms;
10 |
11 | const Monitor._androidMonitor(this.id)
12 | : platforms = const [SupportedPlatform.android];
13 |
14 | const Monitor._iosMonitor(this.id)
15 | : platforms = const [SupportedPlatform.ios];
16 |
17 | const Monitor._commonMonitor(this.id)
18 | : platforms = const [SupportedPlatform.android, SupportedPlatform.ios];
19 | }
20 |
21 | /// Identifiers for supported monitors
22 | class Monitors {
23 | Monitors._();
24 |
25 | static const http = const Monitor._commonMonitor("http");
26 | static const logs = const Monitor._commonMonitor("logs");
27 | static const fileSystem = const Monitor._commonMonitor("file-system");
28 | static const screenshot = const Monitor._commonMonitor("screenshot");
29 | static const environment = const Monitor._commonMonitor("environment");
30 | static const location = const Monitor._commonMonitor("location");
31 | static const performance = const Monitor._commonMonitor("performance");
32 | static const sqLite = const Monitor._commonMonitor("sqlite");
33 | static const sharedPreferences =
34 | const Monitor._androidMonitor("shared-preferences");
35 | static const analytics = const Monitor._iosMonitor("analytics");
36 | static const notification = const Monitor._iosMonitor("notification-center");
37 | static const userDefaults = const Monitor._iosMonitor("user-defaults");
38 | static const coreData = const Monitor._iosMonitor("ios-core-data");
39 |
40 | static List all() => [
41 | http,
42 | logs,
43 | screenshot,
44 | environment,
45 | location,
46 | performance,
47 | sqLite,
48 | fileSystem,
49 | analytics,
50 | notification,
51 | userDefaults,
52 | coreData,
53 | sharedPreferences
54 | ];
55 | }
56 |
--------------------------------------------------------------------------------
/lib/src/request_receiver.dart:
--------------------------------------------------------------------------------
1 | import 'dart:ui' as ui;
2 |
3 | import 'package:flutter/rendering.dart';
4 | import 'package:flutter/services.dart';
5 | import 'package:flutter/widgets.dart';
6 |
7 | typedef Future RequestHandler(dynamic args);
8 |
9 | class RequestReceiver {
10 | static const MethodChannel _channel =
11 | const MethodChannel('appspector_request_channel');
12 | final Map handlers = Map();
13 |
14 | RequestReceiver() {
15 | handlers["take_screenshot"] = _takeScreenshot;
16 | }
17 |
18 | void observeChannel() {
19 | _channel.setMethodCallHandler(_handler);
20 | }
21 |
22 | Future _handler(MethodCall call) async {
23 | final handler = handlers[call.method];
24 | if (handler != null) {
25 | return handler(call.arguments);
26 | }
27 | //todo
28 | }
29 | }
30 |
31 | Future _takeScreenshot(dynamic args) async {
32 | int maxWidth = args["max_width"];
33 | var renderViewElement =
34 | WidgetsFlutterBinding.ensureInitialized().rootElement;
35 | var renderObject = renderViewElement?.findRenderObject();
36 | if (renderObject == null) {
37 | return null;
38 | }
39 | var ratio = maxWidth / renderObject.paintBounds.width;
40 |
41 | // ignore: invalid_use_of_protected_member
42 | var image = await (renderObject.layer as OffsetLayer)
43 | .toImage(renderObject.paintBounds, pixelRatio: ratio > 1.0 ? 1.0 : ratio);
44 |
45 | var byteData = await image.toByteData(format: ui.ImageByteFormat.png);
46 | return byteData != null ? byteData.buffer.asUint8List() : null;
47 | }
48 |
--------------------------------------------------------------------------------
/publish.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | flutter upgrade
4 |
5 | cd example
6 | flutter clean
7 | cd ..
8 |
9 | flutter pub publish && echo "Project was successfully published" || echo "Project wasn't published"
10 |
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: appspector
2 | description: Flutter Plugin that integrate AppSpector to your mobile project. It provides remote access to application data to simplify developing process.
3 | version: 0.10.0
4 | homepage: https://github.com/appspector/flutter-plugin
5 |
6 | environment:
7 | sdk: ">=2.17.0 <4.0.0"
8 | flutter: ">=3.0.0"
9 |
10 | dependencies:
11 | flutter:
12 | sdk: flutter
13 |
14 | flutter:
15 | plugin:
16 | platforms:
17 | android:
18 | package: com.appspector.flutter
19 | pluginClass: AppSpectorPlugin
20 | ios:
21 | pluginClass: AppSpectorPlugin
22 |
--------------------------------------------------------------------------------
/static/appspector_demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/static/appspector_demo.gif
--------------------------------------------------------------------------------
/test_ios.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -ex
4 |
5 | cd ./example/ios
6 |
7 | flutter pub get
8 | pod install
9 |
10 | # Build and test
11 | xcodebuild -workspace Runner.xcworkspace -scheme Runner -configuration Debug -sdk iphonesimulator -derivedDataPath /tmp/dd -destination "platform=iOS Simulator,name=iPhone 13 Pro Max,OS=16.2" build test
12 |
--------------------------------------------------------------------------------