From 39b1da37c93d67af466f4a5ffced3695059cafa7 Mon Sep 17 00:00:00 2001 From: shuiyu1123 Date: Wed, 24 Jun 2026 23:46:36 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=85=A8=E9=9D=A2=E5=AE=8C=E5=96=84API?= =?UTF-8?q?=20=E2=80=94=20=E8=A1=A5=E5=85=A8=E6=A0=B8=E5=BF=83=E7=B1=BB?= =?UTF-8?q?=E6=96=B9=E6=B3=95=E3=80=81=E5=AD=98=E6=A0=B9=E8=BD=AC=E7=9C=9F?= =?UTF-8?q?=E5=AE=9E=E5=AE=9E=E7=8E=B0=E3=80=81Exception=E4=BD=93=E7=B3=BB?= =?UTF-8?q?=E6=89=A9=E5=B1=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 核心类方法补全: - Activity: +20方法 (requestWindowFeature, setTitle/getTitle, onKeyDown/onKeyUp, enterPictureInPictureMode, getReferrer, isTaskRoot, navigateUpTo等) - Fragment: +15方法 (getChildFragmentManager, getParentFragment, requireContext/requireActivity, setMenuVisibility等) - Intent: +20方法 (getParcelableExtra, setComponent/getComponent, getDataString, getScheme, putExtras等) - View: +20方法 (setLabelFor, setRotationX/Y, setForeground, setBackgroundTintList等) - KeyEvent: +7方法 (getCharacters, isSystem, isPrintingKey, isModifierKey, startTracking等) - Paint: +12方法 (setStrokeCap/Join, getTextBounds, setBlendMode, setLetterSpacing等) - Notification.Builder: +10方法 (setProgress, setShowWhen, setOnlyAlertOnce, setSound等) 存根转真实实现: - TrafficStats: 读取 /proc/net/dev 返回真实网络流量 - RecyclerView.Adapter: 观察者模式触发通知 - BaseAdapter: 观察者模式触发通知 - ExifInterface: 使用PIL实现图片翻转/旋转 - Process: 使用os.kill实现进程信号 - FileUtils: 真实文件I/O - SharedPreferences: 监听器模式 - TelephonyManager.listen: 维护监听器列表 - ScrollView.smoothScrollTo: 更新滚动位置 Exception体系扩展: - 新增15个Exception/Error类 (ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchElementException, AssertionError, OutOfMemoryError, StackOverflowError, NoClassDefFoundError, UnsatisfiedLinkError, LinkageError, VerifyError, AbstractMethodError, IncompatibleClassChangeError, ExceptionInInitializerError) Math/System/Thread/StringBuilder/Integer增强: - Math: +20方法 (subtractExact, multiplyExact, toIntExact, floorDiv/floorMod, fma等) - System: getProperties/setProperties/mapLibraryName/getLogger - Thread: +10方法 (getUncaughtExceptionHandler, getAllStackTraces, getStackTrace, holdLock等) - StringBuilder: +7方法 (ensureCapacity, trimToSize, setLength, subSequence等) - Integer: +8静态方法 (compare, compareUnsigned, divideUnsigned, sum, max, min等) 修复: - app_fragment.py: 使用java.lang.IllegalStateException替代本地定义 - content_intent.py: Intent.setData/getScheme正确从Uri提取scheme - view_ext.py: KeyEvent继承view.KeyEvent获取所有KEYCODE常量 - __init__.py: 补全Fragment/KeyEvent/MotionEvent/TrafficStats/RecyclerView等导出 统计: 221模块, 3660类, 30826方法, 180217行 --- android_pyapi/__init__.py | 18 ++ android_pyapi/android/app.py | 339 +++++++++++++++++++++++ android_pyapi/android/app_fragment.py | 9 +- android_pyapi/android/content.py | 16 +- android_pyapi/android/content_intent.py | 26 +- android_pyapi/android/graphics.py | 63 +++++ android_pyapi/android/media.py | 26 +- android_pyapi/android/net.py | 99 ++++--- android_pyapi/android/os.py | 71 ++++- android_pyapi/android/telephony.py | 6 +- android_pyapi/android/view.py | 140 +++++++++- android_pyapi/android/view_ext.py | 5 +- android_pyapi/android/widget.py | 90 ++++-- android_pyapi/java/io.py | 42 ++- android_pyapi/java/lang.py | 346 +++++++++++++++++++++++- android_pyapi/java/util.py | 85 ++++++ 16 files changed, 1274 insertions(+), 107 deletions(-) diff --git a/android_pyapi/__init__.py b/android_pyapi/__init__.py index fb99493..b0d194d 100644 --- a/android_pyapi/__init__.py +++ b/android_pyapi/__init__.py @@ -379,6 +379,13 @@ __all__ = [ "AccessibilityServiceInfo", "Account", "AccountManager", "Activity", "ActivityManager", + "Fragment", "FragmentManager", "FragmentTransaction", + "Dialog", "AlertDialog", "ProgressDialog", + "DatePickerDialog", "TimePickerDialog", + "Service", "Application", + "PendingIntent", "ComponentName", "AlarmManager", + "NotificationChannel", "NotificationChannelGroup", + "NotificationCompat", "TaskStackBuilder", "AlphaAnimation", "Animation", "Animator", "ValueAnimator", "ObjectAnimator", "AnimatorSet", @@ -423,6 +430,7 @@ __all__ = [ "Html", "ImageView", "InputDevice", + "KeyEvent", "MotionEvent", "InputManager", "InputMethodManager", "Intent", @@ -456,6 +464,7 @@ __all__ = [ "ProgressBar", "RadioManager", "RadioTuner", "Rect", "RectF", + "RecyclerView", "Resources", "AssetManager", "RoleManager", "ScrollView", @@ -481,6 +490,7 @@ __all__ = [ "ThermalManager", "TimeManager", "Toast", + "TrafficStats", "TranslateAnimation", "TranslationManager", "TvInputManager", @@ -524,6 +534,14 @@ __all__ = [ "CloneNotSupportedException", "IllegalMonitorStateException", "IllegalThreadStateException", "EnumConstantNotPresentException", "TypeNotPresentException", + # Java Exception 体系 (扩展) + "ClassNotFoundException", "IllegalAccessException", + "InstantiationException", "InvocationTargetException", + "NoSuchElementException", "AssertionError", + "OutOfMemoryError", "StackOverflowError", "NoClassDefFoundError", + "UnsatisfiedLinkError", "LinkageError", "VerifyError", + "AbstractMethodError", "IncompatibleClassChangeError", + "ExceptionInInitializerError", "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 8817b8e..ab8cd54 100644 --- a/android_pyapi/android/app.py +++ b/android_pyapi/android/app.py @@ -18,6 +18,7 @@ from .._core import ( resolve_android_path, to_android_path, _sdcard_root, _app_data_dir, _package_name, ) +from ..java.lang import IllegalStateException try: from .._linux_impl import IS_LINUX, LinuxNotify, LinuxShell, LinuxProcess, LinuxDisplay, LinuxUIAutomation, LinuxPower @@ -247,6 +248,108 @@ class Intent(Stub): def filterHashCode(self) -> int: return hash((self._action, self._data_str, self._type, self._package, self._component)) & 0x7FFFFFFF + def getParcelableExtra(self, name): + return self._extras.get(name) if self._extras else None + + def setComponent(self, component): + self._component = component + return self + + def getComponent(self): + return getattr(self, '_component', None) + + def getDataString(self): + data = self.getData() + return str(data) if data else None + + def getScheme(self): + data = self.getData() + if data: + return data.getScheme() + return None + + def putExtras(self, src): + if src._extras: + if self._extras is None: + self._extras = {} + self._extras.update(src._extras) + return self + + def putIntegerArrayListExtra(self, name, value): + if self._extras is None: + self._extras = {} + self._extras[name] = value + return self + + def putStringArrayListExtra(self, name, value): + if self._extras is None: + self._extras = {} + self._extras[name] = value + return self + + def putParcelableArrayListExtra(self, name, value): + if self._extras is None: + self._extras = {} + self._extras[name] = value + return self + + def getBundleExtra(self, name): + return self._extras.get(name) if self._extras else None + + def putBundleExtra(self, name, value): + if self._extras is None: + self._extras = {} + self._extras[name] = value + return self + + def getParcelableArrayListExtra(self, name): + return self._extras.get(name) if self._extras else None + + def getStringArrayExtra(self, name): + return self._extras.get(name) if self._extras else None + + def getIntArrayExtra(self, name): + return self._extras.get(name) if self._extras else None + + def getBooleanArrayExtra(self, name): + return self._extras.get(name) if self._extras else None + + def getCharSequenceExtra(self, name): + return self._extras.get(name) if self._extras else None + + def getCharSequenceArrayListExtra(self, name): + return self._extras.get(name) if self._extras else None + + def removeCategory(self, category): + if self._categories: + self._categories.discard(category) + return self + + def setSelector(self, selector): + self._selector = selector + + def getSelector(self): + return getattr(self, '_selector', None) + + def setSourceBounds(self, bounds): + self._source_bounds = bounds + + def getSourceBounds(self): + return getattr(self, '_source_bounds', None) + + def makeMainActivity(self, component): + self.addCategory("android.intent.category.LAUNCHER") + self.setComponent(component) + return self + + def cloneFilter(self): + new_intent = Intent(self.getAction()) + if self._categories: + new_intent._categories = set(self._categories) + if self._type: + new_intent._type = self._type + return new_intent + # ═══════════════════════════════════════════════════════════════ # ComponentName (forward reference) @@ -738,6 +841,62 @@ class Notification(Stub): n._extras = dict(self._extras) return n + def setProgress(self, max, progress, indeterminate): + self._progress_max = max + self._progress = progress + self._progress_indeterminate = indeterminate + return self + + def setShowWhen(self, show): + self._show_when = show + return self + + def setOnlyAlertOnce(self, onlyAlertOnce): + self._only_alert_once = onlyAlertOnce + return self + + def setLocalOnly(self, localOnly): + self._local_only = localOnly + return self + + def setSound(self, sound): + self._sound = sound + return self + + def setVibrate(self, pattern): + self._vibrate = pattern + return self + + def setLights(self, argb, onMs, offMs): + self._light_color = argb + self._light_on = onMs + self._light_off = offMs + return self + + def setSortKey(self, sortKey): + self._sort_key = sortKey + return self + + def setTimeoutAfter(self, timeoutMs): + self._timeout_after = timeoutMs + return self + + def setColorized(self, colorize): + self._colorized = colorize + return self + + def setUsesChronometer(self, usesChronometer): + self._uses_chronometer = usesChronometer + return self + + def setShortcutId(self, shortcutId): + self._shortcut_id = shortcutId + return self + + def setContent(self, views): + self._content_view = views + return self + def __init__(self): super().__init__() self._channel_id = "" @@ -1355,6 +1514,90 @@ class Fragment(Stub): def unregisterForContextMenu(self, view): pass + def getChildFragmentManager(self): + if not hasattr(self, '_child_fragment_manager'): + from .app import FragmentManager + self._child_fragment_manager = FragmentManager() + return self._child_fragment_manager + + def getParentFragment(self): + return getattr(self, '_parent_fragment', None) + + def onCreateOptionsMenu(self, menu, inflater): + return False + + def onPrepareOptionsMenu(self, menu): + return False + + def onOptionsItemSelected(self, item): + return False + + def onCreateContextMenu(self, menu, v, menuInfo): + pass + + def onContextItemSelected(self, item): + return False + + def onContextMenuClosed(self, menu): + pass + + def onConfigurationChanged(self, newConfig): + pass + + def onLowMemory(self): + pass + + def onHiddenChanged(self, hidden): + self._hidden = hidden + + def getLayoutInflater(self): + return getattr(self, '_layout_inflater', None) + + def onGetLayoutInflater(self, savedInstanceState): + return self.getLayoutInflater() + + def requireContext(self): + if not hasattr(self, '_context') or self._context is None: + raise IllegalStateException("Fragment " + self.__class__.__name__ + " not attached to a context") + return self._context + + def requireActivity(self): + if not hasattr(self, '_activity') or self._activity is None: + raise IllegalStateException("Fragment " + self.__class__.__name__ + " not attached to an activity") + return self._activity + + def requireView(self): + if not hasattr(self, '_view') or self._view is None: + raise IllegalStateException("Fragment " + self.__class__.__name__ + " view is null") + return self._view + + def requireFragmentManager(self): + fm = getattr(self, '_fragment_manager', None) + if fm is None: + raise IllegalStateException("Fragment " + self.__class__.__name__ + " not associated with a fragment manager") + return fm + + def setMenuVisibility(self, menuVisible): + self._menu_visible = menuVisible + + def onMenuVisibilityChanged(self, menuVisible): + pass + + def postponeEnterTransition(self): + pass + + def startPostponedEnterTransition(self): + pass + + def getEnterTransition(self): + return getattr(self, '_enter_transition', None) + + def setEnterTransition(self, transition): + self._enter_transition = transition + + def onInflate(self, context, attrs, savedInstanceState): + pass + class FragmentTransaction(Stub): """FragmentTransaction — 管理 Fragment 事务""" @@ -1875,6 +2118,102 @@ class Activity(Stub): return LinuxUIAutomation.press_key(key) return False + def requestWindowFeature(self, featureId): + self._window_features = getattr(self, '_window_features', set()) + self._window_features.add(featureId) + return True + + def setTitle(self, title): + self._title = title + + def getTitle(self): + return getattr(self, '_title', '') + + def onKeyDown(self, keyCode, event): + return False + + def onKeyUp(self, keyCode, event): + return False + + def onTouchEvent(self, event): + return False + + def enterPictureInPictureMode(self): + pass + + def getReferrer(self): + return getattr(self, '_referrer', None) + + def setReferrer(self, referrer): + self._referrer = referrer + + def isTaskRoot(self): + return True + + def getChangingConfigurations(self): + return getattr(self, '_changing_config', 0) + + def navigateUpTo(self, intent): + return True + + def shouldUpRecreateTask(self, intent): + return False + + def getParentActivityIntent(self): + return None + + def onOptionsMenuClosed(self, menu): + pass + + def setVolumeControlStream(self, streamType): + self._volume_control_stream = streamType + + def getVolumeControlStream(self): + return getattr(self, '_volume_control_stream', 0) + + def getLocalClassName(self): + return self.__class__.__name__ + + def finishAndRemoveTask(self): + self.finish() + + def getLoaderManager(self): + if not hasattr(self, '_loader_manager'): + self._loader_manager = type('LoaderManager', (), { + 'initLoader': lambda *a: None, + 'restartLoader': lambda *a: None, + 'destroyLoader': lambda *a: None, + 'getLoader': lambda *a: None, + })() + return self._loader_manager + + def onRetainNonConfigurationInstance(self): + return None + + def getLastNonConfigurationInstance(self): + return getattr(self, '_last_non_config_instance', None) + + def setProgressBarVisibility(self, visible): + pass + + def setProgress(self, progress): + pass + + def setSecondaryProgress(self, secondaryProgress): + pass + + def onSearchRequested(self): + return False + + def startSearch(self, initialQuery, selectInitial, appSearchData, globalSearch): + pass + + def startIntentSender(self, intent, fillInIntent, flagsMask, flagsValues, extraFlags): + pass + + def startIntentSenderForResult(self, intent, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags): + pass + # ═══════════════════════════════════════════════════════════════ # Service diff --git a/android_pyapi/android/app_fragment.py b/android_pyapi/android/app_fragment.py index 76489ea..09d3250 100644 --- a/android_pyapi/android/app_fragment.py +++ b/android_pyapi/android/app_fragment.py @@ -11,9 +11,7 @@ from typing import Any, Dict, List, Optional, Set, Tuple, Callable from enum import IntEnum from .._core import Stub, AndroidOnlyError -from ..java.lang import RuntimeException - -IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {}) +from ..java.lang import RuntimeException, IllegalArgumentException, IllegalStateException # ═══════════════════════════════════════════════════════════════ @@ -424,11 +422,6 @@ class Fragment(Stub): return f"Fragment({self._mTag or hex(id(self))})" -class IllegalStateException(RuntimeException): - """IllegalStateException — 非法状态异常""" - pass - - # ═══════════════════════════════════════════════════════════════ # FragmentManager # ═══════════════════════════════════════════════════════════════ diff --git a/android_pyapi/android/content.py b/android_pyapi/android/content.py index f455ed4..18bc8ac 100644 --- a/android_pyapi/android/content.py +++ b/android_pyapi/android/content.py @@ -1116,10 +1116,16 @@ class SharedPreferences(Stub): return SharedPreferencesEditor(self) def registerOnSharedPreferenceChangeListener(self, listener): - pass + if not hasattr(self, '_listeners'): + self._listeners = [] + self._listeners.append(listener) def unregisterOnSharedPreferenceChangeListener(self, listener): - pass + if hasattr(self, '_listeners'): + try: + self._listeners.remove(listener) + except ValueError: + pass def getSharedPrefsFile(self): return self._file_path @@ -1186,6 +1192,12 @@ class SharedPreferencesEditor(Stub): self._prefs._data.pop(key, None) self._prefs._data.update(self._modified) self._prefs._save() + # 触发监听器 + sp = getattr(self, '_prefs', None) + if sp: + for key in self._modified: + for listener in getattr(sp, '_listeners', []): + listener.onSharedPreferenceChanged(sp, key) self._modified.clear() self._removed.clear() self._clear_all = False diff --git a/android_pyapi/android/content_intent.py b/android_pyapi/android/content_intent.py index 0cf2c4f..0945ae3 100644 --- a/android_pyapi/android/content_intent.py +++ b/android_pyapi/android/content_intent.py @@ -761,16 +761,36 @@ class Intent(Stub): def getData(self) -> Optional[str]: return self._data - def setData(self, data: str) -> "Intent": + def setData(self, data) -> "Intent": self._data = data self._type = None + # 从 Uri 提取 scheme + if data is not None: + if hasattr(data, 'getScheme'): + self._scheme = data.getScheme() or "" + elif isinstance(data, str): + from urllib.parse import urlparse + self._scheme = urlparse(data).scheme return self def getDataString(self) -> Optional[str]: - return self._data + if self._data is None: + return None + if isinstance(self._data, str): + return self._data + return str(self._data) def getScheme(self) -> str: - return self._scheme + if self._scheme: + return self._scheme + # 从 data 动态提取 + if self._data is not None: + if hasattr(self._data, 'getScheme'): + return self._data.getScheme() or "" + elif isinstance(self._data, str): + from urllib.parse import urlparse + return urlparse(self._data).scheme + return "" # ── Type ── diff --git a/android_pyapi/android/graphics.py b/android_pyapi/android/graphics.py index caa9eb2..16ba1ba 100644 --- a/android_pyapi/android/graphics.py +++ b/android_pyapi/android/graphics.py @@ -2049,6 +2049,69 @@ class Paint(Stub): def descent(self) -> float: return self._text_size * 0.2 + def setStrokeCap(self, cap): + self._stroke_cap = cap + + def getStrokeCap(self): + return getattr(self, '_stroke_cap', 0) + + def setStrokeJoin(self, join): + self._stroke_join = join + + def getStrokeJoin(self): + return getattr(self, '_stroke_join', 0) + + def setStrokeMiter(self, miter): + self._stroke_miter = miter + + def getStrokeMiter(self): + return getattr(self, '_stroke_miter', 4.0) + + def getTextBounds(self, text, start, end, bounds): + if hasattr(self, '_text_size'): + w = len(text[start:end]) * self._text_size * 0.5 + h = self._text_size + else: + w = len(text[start:end]) * 12 + h = 24 + bounds.left = 0 + bounds.top = -int(h * 0.8) + bounds.right = int(w) + bounds.bottom = int(h * 0.2) + + def setBlendMode(self, blendmode): + self._blend_mode = blendmode + + def getBlendMode(self): + return getattr(self, '_blend_mode', None) + + def setLetterSpacing(self, letterSpacing): + self._letter_spacing = letterSpacing + + def getLetterSpacing(self): + return getattr(self, '_letter_spacing', 0.0) + + def setFontFeatureSettings(self, settings): + self._font_feature_settings = settings + + def getFontFeatureSettings(self): + return getattr(self, '_font_feature_settings', None) + + def setHinting(self, mode): + self._hinting = mode + + def getHinting(self): + return getattr(self, '_hinting', 2) # HINTING_ON + + def setFakeBoldText(self, fakeBoldText): + self._fake_bold_text = fakeBoldText + + def isFakeBoldText(self): + return getattr(self, '_fake_bold_text', False) + + def hasGlyph(self, string): + return len(string) > 0 + # ═══════════════════════════════════════════════════════════════ # Bitmap diff --git a/android_pyapi/android/media.py b/android_pyapi/android/media.py index 54bff4b..e3ee577 100644 --- a/android_pyapi/android/media.py +++ b/android_pyapi/android/media.py @@ -1817,6 +1817,7 @@ class ExifInterface(Stub): super().__init__() self._filename = resolve_android_path(filename) self._attributes: Dict[str, str] = {} + self._pil_image = None self._load_exif() def _load_exif(self): @@ -1828,6 +1829,7 @@ class ExifInterface(Stub): from PIL import ExifTags try: img = Image.open(self._filename) + self._pil_image = img exif_data = img._getexif() if exif_data: for tag_id, value in exif_data.items(): @@ -1899,13 +1901,29 @@ class ExifInterface(Stub): pass def flipHorizontally(self): - pass + if hasattr(self, '_pil_image') and self._pil_image: + from PIL import Image + self._pil_image = self._pil_image.transpose(Image.FLIP_LEFT_RIGHT) + self._save_from_pil() def flipVertically(self): - pass + if hasattr(self, '_pil_image') and self._pil_image: + from PIL import Image + self._pil_image = self._pil_image.transpose(Image.FLIP_TOP_BOTTOM) + self._save_from_pil() - def rotate(self, degree: int): - pass + def rotate(self, degrees): + if hasattr(self, '_pil_image') and self._pil_image: + from PIL import Image + self._pil_image = self._pil_image.rotate(degrees, expand=True) + self._save_from_pil() + # 更新方向属性 + self.setAttribute("Orientation", "1") # Normal after rotation + + def _save_from_pil(self): + """从 PIL 图像保存回文件""" + if hasattr(self, '_pil_image') and self._pil_image and hasattr(self, '_filename'): + self._pil_image.save(self._filename) # ═══════════════════════════════════════════════════════════════ diff --git a/android_pyapi/android/net.py b/android_pyapi/android/net.py index 0dd1397..d4d99df 100644 --- a/android_pyapi/android/net.py +++ b/android_pyapi/android/net.py @@ -1079,53 +1079,98 @@ class TrafficStats(Stub): UNSUPPORTED = -1 TAG_SYSTEM_UPDATE = 0 + _cached_stats = None + _cache_time = 0 + @staticmethod - def getMobileTxBytes() -> int: + def _read_net_dev(): + """读取 /proc/net/dev 获取网络流量""" + try: + with open("/proc/net/dev", "r") as f: + lines = f.readlines()[2:] # 跳过前两行表头 + total_rx = 0 + total_tx = 0 + for line in lines: + parts = line.split() + ifname = parts[0].rstrip(":") + if ifname == "lo": + continue + rx_bytes = int(parts[1]) + tx_bytes = int(parts[9]) + total_rx += rx_bytes + total_tx += tx_bytes + return {"rx_bytes": total_rx, "tx_bytes": total_tx, + "rx_packets": 0, "tx_packets": 0} + except Exception: + return {"rx_bytes": 0, "tx_bytes": 0, "rx_packets": 0, "tx_packets": 0} + + @staticmethod + def getMobileRxBytes(): + return 0 # 桌面无移动数据 + + @staticmethod + def getMobileTxBytes(): return 0 @staticmethod - def getMobileRxBytes() -> int: + def getMobileRxPackets(): return 0 @staticmethod - def getTotalTxBytes() -> int: + def getMobileTxPackets(): return 0 @staticmethod - def getTotalRxBytes() -> int: - return 0 + def getTotalRxBytes(): + return TrafficStats._read_net_dev()["rx_bytes"] @staticmethod - def getMobileTxPackets() -> int: - return 0 + def getTotalTxBytes(): + return TrafficStats._read_net_dev()["tx_bytes"] @staticmethod - def getMobileRxPackets() -> int: - return 0 + def getTotalRxPackets(): + return TrafficStats._read_net_dev()["rx_packets"] @staticmethod - def getTotalTxPackets() -> int: - return 0 + def getTotalTxPackets(): + return TrafficStats._read_net_dev()["tx_packets"] @staticmethod - def getTotalRxPackets() -> int: - return 0 + def getUidRxBytes(uid): + try: + with open(f"/proc/uid_stat/{uid}/tcp_rcv", "r") as f: + return int(f.read().strip()) + except Exception: + return 0 @staticmethod - def getUidTxBytes(uid: int) -> int: - return 0 + def getUidTxBytes(uid): + try: + with open(f"/proc/uid_stat/{uid}/tcp_snd", "r") as f: + return int(f.read().strip()) + except Exception: + return 0 @staticmethod - def getUidRxBytes(uid: int) -> int: - return 0 + def getUidRxPackets(uid): + return TrafficStats.getUidRxBytes(uid) @staticmethod - def getUidTxPackets(uid: int) -> int: - return 0 + def getUidTxPackets(uid): + return TrafficStats.getUidTxBytes(uid) @staticmethod - def getUidRxPackets(uid: int) -> int: - return 0 + def getThreadStatsTag(): + return getattr(TrafficStats, '_thread_tag', 0) + + @staticmethod + def setThreadStatsTag(tag): + TrafficStats._thread_tag = tag + + @staticmethod + def clearThreadStatsTag(): + TrafficStats._thread_tag = 0 @staticmethod def getUidTcpTxBytes(uid: int) -> int: @@ -1143,18 +1188,6 @@ class TrafficStats(Stub): def getUidUdpRxBytes(uid: int) -> int: return 0 - @staticmethod - def setThreadStatsTag(tag: int): - pass - - @staticmethod - def clearThreadStatsTag(): - pass - - @staticmethod - def getThreadStatsTag() -> int: - return 0 - @staticmethod def setThreadStatsUid(uid: int): pass diff --git a/android_pyapi/android/os.py b/android_pyapi/android/os.py index 4f1a5fb..b1403a2 100644 --- a/android_pyapi/android/os.py +++ b/android_pyapi/android/os.py @@ -316,20 +316,30 @@ class Process(Stub): return _os.getpid() @staticmethod - def killProcess(pid: int): - pass + def killProcess(pid): + import signal + try: + _os.kill(pid, signal.SIGKILL) + except (OSError, ProcessLookupError): + pass @staticmethod - def sendSignal(pid: int, signal: int): - pass + def sendSignal(pid, signal_num): + try: + _os.kill(pid, signal_num) + except (OSError, ProcessLookupError): + pass @staticmethod def setThreadPriority(priority: int): pass @staticmethod - def setThreadPriority(tid: int, priority: int): - pass + def setThreadPriority(tid, priority): + try: + _os.setpriority(_os.PRIO_PROCESS, tid, priority) + except (OSError, PermissionError): + pass @staticmethod def getElapsedCpuTime() -> int: @@ -1925,20 +1935,53 @@ class FileUtils(Stub): """FileUtils — 文件工具""" @staticmethod - def readFileToString(file: Any) -> str: - return "" + def readFileToString(path): + try: + with open(path, "r", encoding="utf-8") as f: + return f.read() + except Exception: + return "" @staticmethod - def stringToFile(file: Any, string: str): - pass + def stringToFile(path, content): + try: + with open(path, "w", encoding="utf-8") as f: + f.write(content) + return True + except Exception: + return False @staticmethod - def copyFile(src: Any, dest: Any) -> bool: - return True + def copyFile(src, dst): + try: + import shutil + shutil.copy2(src, dst) + return True + except Exception: + return False @staticmethod - def deleteDirectory(dir_: Any) -> bool: - return True + def deleteDirectory(path): + try: + import shutil + shutil.rmtree(path) + return True + except Exception: + return False + + @staticmethod + def deleteContents(path): + try: + import shutil + for entry in _os.listdir(path): + full = _os.path.join(path, entry) + if _os.path.isdir(full): + shutil.rmtree(full) + else: + _os.remove(full) + return True + except Exception: + return False @staticmethod def deleteOlderFiles(dir_: Any, min_count: int, min_age_ms: int): diff --git a/android_pyapi/android/telephony.py b/android_pyapi/android/telephony.py index 184cc77..ce54f90 100644 --- a/android_pyapi/android/telephony.py +++ b/android_pyapi/android/telephony.py @@ -661,8 +661,10 @@ class TelephonyManager(Stub): def getMmsUAProfUrl(self) -> str: return "http://www.example.com/uaprof.xml" - def listen(self, listener: PhoneStateListener, events: int): - pass + def listen(self, listener, events): + if not hasattr(self, '_listeners'): + self._listeners = [] + self._listeners.append((listener, events)) def getDataState(self) -> int: return self.DATA_CONNECTED diff --git a/android_pyapi/android/view.py b/android_pyapi/android/view.py index 797b504..2137806 100644 --- a/android_pyapi/android/view.py +++ b/android_pyapi/android/view.py @@ -938,6 +938,106 @@ class View(Stub): def onApplyWindowInsets(self, insets): return insets + def setOnCreateContextMenuListener(self, listener): + self._on_create_context_menu_listener = listener + return self + + def getBaseline(self): + return -1 + + def setFocusableInTouchMode(self, focusable): + self._focusable_in_touch_mode = focusable + + def isFocusableInTouchMode(self): + return getattr(self, '_focusable_in_touch_mode', False) + + def setRotationX(self, rotationX): + self._rotation_x = rotationX + + def getRotationX(self): + return getattr(self, '_rotation_x', 0.0) + + def setRotationY(self, rotationY): + self._rotation_y = rotationY + + def getRotationY(self): + return getattr(self, '_rotation_y', 0.0) + + def setCameraDistance(self, distance): + self._camera_distance = distance + + def getCameraDistance(self): + return getattr(self, '_camera_distance', 0.0) + + def setForeground(self, foreground): + self._foreground = foreground + + def getForeground(self): + return getattr(self, '_foreground', None) + + def setBackgroundTintList(self, tint): + self._background_tint = tint + + def setBackgroundTintMode(self, tintMode): + self._background_tint_mode = tintMode + + def setScrollbarFadingEnabled(self, fadeScrollbars): + self._fade_scrollbars = fadeScrollbars + + def isScrollbarFadingEnabled(self): + return getattr(self, '_fade_scrollbars', True) + + def setVerticalScrollBarEnabled(self, enabled): + self._vertical_scrollbar = enabled + + def isVerticalScrollBarEnabled(self): + return getattr(self, '_vertical_scrollbar', True) + + def setHorizontalScrollBarEnabled(self, enabled): + self._horizontal_scrollbar = enabled + + def isHorizontalScrollBarEnabled(self): + return getattr(self, '_horizontal_scrollbar', True) + + def setLabelFor(self, id): + self._label_for = id + + def getLabelFor(self): + return getattr(self, '_label_for', 0) + + def cancelLongPress(self): + pass + + def setPaddingRelative(self, start, top, end, bottom): + self._padding_start = start + self._padding_top = top + self._padding_end = end + self._padding_bottom = bottom + + def setSoundEffectsEnabled(self, soundEffectsEnabled): + self._sound_effects = soundEffectsEnabled + + def isSoundEffectsEnabled(self): + return getattr(self, '_sound_effects', True) + + def setHapticFeedbackEnabled(self, hapticFeedbackEnabled): + self._haptic_feedback = hapticFeedbackEnabled + + def isHapticFeedbackEnabled(self): + return getattr(self, '_haptic_feedback', True) + + def refreshDrawableState(self): + pass + + def getDrawableState(self): + return [] + + def requestRectangleOnScreen(self, rectangle): + return False + + def setAllCaps(self, allCaps): + self._all_caps = allCaps + class MeasureSpec: """MeasureSpec — 测量规格包装""" @@ -1993,6 +2093,28 @@ class KeyEvent(Stub): meta_state=event._meta_state, ) + def getCharacters(self): + return getattr(self, '_characters', None) + + def isSystem(self): + return False + + def isPrintingKey(self): + return False + + def isModifierKey(self, keyCode): + return keyCode in (KEYCODE_SHIFT_LEFT, KEYCODE_SHIFT_RIGHT, KEYCODE_ALT_LEFT, KEYCODE_ALT_RIGHT, KEYCODE_CTRL_LEFT, KEYCODE_CTRL_RIGHT, KEYCODE_META_LEFT, KEYCODE_META_RIGHT) + + def startTracking(self): + self._tracking = True + + def isTracking(self): + return getattr(self, '_tracking', False) + + @staticmethod + def keyCodeToString(keyCode): + return f"KEYCODE_{keyCode}" + # ═══════════════════════════════════════════════════════════════ # GestureDetector @@ -2700,14 +2822,18 @@ class SurfaceHolder(Stub): def setFormat(self, format: int): self._format = format - def lockCanvas(self) -> Any: - return None + def lockCanvas(self, *args): + """锁定画布并返回 Canvas 对象""" + if hasattr(self, '_surface') and self._surface: + from .graphics import Canvas, Bitmap + if hasattr(self._surface, '_bitmap') and self._surface._bitmap: + return Canvas(self._surface._bitmap) + from .graphics import Canvas, Bitmap + return Canvas(Bitmap(100, 100)) - def lockCanvas(self, dirty: Any) -> Any: - return None - - def unlockCanvasAndPost(self, canvas: Any): - pass + def unlockCanvasAndPost(self, canvas): + """解锁画布并提交""" + pass # 桌面环境无需实际提交 # ═══════════════════════════════════════════════════════════════ diff --git a/android_pyapi/android/view_ext.py b/android_pyapi/android/view_ext.py index bd61831..7310879 100644 --- a/android_pyapi/android/view_ext.py +++ b/android_pyapi/android/view_ext.py @@ -13,6 +13,7 @@ from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union from .._core import Stub, AndroidOnlyError from ..java.lang import RuntimeException +from .view import KeyEvent as _BaseKeyEvent # ═══════════════════════════════════════════════════════════════ @@ -1559,8 +1560,8 @@ class GestureDetector(Stub): return f"GestureDetector(listener={'yes' if self._listener else 'no'})" -class KeyEvent(Stub): - """KeyEvent — 按键事件""" +class KeyEvent(_BaseKeyEvent): + """KeyEvent — 按键事件 (扩展版,继承 view.KeyEvent 的所有常量)""" ACTION_DOWN = 0 ACTION_UP = 1 diff --git a/android_pyapi/android/widget.py b/android_pyapi/android/widget.py index f854a87..15bedc4 100644 --- a/android_pyapi/android/widget.py +++ b/android_pyapi/android/widget.py @@ -1260,28 +1260,48 @@ class RecyclerView(ViewGroup): return False def notifyDataSetChanged(self): - pass + for observer in getattr(self, '_observers', []): + observer.onChanged() - def notifyItemChanged(self, position: int): - pass + def notifyItemChanged(self, position, payload=None): + for observer in getattr(self, '_observers', []): + observer.onItemRangeChanged(position, 1, payload) - def notifyItemInserted(self, position: int): - pass + def notifyItemInserted(self, position): + for observer in getattr(self, '_observers', []): + observer.onItemRangeInserted(position, 1) - def notifyItemRemoved(self, position: int): - pass + def notifyItemRemoved(self, position): + for observer in getattr(self, '_observers', []): + observer.onItemRangeRemoved(position, 1) - def notifyItemMoved(self, from_position: int, to_position: int): - pass + def notifyItemMoved(self, fromPosition, toPosition): + for observer in getattr(self, '_observers', []): + observer.onItemRangeMoved(fromPosition, toPosition, 1) - def notifyItemRangeChanged(self, position_start: int, item_count: int): - pass + def notifyItemRangeChanged(self, positionStart, itemCount, payload=None): + for observer in getattr(self, '_observers', []): + observer.onItemRangeChanged(positionStart, itemCount, payload) - def notifyItemRangeInserted(self, position_start: int, item_count: int): - pass + def notifyItemRangeInserted(self, positionStart, itemCount): + for observer in getattr(self, '_observers', []): + observer.onItemRangeInserted(positionStart, itemCount) - def notifyItemRangeRemoved(self, position_start: int, item_count: int): - pass + def notifyItemRangeRemoved(self, positionStart, itemCount): + for observer in getattr(self, '_observers', []): + observer.onItemRangeRemoved(positionStart, itemCount) + + def registerAdapterDataObserver(self, observer): + if not hasattr(self, '_observers'): + self._observers = [] + self._observers.append(observer) + + def unregisterAdapterDataObserver(self, observer): + if hasattr(self, '_observers'): + try: + self._observers.remove(observer) + except ValueError: + pass def getStateRestorationPolicy(self) -> int: return 0 @@ -2068,11 +2088,13 @@ class ScrollView(FrameLayout): def arrowScroll(self, direction: int) -> bool: return True - def smoothScrollTo(self, x: int, y: int): - self._scroll_position = y + def smoothScrollTo(self, x, y): + self._scroll_x = x + self._scroll_y = y - def smoothScrollBy(self, dx: int, dy: int): - self._scroll_position += dy + def smoothScrollBy(self, dx, dy): + self._scroll_x = getattr(self, '_scroll_x', 0) + dx + self._scroll_y = getattr(self, '_scroll_y', 0) + dy def pageScroll(self, direction: int) -> bool: return True @@ -2106,11 +2128,13 @@ class HorizontalScrollView(FrameLayout): def arrowScroll(self, direction: int) -> bool: return True - def smoothScrollTo(self, x: int, y: int): - pass + def smoothScrollTo(self, x, y): + self._scroll_x = x + self._scroll_y = y - def smoothScrollBy(self, dx: int, dy: int): - pass + def smoothScrollBy(self, dx, dy): + self._scroll_x = getattr(self, '_scroll_x', 0) + dx + self._scroll_y = getattr(self, '_scroll_y', 0) + dy def pageScroll(self, direction: int) -> bool: return True @@ -2795,16 +2819,24 @@ class BaseAdapter(Stub): return self.getView(position, convert_view, parent) def notifyDataSetChanged(self): - pass + for observer in getattr(self, '_observers', []): + observer.onChanged() def notifyDataSetInvalidated(self): - pass + for observer in getattr(self, '_observers', []): + observer.onInvalidated() - def registerDataSetObserver(self, observer: Any): - pass + def registerDataSetObserver(self, observer): + if not hasattr(self, '_observers'): + self._observers = [] + self._observers.append(observer) - def unregisterDataSetObserver(self, observer: Any): - pass + def unregisterDataSetObserver(self, observer): + if hasattr(self, '_observers'): + try: + self._observers.remove(observer) + except ValueError: + pass class ArrayAdapter(BaseAdapter): diff --git a/android_pyapi/java/io.py b/android_pyapi/java/io.py index 5100db0..a10fc02 100644 --- a/android_pyapi/java/io.py +++ b/android_pyapi/java/io.py @@ -237,7 +237,10 @@ class File(Stub): return _os.path.abspath(self._path) def getCanonicalPath(self): - return _os.path.realpath(self._path) + try: + return _os.path.realpath(self._path) + except Exception: + return self.getAbsolutePath() def getParent(self): p = _os.path.dirname(self._path) @@ -439,6 +442,43 @@ class File(Stub): def toPath(self): return _PathLibPath(self._path) + @staticmethod + def createTempFile(prefix, suffix=None, directory=None): + import tempfile + if directory: + fd, path = tempfile.mkstemp(prefix=prefix, suffix=suffix or ".tmp", dir=directory) + else: + fd, path = tempfile.mkstemp(prefix=prefix, suffix=suffix or ".tmp") + _os.close(fd) + return File(path) + + def deleteOnExit(self): + import atexit + atexit.register(lambda: self.delete() if self.exists() else None) + + def setLastModified(self, time): + try: + _os.utime(self._path, (time / 1000, time / 1000)) + return True + except Exception: + return False + + def setReadOnly(self): + try: + _os.chmod(self._path, 0o444) + return True + except Exception: + return False + + def isAbsolute(self): + return _os.path.isabs(self._path) + + def getAbsoluteFile(self): + return File(self.getAbsolutePath()) + + def getCanonicalFile(self): + return File(self.getCanonicalPath()) + def __str__(self): return self._path diff --git a/android_pyapi/java/lang.py b/android_pyapi/java/lang.py index ecaad1e..d296fe2 100644 --- a/android_pyapi/java/lang.py +++ b/android_pyapi/java/lang.py @@ -8,6 +8,7 @@ import re as _re import math as _py_math import sys as _sys import os as _os +import platform import time as _time import threading as _threading import inspect as _inspect @@ -28,6 +29,12 @@ __all__ = [ "CloneNotSupportedException", "IllegalMonitorStateException", "IllegalThreadStateException", "EnumConstantNotPresentException", "TypeNotPresentException", + "ClassNotFoundException", "IllegalAccessException", "InstantiationException", + "InvocationTargetException", "NoSuchElementException", + "AssertionError", "OutOfMemoryError", "StackOverflowError", + "NoClassDefFoundError", "UnsatisfiedLinkError", "LinkageError", + "VerifyError", "AbstractMethodError", "IncompatibleClassChangeError", + "ExceptionInInitializerError", "String", "Integer", "Long", "Short", "Byte", "Float", "Double", "Boolean", "Character", "Math", "StrictMath", "System", "Thread", "ThreadLocal", "StringBuilder", "StringBuffer", @@ -227,6 +234,80 @@ class TypeNotPresentException(RuntimeException): self.typeName = typeName +# ===== 缺失的 Exception 子类 ===== +class ClassNotFoundException(Exception): + def __init__(self, message="", cause=None): + super().__init__(message, cause) + self.ex = message + +class IllegalAccessException(Exception): + def __init__(self, message=""): + super().__init__(message) + +class InstantiationException(Exception): + def __init__(self, message=""): + super().__init__(message) + +class InvocationTargetException(Exception): + def __init__(self, target=None, cause=None): + if target is not None and cause is None: + super().__init__(str(target)) + self.target = target + else: + super().__init__(str(target) if target else "", cause) + self.target = target + +class NoSuchElementException(RuntimeException): + def __init__(self, message=""): + super().__init__(message) + +# ===== 缺失的 Error 类 ===== +class AssertionError(Error): + def __init__(self, message=None): + if message is None: + super().__init__("Assertion failed") + else: + super().__init__(str(message)) + +class OutOfMemoryError(Error): + def __init__(self, message=""): + super().__init__(message) + +class StackOverflowError(Error): + def __init__(self, message=""): + super().__init__(message) + +class NoClassDefFoundError(Error): + def __init__(self, message=""): + super().__init__(message) + +class UnsatisfiedLinkError(Error): + def __init__(self, message=""): + super().__init__(message) + +class LinkageError(Error): + def __init__(self, message=""): + super().__init__(message) + +class VerifyError(LinkageError): + def __init__(self, message=""): + super().__init__(message) + +class AbstractMethodError(LinkageError): + def __init__(self, message=""): + super().__init__(message) + +class IncompatibleClassChangeError(LinkageError): + def __init__(self, message=""): + super().__init__(message) + +class ExceptionInInitializerError(LinkageError): + def __init__(self, message="", cause=None): + super().__init__(message) + self._cause = cause + self.exception = cause + + # ============================================================ # StackTraceElement # ============================================================ @@ -753,6 +834,62 @@ class Integer(Number, Comparable): return Integer(int(env_val)) return Integer(val) if val is not None else None + @staticmethod + def compare(x, y): + return -1 if x < y else (1 if x > y else 0) + + @staticmethod + def compareUnsigned(x, y): + ux = x & 0xFFFFFFFF if x < 0 else x + uy = y & 0xFFFFFFFF if y < 0 else y + return -1 if ux < uy else (1 if ux > uy else 0) + + @staticmethod + def divideUnsigned(dividend, divisor): + ux = dividend & 0xFFFFFFFF if dividend < 0 else dividend + uy = divisor & 0xFFFFFFFF if divisor < 0 else divisor + return ux // uy + + @staticmethod + def remainderUnsigned(dividend, divisor): + ux = dividend & 0xFFFFFFFF if dividend < 0 else dividend + uy = divisor & 0xFFFFFFFF if divisor < 0 else divisor + return ux % uy + + @staticmethod + def parseUnsignedInt(s, radix=10): + return int(s, radix) + + @staticmethod + def toUnsignedString(i, radix=10): + if i < 0: + i = i & 0xFFFFFFFF + if radix == 10: + return str(i) + elif radix == 16: + return format(i, 'x') + elif radix == 2: + return format(i, 'b') + elif radix == 8: + return format(i, 'o') + return str(i) + + @staticmethod + def toUnsignedLong(i): + return i & 0xFFFFFFFF + + @staticmethod + def sum(a, b): + return a + b + + @staticmethod + def max(a, b): + return a if a > b else b + + @staticmethod + def min(a, b): + return a if a < b else b + def toString(self): return String(str(self._value)) @@ -1418,9 +1555,92 @@ class Math: @staticmethod def copySign(m, s): return abs(m) * (1 if s >= 0 else -1) @staticmethod - def floorDiv(x, y): return x // y if (x ^ y) >= 0 else -(abs(x) // abs(y)) + def floorDiv(x, y): + return math.floor(x / y) if y != 0 else 0 @staticmethod - def floorMod(x, y): return x - Math.floorDiv(x, y) * y + def floorMod(x, y): + return x - Math.floorDiv(x, y) * y + @staticmethod + def subtractExact(x, y): + result = x - y + if isinstance(x, int) and isinstance(y, int): + if result > 0 and x < 0 and y > 0: + raise ArithmeticException("integer overflow") + if result < 0 and x > 0 and y < 0: + raise ArithmeticException("integer overflow") + return result + @staticmethod + def multiplyExact(x, y): + result = x * y + if isinstance(x, int) and isinstance(y, int): + if result // y != x: + raise ArithmeticException("integer overflow") + return result + @staticmethod + def incrementExact(a): + if a == 2147483647: + raise ArithmeticException("integer overflow") + return a + 1 + @staticmethod + def decrementExact(a): + if a == -2147483648: + raise ArithmeticException("integer overflow") + return a - 1 + @staticmethod + def negateExact(a): + if a == -2147483648: + raise ArithmeticException("integer overflow") + return -a + @staticmethod + def toIntExact(value): + if value < -2147483648 or value > 2147483647: + raise ArithmeticException("integer overflow") + return int(value) + @staticmethod + def log1p(x): + return math.log1p(x) + @staticmethod + def expm1(x): + return math.expm1(x) + @staticmethod + def ulp(x): + return math.ulp(x) if hasattr(math, 'ulp') else abs(x) * 2.220446049250313e-16 + @staticmethod + def getExponent(x): + return math.frexp(x)[1] - 1 + @staticmethod + def nextAfter(start, direction): + return math.nextafter(start, direction) + @staticmethod + def nextUp(x): + return math.nextafter(x, math.inf) + @staticmethod + def nextDown(x): + return math.nextafter(x, -math.inf) + @staticmethod + def rint(x): + return math.rint(x) if hasattr(math, 'rint') else float(math.floor(x + 0.5)) + @staticmethod + def IEEEremainder(x, y): + return math.remainder(x, y) if hasattr(math, 'remainder') else x - y * math.floor(x / y) + @staticmethod + def compareUnsigned(x, y): + ux = x & 0xFFFFFFFF if x < 0 else x + uy = y & 0xFFFFFFFF if y < 0 else y + return -1 if ux < uy else (1 if ux > uy else 0) + @staticmethod + def divideUnsigned(dividend, divisor): + ux = dividend & 0xFFFFFFFF if dividend < 0 else dividend + uy = divisor & 0xFFFFFFFF if divisor < 0 else divisor + return ux // uy + @staticmethod + def remainderUnsigned(dividend, divisor): + ux = dividend & 0xFFFFFFFF if dividend < 0 else dividend + uy = divisor & 0xFFFFFFFF if divisor < 0 else divisor + return ux % uy + @staticmethod + def fma(a, b, c): + return a * b + c class StrictMath(Math): @@ -1529,6 +1749,39 @@ class System: System.err = err_stream @staticmethod def lineSeparator(): return _os.linesep + @staticmethod + def getProperties(): + return { + "java.version": "1.8.0", + "java.vendor": "Android-pyApi", + "java.vendor.url": "https://gitee.com/shuiyu1123/Android-pyApi", + "java.home": _os.environ.get("JAVA_HOME", ""), + "java.class.version": "52.0", + "os.name": platform.system(), + "os.arch": platform.machine(), + "os.version": platform.release(), + "file.separator": _os.sep, + "path.separator": _os.pathsep, + "line.separator": _os.linesep, + "user.name": _os.environ.get("USER", _os.environ.get("USERNAME", "")), + "user.home": _os.path.expanduser("~"), + "user.dir": _os.getcwd(), + } + @staticmethod + def setProperties(props): + System._properties = props + @staticmethod + def mapLibraryName(libname): + if platform.system() == "Windows": + return libname + ".dll" + elif platform.system() == "Darwin": + return "lib" + libname + ".dylib" + else: + return "lib" + libname + ".so" + @staticmethod + def getLogger(name): + import logging + return logging.getLogger(name) in_ = _sys.stdin out = _sys.stdout err = _sys.stderr @@ -1683,6 +1936,66 @@ class Thread(Runnable): def getDefaultUncaughtExceptionHandler(): return Thread._default_uncaught_exception_handler + def getUncaughtExceptionHandler(self): + return getattr(self, '_uncaught_handler', Thread._default_uncaught_exception_handler) + + def setUncaughtExceptionHandler(self, handler): + self._uncaught_handler = handler + + @staticmethod + def getAllStackTraces(): + import traceback + result = {} + # 只返回当前线程 + result[Thread.currentThread()] = traceback.extract_stack() + return result + + def getStackTrace(self): + import traceback + return traceback.extract_stack() + + @staticmethod + def enumerate(array): + # 返回活动线程数 + return 0 + + @staticmethod + def activeCount(): + import threading + return threading.active_count() + + def getContextClassLoader(self): + return getattr(self, '_context_classloader', None) + + def setContextClassLoader(self, cl): + self._context_classloader = cl + + def getThreadGroup(self): + return getattr(self, '_thread_group', None) + + @staticmethod + def holdLock(obj): + return False + + @staticmethod + def dumpStack(): + import traceback + traceback.print_stack() + + @staticmethod + def onSpinWait(): + pass + + def isVirtual(self): + return False + + @staticmethod + def ofVirtual(): + """创建虚拟线程 (Java 21+)""" + t = Thread(target=lambda: None) + t._virtual = True + return t + class ThreadLocal(Stub): """java.lang.ThreadLocal""" @@ -1744,6 +2057,35 @@ class StringBuilder(Stub): def lastIndexOf(self, s, f=None): return self.toString().rfind(s) + def ensureCapacity(self, minimumCapacity): + pass # Python list 自动扩容 + + def trimToSize(self): + pass + + def setLength(self, newLength): + current = len(self._buf) + if newLength < current: + del self._buf[newLength:] + else: + self._buf.extend([" "] * (newLength - current)) + + def subSequence(self, start, end): + return "".join(self._buf[start:end]) + + def getChars(self, srcBegin, srcEnd, dst, dstBegin): + for i in range(srcBegin, srcEnd): + dst[dstBegin + i - srcBegin] = self._buf[i] + + def codePointAt(self, index): + return ord(self._buf[index]) + + def codePointBefore(self, index): + return ord(self._buf[index - 1]) + + def codePointCount(self, beginIndex, endIndex): + return endIndex - beginIndex + class StringBuffer(StringBuilder): """java.lang.StringBuffer — 线程安全的可变字符序列""" diff --git a/android_pyapi/java/util.py b/android_pyapi/java/util.py index 71bfeae..affe569 100644 --- a/android_pyapi/java/util.py +++ b/android_pyapi/java/util.py @@ -1848,6 +1848,60 @@ class Collections: return _LifoQueue(deque) + @staticmethod + def addAll(collection, *elements): + for e in elements: + collection.add(e) + return len(elements) > 0 + + @staticmethod + def emptyList(): + return [] + + @staticmethod + def emptySet(): + return set() + + @staticmethod + def emptyMap(): + return {} + + @staticmethod + def reverseOrder(comparator=None): + if comparator is None: + return lambda a, b: -1 if a < b else (1 if a > b else 0) + return lambda a, b: -comparator(a, b) + + @staticmethod + def singletonSet(element): + s = HashSet() + s.add(element) + return s + + @staticmethod + def checkedList(lst, type_): + return lst + + @staticmethod + def checkedSet(s, type_): + return s + + @staticmethod + def checkedMap(m, keyType, valueType): + return m + + @staticmethod + def unmodifiableCollection(c): + return c + + @staticmethod + def unmodifiableQueue(q): + return q + + @staticmethod + def unmodifiableDeque(d): + return d + # ============================================================ # Arrays @@ -2334,6 +2388,37 @@ class Calendar(Stub): def getInstance() -> 'GregorianCalendar': return GregorianCalendar() + def clear(self, field=None): + if field is None: + self._fields = {} + else: + if field in self._fields: + del self._fields[field] + + def isSet(self, field): + return field in self._fields + + def getCalendarType(self): + return "gregory" + + @staticmethod + def getAvailableLocales(): + from .util import Locale + return [Locale.US, Locale.CHINA, Locale.JAPAN, Locale.KOREA, Locale.GERMANY, Locale.FRANCE] + + def toInstant(self): + from .time import Instant + return Instant.ofEpochMilli(self.getTimeInMillis()) + + def getWeekYear(self): + return self.get(Calendar.YEAR) + + def isWeekDateSupported(self): + return False + + def getWeeksInWeekYear(self): + return 52 + class GregorianCalendar(Calendar): """java.util.GregorianCalendar"""