diff --git a/android_pyapi/__init__.py b/android_pyapi/__init__.py index 5dfe2c6..fb99493 100644 --- a/android_pyapi/__init__.py +++ b/android_pyapi/__init__.py @@ -378,7 +378,7 @@ __all__ = [ "AccessibilityNodeInfo", "AccessibilityService", "AccessibilityServiceInfo", "Account", "AccountManager", - "ActivityManager", + "Activity", "ActivityManager", "AlphaAnimation", "Animation", "Animator", "ValueAnimator", "ObjectAnimator", "AnimatorSet", @@ -512,6 +512,18 @@ __all__ = [ "String", "StringBuilder", "StringBuffer", "Integer", "Math", "System", "Thread", "BigInteger", "BigDecimal", + # Java Exception 体系 + "Throwable", "Exception", "RuntimeException", "Error", + "NullPointerException", "IllegalArgumentException", "IllegalStateException", + "IndexOutOfBoundsException", "ArrayIndexOutOfBoundsException", + "StringIndexOutOfBoundsException", "ClassCastException", + "UnsupportedOperationException", "ArithmeticException", + "NumberFormatException", "SecurityException", + "ConcurrentModificationException", "NegativeArraySizeException", + "NoSuchFieldException", "NoSuchMethodException", "InterruptedException", + "CloneNotSupportedException", "IllegalMonitorStateException", + "IllegalThreadStateException", "EnumConstantNotPresentException", + "TypeNotPresentException", "SimpleDateFormat", "DecimalFormat", "LocalDateTime", "LocalDate", "LocalTime", "Instant", "Duration", "Period", "ChronoUnit", "ChronoField", "DayOfWeek", "Month", "ZoneId", "ZoneOffset", diff --git a/android_pyapi/android/app.py b/android_pyapi/android/app.py index 801a800..b75c043 100644 --- a/android_pyapi/android/app.py +++ b/android_pyapi/android/app.py @@ -1698,6 +1698,129 @@ class Activity(Stub): def onPictureInPictureModeChanged(self, is_in_pip_mode: bool, new_config): pass + # ─── 生命周期 (扩展) ─────────────────────────────────────── + + def onPostCreate(self, savedInstanceState): + pass + + def onPostResume(self): + pass + + def onUserLeaveHint(self): + pass + + def onUserInteraction(self): + pass + + def onCreateContextMenu(self, menu, v, menuInfo): + pass + + def onContextMenuClosed(self, menu): + pass + + def onContextItemSelected(self, item): + return False + + def onWindowFocusChanged(self, hasFocus): + pass + + def onWindowAttributesChanged(self, params): + pass + + def onContentChanged(self): + pass + + def onAttachedToWindow(self): + pass + + def onDetachedFromWindow(self): + pass + + def onTitleChanged(self, title, color): + pass + + # ─── 权限 (扩展) ─────────────────────────────────────────── + + def requestPermissions(self, permissions, requestCode): + pass + + def checkSelfPermission(self, permission): + return 0 # PERMISSION_GRANTED + + def shouldShowRequestPermissionRationale(self, permission): + return False + + # ─── UI (扩展) ───────────────────────────────────────────── + + def getWindow(self): + if not hasattr(self, '_window'): + from .view import Window + self._window = Window() + return self._window + + def getWindowManager(self): + if not hasattr(self, '_window_manager'): + from .view import WindowManager + self._window_manager = WindowManager() + return self._window_manager + + def setRequestedOrientation(self, requestedOrientation): + self._requested_orientation = requestedOrientation + + def getRequestedOrientation(self): + return getattr(self, '_requested_orientation', 0) + + def runOnUiThread(self, action): + import threading + # 在桌面环境直接执行 + action() + + def overridePendingTransition(self, enterAnim, exitAnim): + pass + + def getActionBar(self): + return getattr(self, '_action_bar', None) + + def setActionBar(self, toolbar): + self._action_bar = toolbar + + def getMenuInflater(self): + from .view import MenuInflater + return MenuInflater() + + def onCreateOptionsMenu(self, menu): + return True + + def onPrepareOptionsMenu(self, menu): + return True + + def onOptionsItemSelected(self, item): + return False + + def registerForContextMenu(self, view): + pass + + def unregisterForContextMenu(self, view): + pass + + def openContextMenu(self, view): + pass + + def finishAffinity(self): + self.finish() + + def invalidateOptionsMenu(self): + pass + + def setVisible(self, visible): + self._visible = visible + + def setTheme(self, resid): + self._theme = resid + + def getTheme(self): + return getattr(self, '_theme', 0) + # ═══════════════════════════════════════════════════════════════ # Service diff --git a/android_pyapi/android/content.py b/android_pyapi/android/content.py index 7ec2547..93281a5 100644 --- a/android_pyapi/android/content.py +++ b/android_pyapi/android/content.py @@ -743,6 +743,86 @@ class Context(Stub): def toString(self) -> str: return f"Context{{{self._package_name}}}" + # ─── 文件流 / 权限 (扩展) ───────────────────────────────── + + def openFileInput(self, name): + path = os.path.join(self.getFilesDir(), name) + return open(path, 'rb') + + def openFileOutput(self, name, mode): + path = os.path.join(self.getFilesDir(), name) + os.makedirs(os.path.dirname(path), exist_ok=True) + return open(path, 'wb') + + def getFileStreamPath(self, name): + from ..java.io import File + return File(self.getFilesDir(), name) + + def grantUriPermission(self, toPackage, uri, modeFlags): + pass + + def revokeUriPermission(self, uri, modeFlags): + pass + + def checkUriPermission(self, uri, pid, uid, modeFlags): + return 0 # PERMISSION_GRANTED + + def checkPermission(self, permission, pid, uid): + return 0 # PERMISSION_GRANTED + + def enforceUriPermission(self, uri, pid, uid, modeFlags, message): + pass + + # ─── 资源 / 主题 (扩展) ─────────────────────────────────── + + def getApplicationInfo(self): + from .content_pm import ApplicationInfo + return ApplicationInfo() + + def getTheme(self): + return getattr(self, '_theme', 0) + + def setTheme(self, resid): + self._theme = resid + + def getText(self, resId): + return str(resId) + + def getQuantityString(self, resId, quantity, *formatArgs): + return str(resId) + + def getStringArray(self, resId): + return [] + + def getTextArray(self, resId): + return [] + + def getIntArray(self, resId): + return [] + + # ─── Context 派生 / 回调 (扩展) ─────────────────────────── + + def createConfigurationContext(self, overrideConfiguration): + return Context() + + def createDisplayContext(self, display): + return Context() + + def registerComponentCallbacks(self, callback): + pass + + def unregisterComponentCallbacks(self, callback): + pass + + def getExternalStoragePublicDirectory(self, type_): + from ..java.io import File + from .os import Environment + return Environment.getExternalStoragePublicDirectory(type_) + + def getDisplayMetrics(self): + from .util import DisplayMetrics + return DisplayMetrics() + class ContextWrapper(Context): """ContextWrapper — Context 的包装器""" diff --git a/android_pyapi/android/graphics.py b/android_pyapi/android/graphics.py index 9ef733e..caa9eb2 100644 --- a/android_pyapi/android/graphics.py +++ b/android_pyapi/android/graphics.py @@ -2404,8 +2404,25 @@ class Canvas(Stub): ALL_SAVE_FLAG = 0x1F - def __init__(self, width: int = 0, height: int = 0): + def __init__(self, width=0, height=0): super().__init__() + # 支持 Canvas(Bitmap) 重载 + if isinstance(width, Bitmap): + bitmap = width + self._bitmap = bitmap + self._width = bitmap.getWidth() + self._height = bitmap.getHeight() + if HAS_PIL: + from PIL import Image, ImageDraw + self._image = bitmap._image if hasattr(bitmap, '_image') and bitmap._image else None + self._draw = ImageDraw.Draw(self._image) if self._image else None + else: + self._pixels = None + self._save_count = 0 + self._matrix_stack = [Matrix()] + self._clip_stack = [Rect(0, 0, self._width, self._height)] + self._matrix = Matrix() + return if HAS_PIL: from PIL import Image, ImageDraw self._width = width @@ -2685,21 +2702,43 @@ class Canvas(Stub): except Exception: pass - def drawBitmap(self, bitmap: Bitmap, left: float, top: float, paint: Paint = None): - if HAS_PIL and self._draw is not None and hasattr(bitmap, "_image") and bitmap._image: - self._image.paste(bitmap._image, (int(left), int(top))) - elif HAS_PIL and self._image is not None: - pass - - def drawBitmap(self, bitmap: Bitmap, src: Rect, dst: RectF, paint: Paint = None): - if HAS_PIL and self._image is not None and hasattr(bitmap, "_image") and bitmap._image: - if src: - cropped = bitmap._image.crop((src.left, src.top, src.right, src.bottom)) - resized = cropped.resize((int(dst.width()), int(dst.height()))) - self._image.paste(resized, (int(dst.left), int(dst.top))) + def drawBitmap(self, *args): + """drawBitmap 重载: (bitmap,left,top,paint) / (bitmap,src,dst,paint) / (bitmap,matrix,paint)""" + if len(args) == 4: + bitmap, a, b, paint = args + if isinstance(a, (int, float)) and isinstance(b, (int, float)): + if HAS_PIL and self._draw is not None and hasattr(bitmap, "_image") and bitmap._image: + self._image.paste(bitmap._image, (int(a), int(b))) + elif HAS_PIL and self._image is not None: + pass else: - resized = bitmap._image.resize((int(dst.width()), int(dst.height()))) - self._image.paste(resized, (int(dst.left), int(dst.top))) + src, dst = a, b + if HAS_PIL and self._image is not None and hasattr(bitmap, "_image") and bitmap._image: + if src: + cropped = bitmap._image.crop((src.left, src.top, src.right, src.bottom)) + resized = cropped.resize((int(dst.width()), int(dst.height()))) + self._image.paste(resized, (int(dst.left), int(dst.top))) + else: + resized = bitmap._image.resize((int(dst.width()), int(dst.height()))) + self._image.paste(resized, (int(dst.left), int(dst.top))) + elif len(args) == 3: + bitmap, matrix, paint = args + if HAS_PIL and self._image is not None and hasattr(bitmap, "_image") and bitmap._image: + try: + from PIL import Image + inv = Matrix() + if hasattr(matrix, "invert") and matrix.invert(inv): + v = inv.getValues() + elif hasattr(matrix, "getValues"): + v = matrix.getValues() + else: + v = [1, 0, 0, 0, 1, 0, 0, 0, 1] + transformed = bitmap._image.transform( + bitmap._image.size, Image.AFFINE, + (v[0], v[1], v[2], v[3], v[4], v[5])) + self._image.paste(transformed, (0, 0)) + except Exception: + pass def drawText(self, text: str, x: float, y: float, paint: Paint): if HAS_PIL and self._draw is not None: diff --git a/android_pyapi/android/os.py b/android_pyapi/android/os.py index 0eb3e02..ad67913 100644 --- a/android_pyapi/android/os.py +++ b/android_pyapi/android/os.py @@ -466,6 +466,61 @@ class Bundle(BaseBundle): def putIntegerArrayList(self, key: str, value: Optional[List[int]]): self._map[key] = value + # ---- 数组类型 ---- + def putIntArray(self, key: str, value: Optional[List[int]]): + self._map[key] = list(value) if value is not None else None + + def getIntArray(self, key: str) -> Optional[List[int]]: + v = self._map.get(key) + if isinstance(v, list): + return [int(x) for x in v] + return None + + def putStringArray(self, key: str, value: Optional[List[str]]): + self._map[key] = list(value) if value is not None else None + + def getStringArray(self, key: str) -> Optional[List[str]]: + v = self._map.get(key) + if isinstance(v, list): + return [str(x) for x in v] + return None + + def putLongArray(self, key: str, value: Optional[List[int]]): + self._map[key] = list(value) if value is not None else None + + def getLongArray(self, key: str) -> Optional[List[int]]: + v = self._map.get(key) + if isinstance(v, list): + return [int(x) for x in v] + return None + + def putBooleanArray(self, key: str, value: Optional[List[bool]]): + self._map[key] = list(value) if value is not None else None + + def getBooleanArray(self, key: str) -> Optional[List[bool]]: + v = self._map.get(key) + if isinstance(v, list): + return [bool(x) for x in v] + return None + + def putDoubleArray(self, key: str, value: Optional[List[float]]): + self._map[key] = list(value) if value is not None else None + + def getDoubleArray(self, key: str) -> Optional[List[float]]: + v = self._map.get(key) + if isinstance(v, list): + return [float(x) for x in v] + return None + + def putByteArray(self, key: str, value: Optional[List[int]]): + self._map[key] = list(value) if value is not None else None + + def getByteArray(self, key: str) -> Optional[List[int]]: + v = self._map.get(key) + if isinstance(v, list): + return [int(x) & 0xFF for x in v] + return None + def putAll(self, bundle: "Bundle"): if bundle: self._map.update(bundle._map) @@ -729,6 +784,14 @@ class Message(Stub): def setTarget(self, handler: "Handler"): self._target = handler + @property + def target(self) -> Optional["Handler"]: + return self._target + + @target.setter + def target(self, handler: "Handler"): + self._target = handler + def getCallback(self) -> Optional[Callable]: return self._callback @@ -769,15 +832,17 @@ class Looper(Stub): _main_looper: Optional["Looper"] = None _thread_local = threading.local() - def __init__(self): + def __init__(self, quit_allowed: bool = True): super().__init__() self._thread = threading.current_thread() self._running = False self._message_queue: List[Message] = [] - self._quit_allowed = True + self._queue_lock = threading.Lock() + self._quit_allowed = quit_allowed @staticmethod def prepare(): + """为当前线程准备一个 Looper""" looper = Looper() Looper._thread_local.looper = looper return looper @@ -799,16 +864,76 @@ class Looper(Stub): @staticmethod def loop(): + """运行消息循环,处理到期的消息""" looper = Looper.myLooper() if looper is None: raise RuntimeError("No Looper prepared") looper._running = True + while looper._running: + msg = looper._next() + if msg is not None: + target = msg._target + if target is not None: + target.dispatchMessage(msg) + msg.recycle() + else: + # 无到期消息,短暂 sleep 避免忙等 + _time.sleep(0.001) + + def _next(self) -> Optional[Message]: + """非阻塞地取出一条到期消息(when <= now)""" + with self._queue_lock: + now = SystemClock.uptimeMillis() + for i, msg in enumerate(self._message_queue): + if msg.when <= now: + return self._message_queue.pop(i) + return None + + def _enqueue(self, msg: Message): + """将消息按 when 时间顺序入队""" + with self._queue_lock: + inserted = False + for i, m in enumerate(self._message_queue): + if msg.when < m.when: + self._message_queue.insert(i, msg) + inserted = True + break + if not inserted: + self._message_queue.append(msg) + + def _remove_callbacks(self, callback): + """从队列中移除匹配 callback 的消息""" + with self._queue_lock: + self._message_queue = [ + m for m in self._message_queue + if m._callback is not callback + ] + + def _remove_messages(self, what: int): + """从队列中移除匹配 what 的消息""" + with self._queue_lock: + self._message_queue = [ + m for m in self._message_queue + if m.what != what + ] + + def _has_messages(self, what: int) -> bool: + """检查队列中是否有匹配 what 的消息""" + with self._queue_lock: + return any(m.what == what for m in self._message_queue) def quit(self): - self._running = False + """退出消息循环""" + if not self._quit_allowed: + raise RuntimeError("Main Looper is not allowed to quit") + with self._queue_lock: + self._running = False + self._message_queue.clear() def quitSafely(self): - self._running = False + """安全退出,仅停止循环(已入队消息不再处理)""" + with self._queue_lock: + self._running = False def isCurrentThread(self) -> bool: return threading.current_thread() == self._thread @@ -858,7 +983,6 @@ class Handler(Stub): def postDelayed(self, r: Callable, delay_ms: int) -> bool: msg = Message.obtain(self) msg._callback = r - msg.when = int(_time.time() * 1000) + delay_ms return self.sendMessageDelayed(msg, delay_ms) def postAtTime(self, r: Callable, uptime_ms: int) -> bool: @@ -869,6 +993,11 @@ class Handler(Stub): def sendMessage(self, msg: Message) -> bool: msg._target = self + looper = self._looper + if looper is not None and looper._running: + looper._enqueue(msg) + return True + # 当前线程无运行中的 Looper,回退到同步执行(保持兼容性) self.dispatchMessage(msg) return True @@ -878,31 +1007,53 @@ class Handler(Stub): def sendMessageDelayed(self, msg: Message, delay_ms: int) -> bool: msg._target = self - msg.when = int(_time.time() * 1000) + delay_ms + msg.when = SystemClock.uptimeMillis() + delay_ms + looper = self._looper + if looper is not None and looper._running: + looper._enqueue(msg) + return True + # 回退到同步执行 self.dispatchMessage(msg) return True def sendMessageAtTime(self, msg: Message, uptime_ms: int) -> bool: msg._target = self msg.when = uptime_ms + looper = self._looper + if looper is not None and looper._running: + looper._enqueue(msg) + return True self.dispatchMessage(msg) return True def sendMessageAtFrontOfQueue(self, msg: Message) -> bool: msg._target = self + msg.when = 0 + looper = self._looper + if looper is not None and looper._running: + with looper._queue_lock: + looper._message_queue.insert(0, msg) + return True self.dispatchMessage(msg) return True def removeCallbacks(self, r: Callable): - pass + looper = self._looper + if looper is not None: + looper._remove_callbacks(r) def removeMessages(self, what: int): - pass + looper = self._looper + if looper is not None: + looper._remove_messages(what) def obtainMessage(self, what: int = 0) -> Message: return Message.obtain(self, what) def hasMessages(self, what: int) -> bool: + looper = self._looper + if looper is not None: + return looper._has_messages(what) return False def getLooper(self) -> Looper: @@ -950,6 +1101,42 @@ class HandlerThread(threading.Thread, Stub): return self.quit() +# ═══════════════════════════════════════════════════════════════ +# AsyncTask Executor (守护线程池) +# ═══════════════════════════════════════════════════════════════ + +class _AsyncTaskExecutor: + """守护线程池执行器 — 用于 AsyncTask 后台执行,不阻塞程序退出。 + + 使用 BoundedSemaphore 限制并发数,每个任务在独立的守护线程中运行。 + """ + + def __init__(self, max_workers: int = 4): + self._max_workers = max_workers + self._semaphore = threading.BoundedSemaphore(max_workers) + self._shutdown_flag = False + + def submit(self, fn, *args, **kwargs): + if self._shutdown_flag: + raise RuntimeError("Executor has been shut down") + self._semaphore.acquire() + + def _run(): + try: + fn(*args, **kwargs) + finally: + self._semaphore.release() + + t = threading.Thread(target=_run, daemon=True) + t.start() + + def execute(self, command): + self.submit(command) + + def shutdown(self, wait: bool = True): + self._shutdown_flag = True + + # ═══════════════════════════════════════════════════════════════ # AsyncTask # ═══════════════════════════════════════════════════════════════ @@ -967,6 +1154,7 @@ class AsyncTask(Stub): self._status = AsyncTask.Status.PENDING self._result: Any = None self._cancelled = False + self._handler = Handler(Looper.getMainLooper()) def doInBackground(self, *params) -> Any: raise NotImplementedError @@ -984,19 +1172,25 @@ class AsyncTask(Stub): pass def execute(self, *params) -> "AsyncTask": + return self.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, *params) + + def executeOnExecutor(self, exec: Any, *params) -> "AsyncTask": self._status = AsyncTask.Status.RUNNING self.onPreExecute() + exec.submit(self._run_background, params) + return self + + def _run_background(self, params): + """在后台线程中执行 doInBackground,完成后通过 Handler 回调主线程""" try: self._result = self.doInBackground(*params) except Exception as e: self._result = e self._status = AsyncTask.Status.FINISHED - if not self._cancelled: - self.onPostExecute(self._result) - return self - - def executeOnExecutor(self, exec: Any, *params) -> "AsyncTask": - return self.execute(*params) + if self._cancelled: + self._handler.post(lambda: self.onCancelled(self._result)) + else: + self._handler.post(lambda: self.onPostExecute(self._result)) def get(self) -> Any: return self._result @@ -1012,7 +1206,12 @@ class AsyncTask(Stub): return True def publishProgress(self, *values): - self.onProgressUpdate(*values) + self._handler.post(lambda: self.onProgressUpdate(*values)) + + +# 初始化默认执行器 +AsyncTask.THREAD_POOL_EXECUTOR = _AsyncTaskExecutor(max_workers=4) +AsyncTask.SERIAL_EXECUTOR = _AsyncTaskExecutor(max_workers=1) # ═══════════════════════════════════════════════════════════════ diff --git a/android_pyapi/android/view.py b/android_pyapi/android/view.py index 42df7fd..2e127cc 100644 --- a/android_pyapi/android/view.py +++ b/android_pyapi/android/view.py @@ -777,6 +777,155 @@ class View(Stub): def inflate(context, resource, root): return View(context) + # ─── 事件处理 (扩展) ─────────────────────────────────────── + + def onGenericMotionEvent(self, event) -> bool: + return False + + def onHoverEvent(self, event) -> bool: + return False + + def onDragEvent(self, event) -> bool: + return False + + def onSizeChanged(self, w, h, oldw, oldh): + pass + + def onFocusChanged(self, gainFocus, direction, previouslyFocusedRect): + pass + + def onScrollChanged(self, l, t, oldl, oldt): + pass + + def onVisibilityChanged(self, changedView, visibility): + pass + + # ─── 监听器设置 (扩展) ───────────────────────────────────── + + def setOnGenericMotionListener(self, listener): + self._on_generic_motion_listener = listener + return self + + def setOnHoverListener(self, listener): + self._on_hover_listener = listener + return self + + def setOnDragListener(self, listener): + self._on_drag_listener = listener + return self + + def setOnFocusChangeListener(self, listener): + self._on_focus_change_listener = listener + return self + + def setOnContextClickListener(self, listener): + self._on_context_click_listener = listener + return self + + def addOnLayoutChangeListener(self, listener): + if not hasattr(self, '_layout_change_listeners'): + self._layout_change_listeners = [] + self._layout_change_listeners.append(listener) + + def removeOnLayoutChangeListener(self, listener): + if hasattr(self, '_layout_change_listeners'): + try: + self._layout_change_listeners.remove(listener) + except ValueError: + pass + + def setOnApplyWindowInsetsListener(self, listener): + self._on_apply_window_insets_listener = listener + return self + + # ─── 位置和尺寸 (扩展) ───────────────────────────────────── + + def getLocationOnScreen(self, location): + location[0] = self._left if hasattr(self, '_left') else 0 + location[1] = self._top if hasattr(self, '_top') else 0 + + def getLocationInWindow(self, location): + self.getLocationOnScreen(location) + + def offsetLeftAndRight(self, offset): + if hasattr(self, '_left'): + self._left += offset + + def offsetTopAndBottom(self, offset): + if hasattr(self, '_top'): + self._top += offset + + def getHitRect(self, out_rect): + out_rect.left = self._left if hasattr(self, '_left') else 0 + out_rect.top = self._top if hasattr(self, '_top') else 0 + out_rect.right = out_rect.left + (self._width if hasattr(self, '_width') else 0) + out_rect.bottom = out_rect.top + (self._height if hasattr(self, '_height') else 0) + + def getDrawingRect(self, out_rect): + out_rect.left = 0 + out_rect.top = 0 + out_rect.right = self._width if hasattr(self, '_width') else 0 + out_rect.bottom = self._height if hasattr(self, '_height') else 0 + + # ─── 其他扩展方法 ────────────────────────────────────────── + + def setTooltipText(self, text): + self._tooltip_text = text + + def getTooltipText(self): + return getattr(self, '_tooltip_text', None) + + def setTranslationZ(self, translationZ): + self._translation_z = translationZ + + def getTranslationZ(self): + return getattr(self, '_translation_z', 0.0) + + def setZ(self, z): + self._z = z + + def getZ(self): + return getattr(self, '_z', 0.0) + + def setTransitionName(self, name): + self._transition_name = name + + def getTransitionName(self): + return getattr(self, '_transition_name', None) + + def setClipToOutline(self, clipToOutline): + self._clip_to_outline = clipToOutline + + def getClipToOutline(self): + return getattr(self, '_clip_to_outline', False) + + def setFitsSystemWindows(self, fitSystemWindows): + self._fits_system_windows = fitSystemWindows + + def getFitsSystemWindows(self): + return getattr(self, '_fits_system_windows', False) + + def playSoundEffect(self, soundConstant): + pass + + def setSaveEnabled(self, enabled): + self._save_enabled = enabled + + def isSaveEnabled(self): + return getattr(self, '_save_enabled', True) + + def setSystemUiVisibility(self, visibility): + self._system_ui_visibility = visibility + + def getSystemUiVisibility(self): + return getattr(self, '_system_ui_visibility', 0) + + def requestApplyInsets(self): + pass + + def onApplyWindowInsets(self, insets): + return insets + class MeasureSpec: """MeasureSpec — 测量规格包装""" @@ -2646,6 +2795,24 @@ class SurfaceTexture(Stub): mtx[i] = 1.0 if i % 5 == 0 else 0.0 +# ═══════════════════════════════════════════════════════════════ +# MenuInflater +# ═══════════════════════════════════════════════════════════════ + +class MenuInflater(Stub): + """MenuInflater — 菜单填充器(桌面环境桩实现)""" + + def __init__(self, context=None): + super().__init__() + self._context = context + + def inflate(self, menu_res: int, menu: Any): + return menu + + def toString(self) -> str: + return f"MenuInflater@{hex(id(self))}" + + # ═══════════════════════════════════════════════════════════════ # Window # ═══════════════════════════════════════════════════════════════ diff --git a/android_pyapi/java/io.py b/android_pyapi/java/io.py index c757108..5100db0 100644 --- a/android_pyapi/java/io.py +++ b/android_pyapi/java/io.py @@ -416,9 +416,13 @@ class File(Stub): except OSError: return 0 - def compareTo(self, other): + def compareTo(self, other) -> int: if isinstance(other, File): - return self._path.__cmp__(other._path) + if self._path < other._path: + return -1 + elif self._path > other._path: + return 1 + return 0 return -1 def equals(self, other): diff --git a/android_pyapi/java/lang.py b/android_pyapi/java/lang.py index 59d746e..ecaad1e 100644 --- a/android_pyapi/java/lang.py +++ b/android_pyapi/java/lang.py @@ -18,6 +18,16 @@ from datetime import datetime as _datetime __all__ = [ "Object", "Throwable", "Exception", "RuntimeException", "Error", + "NullPointerException", "IllegalArgumentException", "IllegalStateException", + "IndexOutOfBoundsException", "ArrayIndexOutOfBoundsException", + "StringIndexOutOfBoundsException", "ClassCastException", + "UnsupportedOperationException", "ArithmeticException", + "NumberFormatException", "SecurityException", + "ConcurrentModificationException", "NegativeArraySizeException", + "NoSuchFieldException", "NoSuchMethodException", "InterruptedException", + "CloneNotSupportedException", "IllegalMonitorStateException", + "IllegalThreadStateException", "EnumConstantNotPresentException", + "TypeNotPresentException", "String", "Integer", "Long", "Short", "Byte", "Float", "Double", "Boolean", "Character", "Math", "StrictMath", "System", "Thread", "ThreadLocal", "StringBuilder", "StringBuffer", @@ -148,6 +158,75 @@ class Error(Throwable): pass +class NullPointerException(RuntimeException): + pass + +class IllegalArgumentException(RuntimeException): + pass + +class IllegalStateException(RuntimeException): + pass + +class IndexOutOfBoundsException(RuntimeException): + pass + +class ArrayIndexOutOfBoundsException(IndexOutOfBoundsException): + pass + +class StringIndexOutOfBoundsException(IndexOutOfBoundsException): + pass + +class ClassCastException(RuntimeException): + pass + +class UnsupportedOperationException(RuntimeException): + pass + +class ArithmeticException(RuntimeException): + pass + +class NumberFormatException(IllegalArgumentException): + pass + +class SecurityException(RuntimeException): + pass + +class ConcurrentModificationException(RuntimeException): + pass + +class NegativeArraySizeException(RuntimeException): + pass + +class NoSuchFieldException(Exception): + pass + +class NoSuchMethodException(Exception): + pass + +class InterruptedException(Exception): + pass + +class CloneNotSupportedException(Exception): + pass + +class IllegalMonitorStateException(RuntimeException): + pass + +class IllegalThreadStateException(IllegalArgumentException): + pass + +class EnumConstantNotPresentException(RuntimeException): + def __init__(self, e, constant): + super().__init__(f"{e}.{constant}") + self.enumType = e + self.constantName = constant + +class TypeNotPresentException(RuntimeException): + def __init__(self, typeName, cause=None): + super().__init__(f"Type {typeName} not present", cause) + self.typeName = typeName + + # ============================================================ # StackTraceElement # ============================================================ @@ -277,11 +356,10 @@ class String(CharSequence): def getBytes(self, charset="UTF-8"): return self._str.encode(charset, errors="replace") - def hashCode(self): + def hashCode(self) -> int: h = 0 - for c in self._str: - h = (31 * h + ord(c)) & 0xFFFFFFFF - return h if h < 0x80000000 else h - 0x100000000 + for i in range(len(self._str)): + h = 31 * h + ord(self._str[i]) return h & 0x7FFFFFFF def indexOf(self, ch, fromIndex=0): @@ -362,6 +440,89 @@ class String(CharSequence): def trim(self): return String(self._str.strip()) + def strip(self): + return String(self._str.strip()) + + def stripLeading(self): + return String(self._str.lstrip()) + + def stripTrailing(self): + return String(self._str.rstrip()) + + def isBlank(self): + return self._str.strip() == "" + + def repeat(self, count): + return String(self._str * count) + + def lines(self): + from .util import ArrayList + result = ArrayList() + for line in self._str.splitlines(): + result.add(String(line)) + return result + + def subSequence(self, start, end): + return self.substring(start, end) + + def getChars(self, srcBegin, srcEnd, dst, dstBegin): + chars = self._str[srcBegin:srcEnd] + for i, c in enumerate(chars): + dst[dstBegin + i] = c + + def chars(self): + return [ord(c) for c in self._str] + + def formatted(self, *args): + return String(self._str.format(*args)) + + def translateEscapes(self): + result = [] + s = self._str + i = 0 + n = len(s) + while i < n: + c = s[i] + if c == '\\' and i + 1 < n: + nxt = s[i + 1] + if nxt == 'b': + result.append('\b'); i += 2 + elif nxt == 't': + result.append('\t'); i += 2 + elif nxt == 'n': + result.append('\n'); i += 2 + elif nxt == 'f': + result.append('\f'); i += 2 + elif nxt == 'r': + result.append('\r'); i += 2 + elif nxt == 's': + result.append(' '); i += 2 + elif nxt == '"': + result.append('"'); i += 2 + elif nxt == "'": + result.append("'"); i += 2 + elif nxt == '\\': + result.append('\\'); i += 2 + elif nxt in '01234567': + octal = nxt + i += 2 + max_digits = 2 if nxt in '0123' else 1 + while len(octal) < max_digits + 1 and i < n and s[i] in '01234567': + octal += s[i] + i += 1 + result.append(chr(int(octal, 8))) + elif nxt == '\n': + i += 2 + elif nxt == '\r': + i += 2 + if i < n and s[i] == '\n': + i += 1 + else: + result.append(nxt); i += 2 + else: + result.append(c); i += 1 + return String(''.join(result)) + @staticmethod def valueOf(obj): return String(str(obj)) @@ -1200,7 +1361,8 @@ class Math: @staticmethod def floor(a): return int(math.floor(a)) @staticmethod - def round(a): return round(a) + def round(a: float) -> int: + return int(math.floor(a + 0.5)) @staticmethod def min(a, b): return a if a < b else b @staticmethod @@ -1266,8 +1428,58 @@ class StrictMath(Math): pass +class _Console: + """java.io.Console 简化实现,供 System.console() 使用""" + def readLine(self, fmt=None, *args): + if fmt is not None: + try: + _sys.stdout.write(str(fmt) % args if args else str(fmt)) + except Exception: + _sys.stdout.write(str(fmt)) + _sys.stdout.flush() + line = _sys.stdin.readline() + if line == "": + return None + return line.rstrip("\r\n") + + def readPassword(self, fmt=None, *args): + import getpass + if fmt is not None: + try: + _sys.stderr.write(str(fmt) % args if args else str(fmt)) + except Exception: + _sys.stderr.write(str(fmt)) + _sys.stderr.flush() + pwd = getpass.getpass("") + return list(pwd) if pwd is not None else None + + def printf(self, fmt, *args): + try: + _sys.stdout.write(str(fmt) % args if args else str(fmt)) + except Exception: + _sys.stdout.write(str(fmt)) + return self + + def format(self, fmt, *args): + try: + _sys.stdout.write(str(fmt) % args if args else str(fmt)) + except Exception: + _sys.stdout.write(str(fmt)) + return self + + def flush(self): + _sys.stdout.flush() + + def writer(self): + return _sys.stdout + + def reader(self): + return _sys.stdin + + class System: """java.lang.System""" + _console = None @staticmethod def currentTimeMillis(): return int(time.time() * 1000) @staticmethod @@ -1294,6 +1506,28 @@ class System: os.environ[key] = value return value @staticmethod + def clearProperty(key): + return _os.environ.pop(key, None) + @staticmethod + def getenv(name=None): + if name is None: + return dict(_os.environ) + return _os.environ.get(name) + @staticmethod + def console(): + if System._console is None: + System._console = _Console() + return System._console + @staticmethod + def setIn(in_stream): + System.in_ = in_stream + @staticmethod + def setOut(out_stream): + System.out = out_stream + @staticmethod + def setErr(err_stream): + System.err = err_stream + @staticmethod def lineSeparator(): return _os.linesep in_ = _sys.stdin out = _sys.stdout @@ -1306,6 +1540,18 @@ class Thread(Runnable): NORM_PRIORITY = 5 MAX_PRIORITY = 10 + # Thread.State 常量(java.lang.Thread.State) + NEW = "NEW" + RUNNABLE = "RUNNABLE" + BLOCKED = "BLOCKED" + WAITING = "WAITING" + TIMED_WAITING = "TIMED_WAITING" + TERMINATED = "TERMINATED" + + # 按 Python 线程 ident 缓存 Thread 对象,供 currentThread() 复用 + _thread_cache = {} + _default_uncaught_exception_handler = None + def __init__(self, target=None, name=None): super().__init__() self._target = target @@ -1313,25 +1559,91 @@ class Thread(Runnable): self._alive = False self._daemon = False self._interrupted = False + self._priority = Thread.NORM_PRIORITY + self._state = Thread.NEW + self._py_thread = None + self._id = _threading.get_ident() def start(self): self._alive = True - if self._target: - _threading.Thread(target=self.run, daemon=self._daemon).start() + self._state = Thread.RUNNABLE + self._py_thread = _threading.Thread( + target=self._run_wrapper, daemon=self._daemon, name=self._name + ) + self._py_thread.start() + self._id = self._py_thread.ident + + def _run_wrapper(self): + tid = _threading.get_ident() + self._id = tid + Thread._thread_cache[tid] = self + try: + self.run() + except (SystemExit, KeyboardInterrupt): + raise + except BaseException as e: + # 注意:模块内 Exception 已被 java.lang.Exception 覆盖, + # 这里用 BaseException 捕获 Python 原生异常并转发给未捕获异常处理器 + handler = Thread._default_uncaught_exception_handler + if handler is not None: + try: + handler.uncaughtException(self, e) + except BaseException: + pass + finally: + self._alive = False + self._state = Thread.TERMINATED + Thread._thread_cache.pop(tid, None) def run(self): if self._target: self._target() + def join(self, millis=0): + if self._py_thread is not None: + if millis and millis > 0: + self._py_thread.join(millis / 1000.0) + else: + self._py_thread.join() + def interrupt(self): self._interrupted = True def isInterrupted(self): return self._interrupted + @staticmethod + def interrupted(): + tid = _threading.get_ident() + t = Thread._thread_cache.get(tid) + if t is not None: + status = t._interrupted + t._interrupted = False + return status + return False + def isAlive(self): + if self._py_thread is not None: + return self._py_thread.is_alive() return self._alive + def getState(self): + if self._py_thread is not None: + if self._py_thread.is_alive(): + return Thread.RUNNABLE + return Thread.TERMINATED + return self._state + + def getPriority(self): + return self._priority + + def setPriority(self, priority): + if Thread.MIN_PRIORITY <= priority <= Thread.MAX_PRIORITY: + self._priority = priority + + def getId(self): + return self._id + def getName(self): return self._name def setName(self, n): self._name = n def setDaemon(self, d): self._daemon = d @@ -1339,15 +1651,37 @@ class Thread(Runnable): @staticmethod def currentThread(): - return Thread(name="main") + tid = _threading.get_ident() + t = Thread._thread_cache.get(tid) + if t is not None: + return t + # 未缓存的线程(如主线程):创建并缓存一个代表对象 + try: + is_main = (tid == _threading.main_thread().ident) + except Exception: + is_main = False + t = Thread(name="main" if is_main else f"Thread-{tid}") + t._state = Thread.RUNNABLE + t._alive = True + t._id = tid + Thread._thread_cache[tid] = t + return t + @staticmethod def sleep(millis): - import time - time.sleep(millis / 1000.0) + _time.sleep(millis / 1000.0) + @staticmethod def yield_(): - import time - time.sleep(0) + _time.sleep(0) + + @staticmethod + def setDefaultUncaughtExceptionHandler(handler): + Thread._default_uncaught_exception_handler = handler + + @staticmethod + def getDefaultUncaughtExceptionHandler(): + return Thread._default_uncaught_exception_handler class ThreadLocal(Stub): diff --git a/android_pyapi/java/util.py b/android_pyapi/java/util.py index 5273186..71bfeae 100644 --- a/android_pyapi/java/util.py +++ b/android_pyapi/java/util.py @@ -666,13 +666,20 @@ class HashMap(Stub): return None return self._data[key] - def remove(self, key, value=None) -> bool: - if value is None: - return self._data.pop(key, None) is not None or key in self._data - if key in self._data and self._data[key] == value: - del self._data[key] - return True - return False + def remove(self, key, value=None): + if value is not None: + # remove(key, value) — 返回 boolean + if key in self._data and self._data[key] == value: + del self._data[key] + return True + return False + else: + # remove(key) — 返回被删除的 value(与 Java 语义一致) + if key in self._data: + old = self._data[key] + del self._data[key] + return old + return None def replace(self, key, old_value=None, new_value=None): if old_value is None and new_value is None: