AndroidFridaManager 1.9.3__tar.gz → 1.9.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (19) hide show
  1. {androidfridamanager-1.9.3 → androidfridamanager-1.9.4}/AndroidFridaManager/about.py +1 -1
  2. {androidfridamanager-1.9.3 → androidfridamanager-1.9.4}/AndroidFridaManager/job.py +73 -3
  3. {androidfridamanager-1.9.3 → androidfridamanager-1.9.4}/AndroidFridaManager/job_manager.py +305 -18
  4. {androidfridamanager-1.9.3 → androidfridamanager-1.9.4}/AndroidFridaManager.egg-info/PKG-INFO +2 -2
  5. {androidfridamanager-1.9.3 → androidfridamanager-1.9.4}/PKG-INFO +2 -2
  6. {androidfridamanager-1.9.3 → androidfridamanager-1.9.4}/README.md +1 -1
  7. {androidfridamanager-1.9.3 → androidfridamanager-1.9.4}/AndroidFridaManager/FridaManager.py +0 -0
  8. {androidfridamanager-1.9.3 → androidfridamanager-1.9.4}/AndroidFridaManager/__init__.py +0 -0
  9. {androidfridamanager-1.9.3 → androidfridamanager-1.9.4}/AndroidFridaManager.egg-info/SOURCES.txt +0 -0
  10. {androidfridamanager-1.9.3 → androidfridamanager-1.9.4}/AndroidFridaManager.egg-info/dependency_links.txt +0 -0
  11. {androidfridamanager-1.9.3 → androidfridamanager-1.9.4}/AndroidFridaManager.egg-info/entry_points.txt +0 -0
  12. {androidfridamanager-1.9.3 → androidfridamanager-1.9.4}/AndroidFridaManager.egg-info/requires.txt +0 -0
  13. {androidfridamanager-1.9.3 → androidfridamanager-1.9.4}/AndroidFridaManager.egg-info/top_level.txt +0 -0
  14. {androidfridamanager-1.9.3 → androidfridamanager-1.9.4}/LICENSE +0 -0
  15. {androidfridamanager-1.9.3 → androidfridamanager-1.9.4}/MANIFEST.in +0 -0
  16. {androidfridamanager-1.9.3 → androidfridamanager-1.9.4}/pyproject.toml +0 -0
  17. {androidfridamanager-1.9.3 → androidfridamanager-1.9.4}/requirements.txt +0 -0
  18. {androidfridamanager-1.9.3 → androidfridamanager-1.9.4}/setup.cfg +0 -0
  19. {androidfridamanager-1.9.3 → androidfridamanager-1.9.4}/setup.py +0 -0
@@ -2,4 +2,4 @@
2
2
  # -*- coding: utf-8 -*-
3
3
 
4
4
  __author__ = "Daniel Baier"
5
- __version__ = "1.9.3"
5
+ __version__ = "1.9.4"
@@ -1,18 +1,62 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
+ """Frida Job management for AndroidFridaManager.
3
4
 
5
+ This module provides the Job class for managing individual Frida instrumentation
6
+ jobs, including script loading, message handling, and lifecycle management.
7
+ """
4
8
 
5
9
  import threading
6
10
  import frida
7
11
  import uuid
8
12
  import logging
13
+ import datetime
14
+ from typing import Optional, List, Callable, Any
15
+
9
16
 
10
17
  # Define a custom exception for handling frida based exceptions
11
18
  class FridaBasedException(Exception):
12
19
  pass
13
20
 
21
+
14
22
  class Job:
15
- def __init__(self, frida_script_name, custom_hooking_handler, process):
23
+ """Represents a single Frida instrumentation job.
24
+
25
+ A job encapsulates a Frida script, its message handler, and lifecycle
26
+ management. Jobs run in separate threads and can be started/stopped
27
+ independently.
28
+
29
+ Attributes:
30
+ job_id: Unique identifier for this job.
31
+ job_type: Category of job (e.g., "fritap", "dexray", "trigdroid", "custom").
32
+ display_name: Human-readable name for UI display.
33
+ hooks_registry: List of methods/functions this job hooks (for conflict detection).
34
+ priority: Job priority (lower = higher priority, default 50).
35
+ state: Current state ("initialized", "running", "stopping").
36
+ started_at: Timestamp when job was started (None if not started).
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ frida_script_name: str,
42
+ custom_hooking_handler: Callable,
43
+ process: Any,
44
+ job_type: str = "custom",
45
+ display_name: Optional[str] = None,
46
+ hooks_registry: Optional[List[str]] = None,
47
+ priority: int = 50,
48
+ ):
49
+ """Initialize a new Job.
50
+
51
+ Args:
52
+ frida_script_name: Path to the Frida script file.
53
+ custom_hooking_handler: Callback function for handling Frida messages.
54
+ process: Frida session/process to attach the script to.
55
+ job_type: Category of job for coordination (default: "custom").
56
+ display_name: Human-readable name (default: script filename).
57
+ hooks_registry: List of hooked methods for conflict detection.
58
+ priority: Job priority, lower = higher (default: 50).
59
+ """
16
60
  self.frida_script_name = frida_script_name
17
61
  self.job_id = str(uuid.uuid4())
18
62
  self.state = "initialized"
@@ -24,6 +68,13 @@ class Job:
24
68
  self.is_script_created = False
25
69
  self.logger = logging.getLogger(__name__)
26
70
 
71
+ # New metadata fields for job coordination
72
+ self.job_type = job_type
73
+ self.display_name = display_name or frida_script_name
74
+ self.hooks_registry = hooks_registry or []
75
+ self.priority = priority
76
+ self.started_at: Optional[datetime.datetime] = None
77
+
27
78
 
28
79
  def create_job_script(self):
29
80
  self.instrument(self.process_session)
@@ -31,7 +82,8 @@ class Job:
31
82
 
32
83
 
33
84
  def run_job(self):
34
- #self.is_running_as_thread = True
85
+ """Start the job execution in a separate thread."""
86
+ self.started_at = datetime.datetime.now()
35
87
  self.run_job_as_thread()
36
88
 
37
89
 
@@ -98,4 +150,22 @@ class Job:
98
150
 
99
151
 
100
152
  def get_script_of_job(self):
101
- return self.script
153
+ return self.script
154
+
155
+ def get_info(self) -> dict:
156
+ """Get job information as a dictionary for UI display.
157
+
158
+ Returns:
159
+ Dictionary containing job metadata and state information.
160
+ """
161
+ return {
162
+ "job_id": self.job_id,
163
+ "job_type": self.job_type,
164
+ "display_name": self.display_name,
165
+ "state": self.state,
166
+ "priority": self.priority,
167
+ "hooks_count": len(self.hooks_registry),
168
+ "hooks_registry": self.hooks_registry.copy(),
169
+ "started_at": self.started_at.isoformat() if self.started_at else None,
170
+ "script_name": self.frida_script_name,
171
+ }
@@ -1,30 +1,45 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
+ """Frida JobManager for coordinating multiple instrumentation jobs.
4
+
5
+ This module provides the JobManager class for managing Frida sessions and
6
+ coordinating multiple instrumentation jobs on Android devices.
7
+ """
3
8
 
4
9
  import atexit
5
10
  import subprocess
6
11
  import frida
7
- from typing import Optional, Dict, Union, List
12
+ from typing import Optional, Dict, Union, List, Callable
8
13
  from .job import Job, FridaBasedException
9
14
  import time
10
15
  import re
11
16
  import logging
12
17
 
18
+
13
19
  class JobManager(object):
14
- """ A class representing the current Job manager with multi-device support. """
20
+ """Job manager with multi-device support and hook coordination.
21
+
22
+ Manages Frida sessions and coordinates multiple instrumentation jobs,
23
+ providing hook conflict detection and session status information.
15
24
 
25
+ Attributes:
26
+ jobs: Dictionary of active jobs by job_id.
27
+ process_session: Current Frida session.
28
+ device: Current Frida device.
29
+ package_name: Name of the attached/spawned package.
30
+ pid: Process ID (-1 if attach mode).
31
+ """
16
32
 
17
33
  def __init__(self, host="", enable_spawn_gating=False, device_serial: Optional[str] = None) -> None:
18
- """
19
- Init a new job manager with optional device targeting.
34
+ """Init a new job manager with optional device targeting.
20
35
 
21
- :param host: Remote host for Frida connection (ip:port format)
22
- :param enable_spawn_gating: Enable spawn gating for child process tracking
23
- :param device_serial: Specific device serial to target (e.g., 'emulator-5554').
24
- If None, uses default device selection.
36
+ Args:
37
+ host: Remote host for Frida connection (ip:port format).
38
+ enable_spawn_gating: Enable spawn gating for child process tracking.
39
+ device_serial: Specific device serial to target (e.g., 'emulator-5554').
40
+ If None, uses default device selection.
25
41
  """
26
-
27
- self.jobs = {}
42
+ self.jobs: Dict[str, Job] = {}
28
43
  self.is_first_job = True
29
44
  self.process_session = None
30
45
  self.host = host
@@ -42,6 +57,11 @@ class JobManager(object):
42
57
  self._device_serial = device_serial
43
58
  self._multiple_devices = self._check_multiple_devices()
44
59
 
60
+ # Hook coordination (NEW)
61
+ self._hook_registry: Dict[str, str] = {} # hook_target -> job_id
62
+ self._mode: Optional[str] = None # "spawn" or "attach"
63
+ self._paused: bool = False # Track if spawned process is paused
64
+
45
65
  atexit.register(self.cleanup)
46
66
 
47
67
  def _ensure_logging_setup(self):
@@ -134,13 +154,80 @@ class JobManager(object):
134
154
  '''
135
155
 
136
156
  def spawn(self, target_process):
157
+ """Spawn a new process and attach to it.
158
+
159
+ Args:
160
+ target_process: Package name to spawn.
161
+
162
+ Returns:
163
+ Process ID of the spawned process.
164
+ """
165
+ self._mode = "spawn"
137
166
  self.package_name = target_process
138
167
  self.logger.info("[*] spawning app: "+ target_process)
139
168
  pid = self.device.spawn(target_process)
140
169
  self.process_session = self.device.attach(pid)
170
+ self.pid = pid
171
+ self._paused = True # Spawned processes start paused
141
172
  self.logger.info(f"Spawned {target_process} with PID {pid}")
142
173
  return pid
143
174
 
175
+ def spawn_paused(self, target_process: str) -> int:
176
+ """Spawn a process but keep it paused for multi-tool loading.
177
+
178
+ Unlike the regular spawn flow where resume happens on first job,
179
+ this method keeps the process paused until explicitly resumed
180
+ via resume_app(). This allows loading multiple Jobs before
181
+ the app starts executing.
182
+
183
+ Args:
184
+ target_process: Package name to spawn.
185
+
186
+ Returns:
187
+ Process ID of the spawned (paused) process.
188
+ """
189
+ self._mode = "spawn"
190
+ self.package_name = target_process
191
+ self.logger.info(f"[*] spawning app (paused): {target_process}")
192
+ pid = self.device.spawn(target_process)
193
+ self.process_session = self.device.attach(pid)
194
+ self.pid = pid
195
+ self._paused = True
196
+ # Mark that auto-resume should NOT happen
197
+ self.is_first_job = False # Prevent auto-resume in start_job()
198
+ self.logger.info(f"Spawned {target_process} with PID {pid} (PAUSED - awaiting manual resume)")
199
+ return pid
200
+
201
+ def resume_app(self) -> bool:
202
+ """Resume a paused spawned process.
203
+
204
+ Call this after loading all Jobs to start the app with
205
+ all hooks already installed.
206
+
207
+ Returns:
208
+ True if resumed successfully, False if not paused or failed.
209
+ """
210
+ if not self._paused or self.pid == -1:
211
+ self.logger.warning("No paused process to resume")
212
+ return False
213
+
214
+ try:
215
+ self.device.resume(self.pid)
216
+ self._paused = False
217
+ time.sleep(1) # Required for Java.perform stability
218
+ self.logger.info(f"Resumed process {self.pid}")
219
+ return True
220
+ except Exception as e:
221
+ self.logger.error(f"Failed to resume process: {e}")
222
+ return False
223
+
224
+ def is_paused(self) -> bool:
225
+ """Check if the spawned process is currently paused.
226
+
227
+ Returns:
228
+ True if process is spawned and waiting for resume.
229
+ """
230
+ return self._paused
144
231
 
145
232
  def start_android_app(self, package_name: str, main_activity: Optional[str] = None, extras: Optional[Dict[str, Union[str, bool]]] = None):
146
233
  """
@@ -190,6 +277,13 @@ class JobManager(object):
190
277
 
191
278
 
192
279
  def attach_app(self, target_process, foreground=False):
280
+ """Attach to a running process.
281
+
282
+ Args:
283
+ target_process: Package name or PID to attach to.
284
+ foreground: If True, attach to the frontmost application.
285
+ """
286
+ self._mode = "attach"
193
287
  self.package_name = target_process
194
288
 
195
289
  if foreground:
@@ -261,11 +355,44 @@ class JobManager(object):
261
355
  raise FridaBasedException(f"Frida-Error: {fe}")
262
356
 
263
357
 
264
- def start_job(self,frida_script_name, custom_hooking_handler_name):
358
+ def start_job(
359
+ self,
360
+ frida_script_name: str,
361
+ custom_hooking_handler_name: Callable,
362
+ job_type: str = "custom",
363
+ display_name: Optional[str] = None,
364
+ hooks_registry: Optional[List[str]] = None,
365
+ priority: int = 50,
366
+ ) -> Optional[Job]:
367
+ """Start a new instrumentation job.
368
+
369
+ Args:
370
+ frida_script_name: Path to the Frida script file.
371
+ custom_hooking_handler_name: Callback for handling Frida messages.
372
+ job_type: Category of job (e.g., "fritap", "dexray", "trigdroid").
373
+ display_name: Human-readable name for UI display.
374
+ hooks_registry: List of hooked methods for conflict detection.
375
+ priority: Job priority (lower = higher priority).
376
+
377
+ Returns:
378
+ The created Job instance, or None if no session exists.
379
+
380
+ Raises:
381
+ FridaBasedException: If Frida encounters an error.
382
+ """
383
+ job = None # Initialize before try block for safe exception handling
265
384
  try:
266
385
  if self.process_session:
267
- job = Job(frida_script_name, custom_hooking_handler_name, self.process_session)
268
- self.logger.info(f"[*] created job: {job.job_id}")
386
+ job = Job(
387
+ frida_script_name,
388
+ custom_hooking_handler_name,
389
+ self.process_session,
390
+ job_type=job_type,
391
+ display_name=display_name,
392
+ hooks_registry=hooks_registry,
393
+ priority=priority,
394
+ )
395
+ self.logger.info(f"[*] created job: {job.job_id} ({job.display_name})")
269
396
  self.jobs[job.job_id] = job
270
397
  self.last_created_job = job
271
398
  job.run_job()
@@ -274,13 +401,14 @@ class JobManager(object):
274
401
  self.first_instrumenation_script = custom_hooking_handler_name
275
402
  if self.pid != -1:
276
403
  self.device.resume(self.pid)
277
- time.sleep(1) # without it Java.perform silently fails
404
+ self._paused = False # Mark as resumed
405
+ time.sleep(1) # without it Java.perform silently fails
278
406
 
279
407
  return job
280
408
 
281
409
  else:
282
410
  self.logger.error("[-] no frida session. Aborting...")
283
-
411
+ return None
284
412
 
285
413
  except frida.TransportError as fe:
286
414
  raise FridaBasedException(f"Problems while attaching to frida-server: {fe}")
@@ -291,8 +419,9 @@ class JobManager(object):
291
419
  except frida.ProcessNotFoundError as pe:
292
420
  raise FridaBasedException(f"ProcessNotFoundError: {pe}")
293
421
  except KeyboardInterrupt:
294
- self.stop_app_with_last_job(job,self.package_name)
295
- pass
422
+ if job:
423
+ self.stop_app_with_last_job(job, self.package_name)
424
+ return None
296
425
 
297
426
 
298
427
  def stop_jobs(self):
@@ -306,9 +435,16 @@ class JobManager(object):
306
435
  'no longer be available.'.format(job_id))
307
436
 
308
437
 
309
- def stop_job_with_id(self,job_id):
438
+ def stop_job_with_id(self, job_id: str) -> None:
439
+ """Stop a specific job by ID.
440
+
441
+ Args:
442
+ job_id: UUID of the job to stop.
443
+ """
310
444
  if job_id in self.jobs:
311
445
  job = self.jobs[job_id]
446
+ # Unregister hooks before closing
447
+ self.unregister_hooks(job_id)
312
448
  job.close_job()
313
449
  del self.jobs[job_id]
314
450
 
@@ -411,3 +547,154 @@ class JobManager(object):
411
547
  raise FridaBasedException("Unable to find device")
412
548
  except frida.ServerNotRunningError:
413
549
  raise FridaBasedException("Frida server not running. Start frida-server and try it again.")
550
+
551
+ # ==================== Hook Coordination Methods ====================
552
+
553
+ def register_hooks(self, job_id: str, hooks: List[str]) -> List[str]:
554
+ """Register hooks for a job and detect conflicts.
555
+
556
+ Args:
557
+ job_id: UUID of the job registering hooks.
558
+ hooks: List of hook targets (method names, function signatures).
559
+
560
+ Returns:
561
+ List of conflicting hooks that are already registered by other jobs.
562
+ """
563
+ conflicts = []
564
+ for hook in hooks:
565
+ if hook in self._hook_registry:
566
+ existing_job_id = self._hook_registry[hook]
567
+ if existing_job_id != job_id:
568
+ conflicts.append(hook)
569
+ self.logger.warning(
570
+ f"Hook conflict: '{hook}' already registered by job {existing_job_id[:8]}"
571
+ )
572
+ else:
573
+ self._hook_registry[hook] = job_id
574
+
575
+ if conflicts:
576
+ self.logger.warning(
577
+ f"Job {job_id[:8]} has {len(conflicts)} hook conflict(s)"
578
+ )
579
+
580
+ return conflicts
581
+
582
+ def unregister_hooks(self, job_id: str) -> None:
583
+ """Remove all hooks registered by a job.
584
+
585
+ Args:
586
+ job_id: UUID of the job whose hooks should be removed.
587
+ """
588
+ hooks_to_remove = [
589
+ hook for hook, jid in self._hook_registry.items() if jid == job_id
590
+ ]
591
+ for hook in hooks_to_remove:
592
+ del self._hook_registry[hook]
593
+
594
+ if hooks_to_remove:
595
+ self.logger.debug(
596
+ f"Unregistered {len(hooks_to_remove)} hooks for job {job_id[:8]}"
597
+ )
598
+
599
+ def check_hook_conflicts(self, hooks: List[str]) -> Dict[str, str]:
600
+ """Check for potential conflicts before registering hooks.
601
+
602
+ Args:
603
+ hooks: List of hook targets to check.
604
+
605
+ Returns:
606
+ Dictionary mapping conflicting hooks to their owning job IDs.
607
+ """
608
+ return {
609
+ hook: self._hook_registry[hook]
610
+ for hook in hooks
611
+ if hook in self._hook_registry
612
+ }
613
+
614
+ def get_hook_registry(self) -> Dict[str, str]:
615
+ """Get a copy of the current hook registry.
616
+
617
+ Returns:
618
+ Dictionary mapping hook targets to job IDs.
619
+ """
620
+ return self._hook_registry.copy()
621
+
622
+ # ==================== Session Info Methods ====================
623
+
624
+ def has_active_session(self) -> bool:
625
+ """Check if there's an active Frida session.
626
+
627
+ Returns:
628
+ True if a session is active, False otherwise.
629
+ """
630
+ return self.process_session is not None
631
+
632
+ def get_session_info(self) -> Dict[str, any]:
633
+ """Get current session information for UI display.
634
+
635
+ Returns:
636
+ Dictionary containing session state information.
637
+ """
638
+ return {
639
+ "package": self.package_name,
640
+ "pid": self.pid,
641
+ "mode": self._mode or ("spawn" if self.pid != -1 else "attach"),
642
+ "device_serial": self._device_serial,
643
+ "has_session": self.process_session is not None,
644
+ "job_count": len(self.jobs),
645
+ "running_job_count": len(self.running_jobs()),
646
+ }
647
+
648
+ def get_running_jobs_info(self) -> List[Dict[str, any]]:
649
+ """Get information about running jobs for UI display.
650
+
651
+ Returns:
652
+ List of dictionaries containing job information.
653
+ """
654
+ return [
655
+ job.get_info()
656
+ for job in self.jobs.values()
657
+ if job.state == "running"
658
+ ]
659
+
660
+ def get_all_jobs_info(self) -> List[Dict[str, any]]:
661
+ """Get information about all jobs (running and stopped).
662
+
663
+ Returns:
664
+ List of dictionaries containing job information.
665
+ """
666
+ return [job.get_info() for job in self.jobs.values()]
667
+
668
+ @property
669
+ def mode(self) -> Optional[str]:
670
+ """Get the current session mode ('spawn' or 'attach')."""
671
+ return self._mode
672
+
673
+ def reset_session(self) -> None:
674
+ """Reset the session state for a new connection.
675
+
676
+ Stops all jobs, clears hook registry, and resets session state.
677
+ """
678
+ # Stop all running jobs
679
+ self.stop_jobs()
680
+
681
+ # Clear hook registry
682
+ self._hook_registry.clear()
683
+
684
+ # Detach from app
685
+ if self.process_session:
686
+ try:
687
+ self.process_session.detach()
688
+ except Exception:
689
+ pass
690
+
691
+ # Reset state
692
+ self.process_session = None
693
+ self.pid = -1
694
+ self.package_name = ""
695
+ self._mode = None
696
+ self.is_first_job = True
697
+ self.last_created_job = None
698
+ self.init_last_job = False
699
+
700
+ self.logger.info("[*] Session reset complete")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: AndroidFridaManager
3
- Version: 1.9.3
3
+ Version: 1.9.4
4
4
  Summary: A python API in order to install and run the frida-server on an Android device.
5
5
  Home-page: https://github.com/fkie-cad/AndroidFridaManager
6
6
  Author: Daniel Baier
@@ -33,7 +33,7 @@ Dynamic: requires-dist
33
33
  Dynamic: requires-python
34
34
  Dynamic: summary
35
35
 
36
- ![version](https://img.shields.io/badge/version-1.9.3-blue) [![PyPI version](https://badge.fury.io/py/AndroidFridaManager.svg)](https://badge.fury.io/py/AndroidFridaManager) [![Publish status](https://github.com/fkie-cad/friTap/actions/workflows/publish.yml/badge.svg?branch=main)](https://github.com/fkie-cad/AndroidFridaManager/actions/workflows/publish-to-pypi.yml)
36
+ ![version](https://img.shields.io/badge/version-1.9.4-blue) [![PyPI version](https://badge.fury.io/py/AndroidFridaManager.svg)](https://badge.fury.io/py/AndroidFridaManager) [![Publish status](https://github.com/fkie-cad/friTap/actions/workflows/publish.yml/badge.svg?branch=main)](https://github.com/fkie-cad/AndroidFridaManager/actions/workflows/publish-to-pypi.yml)
37
37
 
38
38
  # AndroidFridaManager
39
39
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: AndroidFridaManager
3
- Version: 1.9.3
3
+ Version: 1.9.4
4
4
  Summary: A python API in order to install and run the frida-server on an Android device.
5
5
  Home-page: https://github.com/fkie-cad/AndroidFridaManager
6
6
  Author: Daniel Baier
@@ -33,7 +33,7 @@ Dynamic: requires-dist
33
33
  Dynamic: requires-python
34
34
  Dynamic: summary
35
35
 
36
- ![version](https://img.shields.io/badge/version-1.9.3-blue) [![PyPI version](https://badge.fury.io/py/AndroidFridaManager.svg)](https://badge.fury.io/py/AndroidFridaManager) [![Publish status](https://github.com/fkie-cad/friTap/actions/workflows/publish.yml/badge.svg?branch=main)](https://github.com/fkie-cad/AndroidFridaManager/actions/workflows/publish-to-pypi.yml)
36
+ ![version](https://img.shields.io/badge/version-1.9.4-blue) [![PyPI version](https://badge.fury.io/py/AndroidFridaManager.svg)](https://badge.fury.io/py/AndroidFridaManager) [![Publish status](https://github.com/fkie-cad/friTap/actions/workflows/publish.yml/badge.svg?branch=main)](https://github.com/fkie-cad/AndroidFridaManager/actions/workflows/publish-to-pypi.yml)
37
37
 
38
38
  # AndroidFridaManager
39
39
 
@@ -1,4 +1,4 @@
1
- ![version](https://img.shields.io/badge/version-1.9.3-blue) [![PyPI version](https://badge.fury.io/py/AndroidFridaManager.svg)](https://badge.fury.io/py/AndroidFridaManager) [![Publish status](https://github.com/fkie-cad/friTap/actions/workflows/publish.yml/badge.svg?branch=main)](https://github.com/fkie-cad/AndroidFridaManager/actions/workflows/publish-to-pypi.yml)
1
+ ![version](https://img.shields.io/badge/version-1.9.4-blue) [![PyPI version](https://badge.fury.io/py/AndroidFridaManager.svg)](https://badge.fury.io/py/AndroidFridaManager) [![Publish status](https://github.com/fkie-cad/friTap/actions/workflows/publish.yml/badge.svg?branch=main)](https://github.com/fkie-cad/AndroidFridaManager/actions/workflows/publish-to-pypi.yml)
2
2
 
3
3
  # AndroidFridaManager
4
4