From e24b47fe9813c76f65ac1d3f011a986e8d0faa37 Mon Sep 17 00:00:00 2001 From: FyukMdaa Date: Fri, 31 Jul 2026 11:52:12 +0900 Subject: [PATCH 01/17] devide files --- app/release/output-metadata.json | 20 - .../lockscreencamera/LockscreenCamera.java | 593 ++---------------- .../SecureViewerActivity.java | 306 ++------- .../hook/ActivityVisibilitySpoofHook.java | 26 + .../hook/CameraActivityLifecycleHook.java | 144 +++++ .../hook/CameraGestureLauncherHook.java | 110 ++++ .../hook/DecorViewProtectionHook.java | 46 ++ .../hook/GalleryRedirectHook.java | 109 ++++ .../hook/KeyguardDismissBlockHook.java | 30 + .../hook/KeyguardIntentRewriteHook.java | 31 + .../hook/MediaStoreSessionTrackingHook.java | 67 ++ .../lockscreencamera/hook/MiscSystemHook.java | 35 ++ .../hook/ViewVisibilityProtectionHook.java | 29 + .../hook/WindowSecurityBypass.java | 86 +++ .../session/SessionManager.java | 43 ++ .../lockscreencamera/ui/PhotoAdapter.java | 116 ++++ .../ui/SwipeDismissLayout.java | 96 +++ .../lockscreencamera/util/BitmapDecoder.java | 55 ++ .../util/CameraPackageUtil.java | 48 ++ .../util/ReflectionFieldUtil.java | 58 ++ 20 files changed, 1214 insertions(+), 834 deletions(-) delete mode 100644 app/release/output-metadata.json create mode 100644 app/src/main/java/com/github/droserasprout/lockscreencamera/hook/ActivityVisibilitySpoofHook.java create mode 100644 app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraActivityLifecycleHook.java create mode 100644 app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraGestureLauncherHook.java create mode 100644 app/src/main/java/com/github/droserasprout/lockscreencamera/hook/DecorViewProtectionHook.java create mode 100644 app/src/main/java/com/github/droserasprout/lockscreencamera/hook/GalleryRedirectHook.java create mode 100644 app/src/main/java/com/github/droserasprout/lockscreencamera/hook/KeyguardDismissBlockHook.java create mode 100644 app/src/main/java/com/github/droserasprout/lockscreencamera/hook/KeyguardIntentRewriteHook.java create mode 100644 app/src/main/java/com/github/droserasprout/lockscreencamera/hook/MediaStoreSessionTrackingHook.java create mode 100644 app/src/main/java/com/github/droserasprout/lockscreencamera/hook/MiscSystemHook.java create mode 100644 app/src/main/java/com/github/droserasprout/lockscreencamera/hook/ViewVisibilityProtectionHook.java create mode 100644 app/src/main/java/com/github/droserasprout/lockscreencamera/hook/WindowSecurityBypass.java create mode 100644 app/src/main/java/com/github/droserasprout/lockscreencamera/session/SessionManager.java create mode 100644 app/src/main/java/com/github/droserasprout/lockscreencamera/ui/PhotoAdapter.java create mode 100644 app/src/main/java/com/github/droserasprout/lockscreencamera/ui/SwipeDismissLayout.java create mode 100644 app/src/main/java/com/github/droserasprout/lockscreencamera/util/BitmapDecoder.java create mode 100644 app/src/main/java/com/github/droserasprout/lockscreencamera/util/CameraPackageUtil.java create mode 100644 app/src/main/java/com/github/droserasprout/lockscreencamera/util/ReflectionFieldUtil.java diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json deleted file mode 100644 index 07b8210..0000000 --- a/app/release/output-metadata.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "version": 3, - "artifactType": { - "type": "APK", - "kind": "Directory" - }, - "applicationId": "com.github.droserasprout.lockscreencamera", - "variantName": "release", - "elements": [ - { - "type": "SINGLE", - "filters": [], - "attributes": [], - "versionCode": 1, - "versionName": "1.0.0", - "outputFile": "app-release.apk" - } - ], - "elementType": "File" -} diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/LockscreenCamera.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/LockscreenCamera.java index 0a6febf..37b39dc 100644 --- a/app/src/main/java/com/github/droserasprout/lockscreencamera/LockscreenCamera.java +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/LockscreenCamera.java @@ -1,583 +1,72 @@ package com.github.droserasprout.lockscreencamera; -import android.app.Activity; -import android.app.ActivityOptions; -import android.app.KeyguardManager; -import android.content.BroadcastReceiver; -import android.content.ClipData; -import android.content.ComponentName; -import android.content.Context; -import android.content.ContextWrapper; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.graphics.PixelFormat; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.provider.MediaStore; import android.util.Log; -import android.view.View; -import android.view.Window; -import android.view.WindowManager; -import android.content.ContentResolver; -import android.content.ContentValues; import androidx.annotation.NonNull; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; +import com.github.droserasprout.lockscreencamera.hook.ActivityVisibilitySpoofHook; +import com.github.droserasprout.lockscreencamera.hook.CameraActivityLifecycleHook; +import com.github.droserasprout.lockscreencamera.hook.CameraGestureLauncherHook; +import com.github.droserasprout.lockscreencamera.hook.DecorViewProtectionHook; +import com.github.droserasprout.lockscreencamera.hook.GalleryRedirectHook; +import com.github.droserasprout.lockscreencamera.hook.KeyguardDismissBlockHook; +import com.github.droserasprout.lockscreencamera.hook.KeyguardIntentRewriteHook; +import com.github.droserasprout.lockscreencamera.hook.MediaStoreSessionTrackingHook; +import com.github.droserasprout.lockscreencamera.hook.MiscSystemHook; +import com.github.droserasprout.lockscreencamera.hook.ViewVisibilityProtectionHook; +import com.github.droserasprout.lockscreencamera.util.CameraPackageUtil; import io.github.libxposed.api.XposedModule; -import io.github.libxposed.api.XposedModuleInterface; import io.github.libxposed.api.XposedModuleInterface.PackageReadyParam; import io.github.libxposed.api.XposedModuleInterface.SystemServerStartingParam; +/** + * モジュールのエントリーポイント。 + * 各機能は {@code hook} パッケージ内の専用クラスに委譲し、このクラス自体は + * 「対象パッケージかどうかの判定」と「どのフックを組み込むか」の一覧だけを持つ。 + * + * 各フック機能の内容: + * 0. DecorViewProtectionHook - DecorView の透明化・非表示化を阻止 + * 1. KeyguardDismissBlockHook - requestDismissKeyguard(PIN画面表示要求)をブロック + * 2. ActivityVisibilitySpoofHook - hasWindowFocus/isResumed のスプーフィング + * 3. KeyguardIntentRewriteHook - getIntent() の動的書き換え + * 4. ViewVisibilityProtectionHook - SurfaceView 等の非表示化を阻止 + * 5. GalleryRedirectHook - ギャラリー確認画面を SecureViewerActivity へリダイレクト + * 6. CameraActivityLifecycleHook - ライフサイクルフック(セッション開始・自動終了・ウィンドウ復元) + * 7. MediaStoreSessionTrackingHook - 撮影された写真の URI をセッションに記録 + * 8. MiscSystemHook - onCameraUnavailable / canAuthenticate 対策 + * -. CameraGestureLauncherHook - システムサーバー側:カメラジェスチャーの起動ロジック + */ public class LockscreenCamera extends XposedModule { private static final String TAG = "LockscreenCamera"; - // フィールド検索用キャッシュ - private static final Map fieldCache = new ConcurrentHashMap<>(); - - // 書き換え対象の内部フィールド名リスト - private static final String[] TARGET_BOOLEAN_FIELDS = { - "mIsSecure", "mIsSecureCamera", "mKeyguardLocked", - "mInLockScreen", "mIgnoreKeyguard", "mIsScreenOn", - "mSecureCamera", "mIsKeyguardLocked", "mIsHideForeground", - "mIsGalleryLock", "mIsCaptureIntent", "mIsPortraitIntent", "mIsVideoIntent", - "mUserAuthenticationFlag", "mIgnoreKeyguardCheck", - "mIsCameraApp", "mPrivacyAuthorized", "mIsForeground" - }; public LockscreenCamera() { super(); } - // === SessionManager (カメラアプリプロセス内で動作) === - public static class SessionManager { - public static volatile boolean isActive = false; - public static final List SESSION_URIS = new CopyOnWriteArrayList<>(); - - public static void start() { - isActive = true; - SESSION_URIS.clear(); - Log.i(TAG, "Secure Camera Session Started"); - } - - public static void end() { - isActive = false; - SESSION_URIS.clear(); - Log.i(TAG, "Secure Camera Session Cleared"); - } - - public static void add(Uri uri) { - if (isActive && uri != null) { - if (!SESSION_URIS.contains(uri)) { - SESSION_URIS.add(uri); - Log.d(TAG, "Added to Session: " + uri + " | Total: " + SESSION_URIS.size()); - } else { - Log.d(TAG, "Duplicate URI skipped: " + uri); - } - } - } - } - @Override public void onPackageReady(@NonNull PackageReadyParam param) { - // カメラパッケージのみを対象にする String pkg = param.getPackageName(); - if (!isCameraPackage(pkg)) { + if (!CameraPackageUtil.isCameraPackage(pkg)) { return; } log(Log.INFO, TAG, "Targeting Camera App: " + pkg); - // 0. DecorView の透明化・非表示化を物理的に阻止 - try { - hook(View.class.getDeclaredMethod("setAlpha", float.class)).intercept(chain -> { - View view = (View) chain.getThisObject(); - if (isDecorView(view) && isCameraContext(view.getContext())) { - List args = chain.getArgs(); - float alpha = (float) args.get(0); - if (alpha < 1.0f) args.set(0, 1.0f); - } - return chain.proceed(); - }); - hook(View.class.getDeclaredMethod("setVisibility", int.class)).intercept(chain -> { - View view = (View) chain.getThisObject(); - if (isDecorView(view) && isCameraContext(view.getContext())) { - List args = chain.getArgs(); - int vis = (int) args.get(0); - if (vis != View.VISIBLE) args.set(0, View.VISIBLE); - } - return chain.proceed(); - }); - } catch (Throwable ignored) {} - - // 1. Keyguard の解除要求(PIN 画面表示)を完全にブロック - try { - Method dismissMethod = KeyguardManager.class.getDeclaredMethod( - "requestDismissKeyguard", Activity.class, KeyguardManager.KeyguardDismissCallback.class); - hook(dismissMethod).intercept(chain -> { - log(Log.WARN, TAG, "BLOCKED: requestDismissKeyguard (Preventing PIN screen)"); - return null; - }); - } catch (Throwable ignored) {} - - // 2. Activity Visibility Spoofing - try { - hook(Activity.class.getDeclaredMethod("hasWindowFocus")).intercept(chain -> { - if (isCameraActivity((Activity) chain.getThisObject())) return true; - return (Boolean) chain.proceed(); - }); - hook(Activity.class.getDeclaredMethod("isResumed")).intercept(chain -> { - if (isCameraActivity((Activity) chain.getThisObject())) return true; - return (Boolean) chain.proceed(); - }); - } catch (Throwable ignored) {} - - // 3. Intent の動的書き換え - try { - hook(Activity.class.getDeclaredMethod("getIntent")).intercept(chain -> { - Intent intent = (Intent) chain.proceed(); - if (isCameraActivity((Activity) chain.getThisObject()) && intent != null) { - if (intent.getBooleanExtra("com.miui.camera.extra.START_BY_KEYGUARD", false)) { - intent.putExtra("is_secure_camera", true); - intent.putExtra("ShowCameraWhenLocked", true); - } - } - return intent; - }); - } catch (Throwable ignored) {} - - // 4. SurfaceView/View 非表示化を阻止 - try { - hook(View.class.getMethod("setVisibility", int.class)).intercept(chain -> { - View v = (View) chain.getThisObject(); - if (isCameraContext(v.getContext()) && !isDecorView(v)) { - List args = chain.getArgs(); - int vis = (int) args.get(0); - if (vis != View.VISIBLE) args.set(0, View.VISIBLE); - } - return chain.proceed(); - }); - } catch (Throwable ignored) {} - - // 5. Secure Gallery Redirect - try { - Method startAct = Activity.class.getDeclaredMethod("startActivity", Intent.class); - hook(startAct).intercept(chain -> { - handleGalleryRedirect((Context) chain.getThisObject(), (Intent) chain.getArgs().get(0)); - return chain.proceed(); - }); - - Method startRes = Activity.class.getDeclaredMethod("startActivityForResult", Intent.class, int.class); - hook(startRes).intercept(chain -> { - handleGalleryRedirect((Context) chain.getThisObject(), (Intent) chain.getArgs().get(0)); - return chain.proceed(); - }); - - Method startCtx = ContextWrapper.class.getDeclaredMethod("startActivity", Intent.class); - hook(startCtx).intercept(chain -> { - handleGalleryRedirect((Context) chain.getThisObject(), (Intent) chain.getArgs().get(0)); - return chain.proceed(); - }); - } catch (Throwable t) { - log(Log.ERROR, TAG, "Failed to hook gallery redirect", t); - } - - // 6. ライフサイクルフック(+ 自動終了機能の実装) - String[] criticalMethods = {"attachBaseContext", "onCreate", "onStart", "onResume", "onWindowFocusChanged", "onDestroy"}; - - for (String mname : criticalMethods) { - try { - Method m; - if ("attachBaseContext".equals(mname)) { - m = ContextWrapper.class.getDeclaredMethod("attachBaseContext", Context.class); - } else if ("onCreate".equals(mname)) { - m = Activity.class.getDeclaredMethod("onCreate", Bundle.class); - } else if ("onWindowFocusChanged".equals(mname)) { - m = Activity.class.getDeclaredMethod("onWindowFocusChanged", boolean.class); - } else if ("onDestroy".equals(mname)) { - m = Activity.class.getDeclaredMethod("onDestroy"); - } else { - m = Activity.class.getDeclaredMethod(mname); - } - - final String methodName = mname; - hook(m).intercept(chain -> { - Object thisObj = chain.getThisObject(); - if (thisObj instanceof Activity) { - Activity act = (Activity) thisObj; - boolean isTarget = isCameraActivity(act); - - if (isTarget) { - // onDestroy でセッションをクリーンアップ - if ("onDestroy".equals(methodName)) { - if (SessionManager.isActive) { - SessionManager.end(); - } - return chain.proceed(); - } - - if ("onWindowFocusChanged".equals(methodName)) { - boolean hasFocus = (boolean) ((List) chain.getArgs()).get(0); - if (!hasFocus) return chain.proceed(); - } - - if ("onCreate".equals(methodName)) { - Object res = chain.proceed(); - - Intent intent = act.getIntent(); - // フラグで判定(GCam/MIUI 共通) - boolean isLockscreenLaunch = intent != null && intent.getBooleanExtra("com.miui.camera.extra.START_BY_KEYGUARD", false); - - if (isLockscreenLaunch) { - // 【修正】ここ(カメラプロセス内)で Session を開始 - SessionManager.start(); - log(Log.INFO, TAG, "Secure Lockscreen launch detected. Session Started."); - - IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_OFF); - BroadcastReceiver screenOffReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent i) { - SessionManager.end(); - act.finish(); - } - }; - - try { - if (Build.VERSION.SDK_INT >= 33) { - act.registerReceiver(screenOffReceiver, filter, Context.RECEIVER_NOT_EXPORTED); - } else { - act.registerReceiver(screenOffReceiver, filter); - } - } catch (Exception e) { - log(Log.WARN, TAG, "Failed to register receiver: " + e.getMessage()); - } - } - - applyWindowAndBufferFixes(act); - return res; - } - applyWindowAndBufferFixes(act); - } - } - return chain.proceed(); - }); - } catch (Throwable ignored) {} - } - - // 7. プレビュー画像の絞り込み(写真保存のトラッキング) - try { - hook(ContentResolver.class.getDeclaredMethod("insert", Uri.class, ContentValues.class)) - .intercept(chain -> { - if (SessionManager.isActive) { - Uri uri = (Uri) chain.getArgs().get(0); - Uri returnedUri = (Uri) chain.getArgs().get(1); - if (returnedUri != null) { - SessionManager.add(returnedUri); - } - return returnedUri; - } - return chain.proceed(); - }); - } catch (Throwable ignored) {} - - // --- Added try-catch block for the update hook --- - try { - hook(ContentResolver.class.getDeclaredMethod("update", Uri.class, ContentValues.class, String.class, String[].class)) - .intercept(chain -> { - if (SessionManager.isActive) { - Uri uri = (Uri) chain.getArgs().get(0); - ContentValues values = (ContentValues) chain.getArgs().get(1); - - if (uri != null && values != null) { - boolean isFinished = false; - if (Build.VERSION.SDK_INT >= 29 && values.containsKey(MediaStore.MediaColumns.IS_PENDING)) { - isFinished = (Integer) values.get(MediaStore.MediaColumns.IS_PENDING) == 0; - } else if (values.containsKey(MediaStore.Images.Media.DATA)) { - isFinished = true; - } - - if (isFinished) { - SessionManager.add(uri); - } - } - } - return chain.proceed(); - }); - } catch (Throwable ignored) {} - - // 8. その他システムフック群 - try { - Class callbackClass = Class.forName("android.hardware.camera2.CameraManager$AvailabilityCallback", true, param.getClassLoader()); - hook(callbackClass.getDeclaredMethod("onCameraUnavailable", String.class)).intercept(chain -> { - log(Log.INFO, TAG, "Blocked onCameraUnavailable for ID: " + ((List) chain.getArgs()).get(0)); - return null; - }); - } catch (Throwable ignored) {} - - try { - Class biometricClass = Class.forName("android.hardware.biometrics.BiometricManager", true, param.getClassLoader()); - hook(biometricClass.getDeclaredMethod("canAuthenticate", int.class)).intercept(chain -> 0); - } catch (Throwable ignored) {} - } - - // --- ヘルパーメソッド群 --- - - private boolean isCameraPackage(String pkg) { - if (pkg == null) return false; - return pkg.equals("com.android.camera") || - pkg.contains("GoogleCamera") || - pkg.equals("org.codeaurora.snapcam") || - pkg.contains("camera"); - } - - private boolean isCameraActivity(Activity act) { - try { - return act != null && isCameraPackage(act.getPackageName()); - } - catch (Exception e) { - try { return act.getClass().getName().contains("camera"); } - catch (Exception e2) { return false; } - } - } - - private boolean isCameraContext(Context ctx) { - if (ctx == null) return false; - try { return isCameraPackage(ctx.getPackageName()); } - catch (Exception e) { return ctx.getClass().getName().contains("camera"); } - } - - private boolean isDecorView(View v) { - if (v == null) return false; - return v.getClass().getName().endsWith("DecorView"); - } - - private void handleGalleryRedirect(Context ctx, Intent intent) { - if (ctx == null || intent == null || intent.getAction() == null) return; - if (!SessionManager.isActive) return; - - try { - if (!isCameraPackage(ctx.getPackageName())) return; - } catch (Exception e) { return; } - - String action = intent.getAction(); - // GCamのレビューアクションを確実に捉える - boolean isGallery = Intent.ACTION_VIEW.equals(action) || - Intent.ACTION_PICK.equals(action) || - action.contains("REVIEW") || - action.contains("STILL_IMAGE_CAMERA"); - - if (isGallery) { - Log.i(TAG, "Redirecting to SecureViewer: Force hijacking intent"); - - ArrayList uriList = new ArrayList<>(SessionManager.SESSION_URIS); - if (uriList.isEmpty() && intent.getData() != null) { - uriList.add(intent.getData()); - } - - // 既存のインテントを完全に再利用・改変して、システムを騙す - intent.setComponent(new ComponentName( - "com.github.droserasprout.lockscreencamera", - "com.github.droserasprout.lockscreencamera.SecureViewerActivity" - )); - - // Googleフォトがリストに並ぶのを防ぐ - intent.setPackage(null); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { - intent.setSelector(null); - } - - // 複数枚のプレビューに対応させるためのClipData - if (!uriList.isEmpty()) { - ClipData clipData = ClipData.newRawUri("Photos", uriList.get(0)); - for (int i = 1; i < uriList.size(); i++) { - clipData.addItem(new ClipData.Item(uriList.get(i))); - } - intent.setClipData(clipData); - } - - intent.putParcelableArrayListExtra("session_photos_list", uriList); - - // ロック解除要求を回避するためのフラグ群 - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | - Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | // 永続権限 - Intent.FLAG_ACTIVITY_NEW_TASK | - Intent.FLAG_ACTIVITY_CLEAR_TOP | - Intent.FLAG_ACTIVITY_NO_ANIMATION); - - // ロック画面上に表示するための隠しフラグ (SHOW_WHEN_LOCKED) - intent.addFlags(0x00080000 | 0x00400000 | 0x00200000); - - // ここで startActivity を呼ぶのではなく、この intent 自体を書き換えた状態で - // 元のメソッド(proceed)に戻すことで、カメラアプリに「自分の意志で」起動させる - Log.d(TAG, "Intent modification complete. Proceeding with hijacked intent."); - } + DecorViewProtectionHook.install(this); + KeyguardDismissBlockHook.install(this); + ActivityVisibilitySpoofHook.install(this); + KeyguardIntentRewriteHook.install(this); + ViewVisibilityProtectionHook.install(this); + GalleryRedirectHook.install(this); + CameraActivityLifecycleHook.install(this); + MediaStoreSessionTrackingHook.install(this); + MiscSystemHook.install(this, param); } - // システムサーバー側の起動ロジックを最適化 - @Override + @Override public void onSystemServerStarting(@NonNull SystemServerStartingParam param) { - try { - Class gestureClass = Class.forName("com.android.server.GestureLauncherService", true, param.getClassLoader()); - Method handleCameraGesture = gestureClass.getDeclaredMethod("handleCameraGesture", boolean.class, int.class); - - hook(handleCameraGesture).intercept(chain -> { - try { - Method getContextMethod = chain.getThisObject().getClass().getMethod("getContext"); - Context context = (Context) getContextMethod.invoke(chain.getThisObject()); - - KeyguardManager km = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); - boolean isLocked = (km != null && km.isKeyguardLocked()); - - // 1. デフォルトカメラを解決 - PackageManager pm = context.getPackageManager(); - Intent resolveIntent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE); - ResolveInfo info = pm.resolveActivity(resolveIntent, PackageManager.MATCH_DEFAULT_ONLY); - - // SECURE が解決できない場合は標準アクションにフォールバック - if (info == null || info.activityInfo.name.contains("Resolver")) { - resolveIntent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA); - info = pm.resolveActivity(resolveIntent, PackageManager.MATCH_DEFAULT_ONLY); - } - - String targetPkg = "com.android.camera"; - String targetCls = "com.android.camera.Camera"; - - // 2. 解決結果が有効かつ Resolver でない場合はそちらを優先 - if (info != null && !info.activityInfo.name.contains("Resolver")) { - targetPkg = info.activityInfo.packageName; - targetCls = info.activityInfo.name; - } - - // 3. Intent 生成 - Intent intent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE); - - // 4. MIUI カメラの場合は Resolver を完全にスキップするため Component を強制指定 - // GCam などのサードパーティ製がデフォルトに設定されている場合も同様に明示起動 - if (targetPkg != null && targetCls != null) { - intent.setComponent(new ComponentName(targetPkg, targetCls)); - } - - int flags = 0x00040000 | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP; - - if (isLocked) { - flags |= Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS; - intent.putExtra("com.miui.camera.extra.START_BY_KEYGUARD", true); - intent.putExtra("StartActivityWhenLocked", true); - intent.putExtra("is_secure_camera", true); - } else { - intent.putExtra("com.miui.camera.extra.START_BY_KEYGUARD", false); - intent.putExtra("StartActivityWhenLocked", false); - } - - intent.addFlags(flags); - intent.putExtra("android.intent.extra.CAMERA_OPEN_ONLY", true); - intent.putExtra("com.android.systemui.camera_launch_source", "lockscreen_affordance"); - - ActivityOptions options = ActivityOptions.makeBasic(); - if (Build.VERSION.SDK_INT >= 34) { - options.setPendingIntentBackgroundActivityStartMode(2); - } - - // SessionManager.start() はカメラアプリ側の onCreate で実行されるためここでは呼ばない - context.startActivity(intent, options.toBundle()); - return true; - } catch (Throwable t) { - log(Log.ERROR, TAG, "Failed to launch", t); - } - return chain.proceed(); - }); - } catch (Throwable t) { - log(Log.WARN, TAG, "System hook skipped", t); - } - } - private void applyWindowAndBufferFixes(Activity activity) { - try { - activity.setShowWhenLocked(true); - activity.setTurnScreenOn(true); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { - activity.setInheritShowWhenLocked(true); - } - - Window window = activity.getWindow(); - if (window != null) { - window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE); - window.setFormat(PixelFormat.TRANSLUCENT); - - WindowManager.LayoutParams lp = window.getAttributes(); - lp.flags |= WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED - | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON - | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON - | WindowManager.LayoutParams.FLAG_FULLSCREEN - | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON; - - // 描画レイヤーを最前面に固定し、背面に隠れるのを防ぐ - lp.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN - | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; - - lp.flags &= ~WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD; - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; - } - window.setAttributes(lp); - window.addFlags(lp.flags); - - window.setFlags( - WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, - WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED - ); - - View decorView = window.getDecorView(); - if (decorView != null) { - decorView.setAlpha(1.0f); - decorView.setVisibility(View.VISIBLE); - decorView.requestFocus(); - } - } - - for (String fieldName : TARGET_BOOLEAN_FIELDS) { - setFieldFast(activity, fieldName, true); - } - setFieldFast(activity, "mIsNormalIntent", false); - setFieldFast(activity, "mShowEnteringAnimation", false); - setFieldFast(activity, "mKeyguardStatus", 1); - setFieldFast(activity, "mIsSecureCameraId", 0); - - } catch (Throwable t) { - log(Log.DEBUG, TAG, "UI Fixes failed: " + t.toString()); - } - } - - private void setFieldFast(Object obj, String fieldName, Object value) { - try { - Class current = obj.getClass(); - String cacheKey = current.getName() + ":" + fieldName; - Field f = fieldCache.get(cacheKey); - - if (f == null && !fieldCache.containsKey(cacheKey)) { - while (current != null && !current.getName().equals("android.app.Activity")) { - try { - f = current.getDeclaredField(fieldName); - f.setAccessible(true); - break; - } catch (NoSuchFieldException e) { - current = current.getSuperclass(); - } - } - fieldCache.put(cacheKey, f); - } - - if (f != null) f.set(obj, value); - } catch (Throwable ignored) {} + CameraGestureLauncherHook.install(this, param); } - } +} diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/SecureViewerActivity.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/SecureViewerActivity.java index 30af12b..3638cff 100644 --- a/app/src/main/java/com/github/droserasprout/lockscreencamera/SecureViewerActivity.java +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/SecureViewerActivity.java @@ -5,47 +5,41 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.util.Log; -import android.util.TypedValue; -import android.view.MotionEvent; import android.view.ViewGroup; import android.view.WindowManager; -import android.widget.FrameLayout; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.RecyclerView; import androidx.viewpager2.widget.ViewPager2; -import com.github.chrisbanes.photoview.PhotoView; +import com.github.droserasprout.lockscreencamera.ui.PhotoAdapter; +import com.github.droserasprout.lockscreencamera.ui.SwipeDismissLayout; -import java.io.InputStream; -import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; -import java.util.concurrent.ConcurrentHashMap; +/** + * ロック画面上でセッション中の写真をスワイプ閲覧するための Activity。 + * FLAG_SECURE を維持し、スクリーンショット・画面録画からは保護する。 + */ public class SecureViewerActivity extends Activity { private static final String TAG = "SecureViewer"; + private static final String EXTRA_SESSION_PHOTOS = "session_photos_list"; - // 並列デコードのためスレッド数を2に拡張 + // 並列デコードのためスレッド数を2に確保 private final ExecutorService executor = Executors.newFixedThreadPool(2); - // position → 実行中タスク のマップ(高速スワイプ時の古いタスクをキャンセルするため) private final ConcurrentHashMap> pendingTasks = new ConcurrentHashMap<>(); private BroadcastReceiver screenOffReceiver; private ViewPager2 viewPager; - private PhotoAdapter adapter; @Override protected void onCreate(Bundle savedInstanceState) { @@ -56,25 +50,26 @@ protected void onCreate(Bundle savedInstanceState) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { setInheritShowWhenLocked(true); } - // SecureViewerActivity では FLAG_SECURE を意図的に維持する - // (LockscreenCamera の clearFlags フックとは異なり、ここではスクリーンショット禁止が必要) - getWindow().setFlags( - WindowManager.LayoutParams.FLAG_SECURE, - WindowManager.LayoutParams.FLAG_SECURE - ); + // LockscreenCamera 側の clearFlags フックとは異なり、 + // ここではスクリーンショット禁止のため FLAG_SECURE を意図的に維持する + getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE); - List uris = getIntent().getParcelableArrayListExtra("session_photos_list"); - if (uris == null || uris.isEmpty()) { - Uri singleUri = getIntent().getData(); - if (singleUri != null) { - uris = new ArrayList<>(); - uris.add(singleUri); - } + List safeUris = resolveSafeUris(); + if (safeUris.isEmpty()) { + finish(); + return; } + setupViewPager(safeUris); + registerScreenOffReceiver(); + } + + private List resolveSafeUris() { + List uris = getIntent().getParcelableArrayListExtra(EXTRA_SESSION_PHOTOS); if (uris == null || uris.isEmpty()) { - finish(); - return; + uris = new ArrayList<>(); + Uri singleUri = getIntent().getData(); + if (singleUri != null) uris.add(singleUri); } // URI のスキームを検証し content:// 以外を除外する @@ -86,28 +81,27 @@ protected void onCreate(Bundle savedInstanceState) { Log.w(TAG, "Rejected non-content URI: " + uri.getScheme()); } } + return safeUris; + } - if (safeUris.isEmpty()) { - finish(); - return; - } - + private void setupViewPager(List safeUris) { viewPager = new ViewPager2(this); viewPager.setLayoutParams(new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - )); + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); viewPager.setBackgroundColor(0xFF000000); viewPager.setOffscreenPageLimit(1); - SwipeDismissLayout container = new SwipeDismissLayout(this); + SwipeDismissLayout container = new SwipeDismissLayout(this, viewPager, this::finish); container.addView(viewPager); setContentView(container); - adapter = new PhotoAdapter(safeUris, this, executor, pendingTasks); + PhotoAdapter adapter = new PhotoAdapter(safeUris, this, executor, pendingTasks); viewPager.setAdapter(adapter); viewPager.setCurrentItem(safeUris.size() - 1, false); + } + private void registerScreenOffReceiver() { screenOffReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -127,24 +121,15 @@ public void onReceive(Context context, Intent intent) { protected void onStop() { super.onStop(); // onDestroy より早い段階で解除することで、プロセスが強制終了する前にも対応 - if (screenOffReceiver != null) { - try { - unregisterReceiver(screenOffReceiver); - } catch (Exception ignored) {} - screenOffReceiver = null; - } + unregisterScreenOffReceiver(); } @Override protected void onDestroy() { super.onDestroy(); - // onStop で解除済みでも念のりガード - if (screenOffReceiver != null) { - try { - unregisterReceiver(screenOffReceiver); - } catch (Exception ignored) {} - screenOffReceiver = null; - } + // onStop で解除済みでも念のためガードする + unregisterScreenOffReceiver(); + // 実行中タスクを全キャンセル for (Future task : pendingTasks.values()) { task.cancel(true); @@ -153,216 +138,13 @@ protected void onDestroy() { executor.shutdownNow(); } - // ========================================================================= - // ビットマップデコード(static — アクティビティへの暗黙参照を持たない) - // ========================================================================= - - @Nullable - static Bitmap decodeSampledBitmapFromUri( - Context context, Uri uri, int reqWidth, int reqHeight) { + private void unregisterScreenOffReceiver() { + if (screenOffReceiver == null) return; try { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - try (InputStream is = context.getContentResolver().openInputStream(uri)) { - BitmapFactory.decodeStream(is, null, options); - } - options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); - options.inJustDecodeBounds = false; - try (InputStream is = context.getContentResolver().openInputStream(uri)) { - return BitmapFactory.decodeStream(is, null, options); - } - } catch (Exception e) { - Log.e(TAG, "Failed to decode: " + uri, e); - return null; - } - } - - static int calculateInSampleSize( - BitmapFactory.Options options, int reqWidth, int reqHeight) { - final int height = options.outHeight; - final int width = options.outWidth; - int inSampleSize = 1; - if (height > reqHeight || width > reqWidth) { - final int halfHeight = height / 2; - final int halfWidth = width / 2; - while ((halfHeight / inSampleSize) >= reqHeight - && (halfWidth / inSampleSize) >= reqWidth) { - inSampleSize *= 2; - } - } - return inSampleSize; - } - - // ========================================================================= - // SwipeDismissLayout - // ========================================================================= - - private class SwipeDismissLayout extends FrameLayout { - private float initialY; - private float initialX; - private boolean isSwiping; - // dp → px 変換でハードコードピクセル値を排除 - private final float swipeStartThresholdPx; - private final float swipeDismissThresholdPx; - - SwipeDismissLayout(Context context) { - super(context); - swipeDismissThresholdPx = - context.getResources().getDisplayMetrics().heightPixels * 0.2f; - swipeStartThresholdPx = TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, 16, - context.getResources().getDisplayMetrics() - ); - } - - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - switch (ev.getActionMasked()) { - case MotionEvent.ACTION_DOWN: - initialY = ev.getY(); - initialX = ev.getX(); - isSwiping = false; - break; - case MotionEvent.ACTION_MOVE: - float dy = ev.getY() - initialY; - float dx = Math.abs(ev.getX() - initialX); - // 下方向かつ水平移動より大きい場合のみ dismiss スワイプとして横取り - if (dy > swipeStartThresholdPx && dy > dx) { - isSwiping = true; - return true; - } - break; - } - return super.onInterceptTouchEvent(ev); - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - if (!isSwiping) return super.onTouchEvent(ev); - switch (ev.getActionMasked()) { - case MotionEvent.ACTION_MOVE: - float dy = ev.getY() - initialY; - float translationY = Math.max(0, dy); - float progress = getHeight() > 0 ? translationY / getHeight() : 0; - viewPager.setTranslationY(translationY); - viewPager.setAlpha(1.0f - (progress * 0.8f)); - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - if (viewPager.getTranslationY() > swipeDismissThresholdPx) { - viewPager.setAlpha(0f); - finish(); - } else { - viewPager.animate() - .translationY(0) - .alpha(1.0f) - .setDuration(200) - .withStartAction(() -> isSwiping = false) - .start(); - } - break; - } - return true; - } - } - - // ========================================================================= - // PhotoAdapter(静的内部クラス — アクティビティへの暗黙参照を持たない) - // ========================================================================= - - private static class PhotoAdapter extends RecyclerView.Adapter { - - private final List uris; - private final WeakReference activityRef; - private final ExecutorService executor; - private final ConcurrentHashMap> pendingTasks; - - PhotoAdapter( - List uris, - Activity activity, - ExecutorService executor, - ConcurrentHashMap> pendingTasks) { - this.uris = uris; - this.activityRef = new WeakReference<>(activity); - this.executor = executor; - this.pendingTasks = pendingTasks; - } - - @NonNull - @Override - public PhotoViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - PhotoView photoView = new PhotoView(parent.getContext()); - photoView.setBackgroundColor(0xFF000000); - photoView.setLayoutParams(new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - )); - photoView.setZoomable(true); - return new PhotoViewHolder(photoView); - } - - @Override - public void onBindViewHolder(@NonNull PhotoViewHolder holder, int position) { - holder.photoView.setImageDrawable(null); - - // 同じポジションの前回タスクをキャンセルして競合を防ぐ - Future prev = pendingTasks.remove(position); - if (prev != null) prev.cancel(true); - - Activity activity = activityRef.get(); - if (activity == null || activity.isDestroyed()) return; - - int screenWidth = activity.getResources().getDisplayMetrics().widthPixels; - int screenHeight = activity.getResources().getDisplayMetrics().heightPixels; - // Context のみ渡し、アクティビティへの強参照をラムダに持ち込まない - Context appContext = activity.getApplicationContext(); - Uri uri = uris.get(position); - - Future task = executor.submit(() -> { - Bitmap bitmap = decodeSampledBitmapFromUri( - appContext, uri, screenWidth, screenHeight); - if (bitmap == null) return; - - Activity act = activityRef.get(); - if (act == null || act.isDestroyed()) { - bitmap.recycle(); - return; - } - - act.runOnUiThread(() -> { - pendingTasks.remove(position); - // バインド時のポジションと現在のポジションが一致する場合のみ反映 - if (holder.getAdapterPosition() == position) { - holder.photoView.setImageBitmap(bitmap); - } else { - // ポジションずれが発生した場合はネイティブメモリを即解放 - bitmap.recycle(); - } - }); - }); - - pendingTasks.put(position, task); - } - - @Override - public void onViewRecycled(@NonNull PhotoViewHolder holder) { - super.onViewRecycled(holder); - // Drawable 参照を切り、GC がビットマップを回収できるようにする - holder.photoView.setImageDrawable(null); - } - - @Override - public int getItemCount() { - return uris.size(); - } - - static class PhotoViewHolder extends RecyclerView.ViewHolder { - final PhotoView photoView; - - PhotoViewHolder(PhotoView pv) { - super(pv); - photoView = pv; - } + unregisterReceiver(screenOffReceiver); + } catch (Exception ignored) { + // 既に解除済み } + screenOffReceiver = null; } } diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/ActivityVisibilitySpoofHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/ActivityVisibilitySpoofHook.java new file mode 100644 index 0000000..eeb547d --- /dev/null +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/ActivityVisibilitySpoofHook.java @@ -0,0 +1,26 @@ +package com.github.droserasprout.lockscreencamera.hook; + +import android.app.Activity; + +import com.github.droserasprout.lockscreencamera.util.CameraPackageUtil; + +import io.github.libxposed.api.XposedModule; + +/** カメラ Activity の hasWindowFocus/isResumed を常に true として返す(フォーカス喪失の誤検知対策)。 */ +public final class ActivityVisibilitySpoofHook { + + private ActivityVisibilitySpoofHook() {} + + public static void install(XposedModule module) { + try { + module.hook(Activity.class.getDeclaredMethod("hasWindowFocus")).intercept(chain -> { + if (CameraPackageUtil.isCameraActivity((Activity) chain.getThisObject())) return true; + return (Boolean) chain.proceed(); + }); + module.hook(Activity.class.getDeclaredMethod("isResumed")).intercept(chain -> { + if (CameraPackageUtil.isCameraActivity((Activity) chain.getThisObject())) return true; + return (Boolean) chain.proceed(); + }); + } catch (Throwable ignored) {} + } +} diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraActivityLifecycleHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraActivityLifecycleHook.java new file mode 100644 index 0000000..f5d689b --- /dev/null +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraActivityLifecycleHook.java @@ -0,0 +1,144 @@ +package com.github.droserasprout.lockscreencamera.hook; + +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; + +import com.github.droserasprout.lockscreencamera.session.SessionManager; +import com.github.droserasprout.lockscreencamera.util.CameraPackageUtil; + +import io.github.libxposed.api.XposedModule; + +/** + * カメラ Activity のライフサイクルをフックし、 + * - ロック画面起動時に SessionManager を開始 + * - 画面 OFF で自動終了させる BroadcastReceiver を登録 + * - onDestroy でセッション・レシーバーを後始末する + * - ライフサイクル中は常にウィンドウのロック画面バイパス属性を再適用する({@link WindowSecurityBypass}) + * + * 修正メモ: 元の実装は onCreate で毎回 BroadcastReceiver を register していたが、 + * unregister する場所がどこにも無かった(onDestroy は SessionManager.end() のみ呼んでいた)。 + * ここでは Activity ごとに登録したレシーバーを WeakHashMap で保持し、onDestroy で確実に解除する。 + */ +public final class CameraActivityLifecycleHook { + + private static final String TAG = "LockscreenCamera"; + private static final String EXTRA_START_BY_KEYGUARD = "com.miui.camera.extra.START_BY_KEYGUARD"; + private static final String[] LIFECYCLE_METHODS = + {"attachBaseContext", "onCreate", "onStart", "onResume", "onWindowFocusChanged", "onDestroy"}; + + private static final Map ACTIVE_RECEIVERS = new WeakHashMap<>(); + + private CameraActivityLifecycleHook() {} + + public static void install(XposedModule module) { + for (String methodName : LIFECYCLE_METHODS) { + try { + Method method = resolveMethod(methodName); + final String mName = methodName; + module.hook(method).intercept(chain -> { + Object thisObj = chain.getThisObject(); + if (!(thisObj instanceof Activity)) return chain.proceed(); + + Activity act = (Activity) thisObj; + if (!CameraPackageUtil.isCameraActivity(act)) return chain.proceed(); + + if ("onDestroy".equals(mName)) { + if (SessionManager.isActive) { + SessionManager.end(); + } + unregisterReceiver(act); + return chain.proceed(); + } + + if ("onWindowFocusChanged".equals(mName)) { + boolean hasFocus = (boolean) ((List) chain.getArgs()).get(0); + if (!hasFocus) return chain.proceed(); + WindowSecurityBypass.apply(act); + return chain.proceed(); + } + + if ("onCreate".equals(mName)) { + Object res = chain.proceed(); + + Intent intent = act.getIntent(); + boolean isLockscreenLaunch = + intent != null && intent.getBooleanExtra(EXTRA_START_BY_KEYGUARD, false); + + if (isLockscreenLaunch) { + SessionManager.start(); + Log.i(TAG, "Secure Lockscreen launch detected. Session Started."); + registerScreenOffReceiver(act); + } + + WindowSecurityBypass.apply(act); + return res; + } + + WindowSecurityBypass.apply(act); + return chain.proceed(); + }); + } catch (Throwable ignored) { + // このメソッドがこの OS バージョンに存在しない場合はスキップ + } + } + } + + private static Method resolveMethod(String methodName) throws NoSuchMethodException { + switch (methodName) { + case "attachBaseContext": + return ContextWrapper.class.getDeclaredMethod("attachBaseContext", Context.class); + case "onCreate": + return Activity.class.getDeclaredMethod("onCreate", Bundle.class); + case "onWindowFocusChanged": + return Activity.class.getDeclaredMethod("onWindowFocusChanged", boolean.class); + case "onDestroy": + return Activity.class.getDeclaredMethod("onDestroy"); + default: + return Activity.class.getDeclaredMethod(methodName); + } + } + + private static void registerScreenOffReceiver(Activity act) { + IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_OFF); + BroadcastReceiver receiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent i) { + SessionManager.end(); + act.finish(); + } + }; + + try { + if (Build.VERSION.SDK_INT >= 33) { + act.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED); + } else { + act.registerReceiver(receiver, filter); + } + ACTIVE_RECEIVERS.put(act, receiver); + } catch (Exception e) { + Log.w(TAG, "Failed to register receiver: " + e.getMessage()); + } + } + + private static void unregisterReceiver(Activity act) { + BroadcastReceiver receiver = ACTIVE_RECEIVERS.remove(act); + if (receiver == null) return; + try { + act.unregisterReceiver(receiver); + } catch (Exception ignored) { + // 既に解除済み、または登録に失敗していた場合 + } + } +} diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraGestureLauncherHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraGestureLauncherHook.java new file mode 100644 index 0000000..433d352 --- /dev/null +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraGestureLauncherHook.java @@ -0,0 +1,110 @@ +package com.github.droserasprout.lockscreencamera.hook; + +import android.app.ActivityOptions; +import android.app.KeyguardManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Build; +import android.provider.MediaStore; +import android.util.Log; + +import java.lang.reflect.Method; + +import io.github.libxposed.api.XposedModule; +import io.github.libxposed.api.XposedModuleInterface.SystemServerStartingParam; + +/** + * 電源ボタン二度押し等の「カメラジェスチャー」ハンドラ (GestureLauncherService) をフックし、 + * ロック中でも Secure Camera Intent を明示的な Component 指定で起動する。 + * MIUI の Resolver 経由起動やサードパーティ製カメラアプリが優先されない問題を回避する。 + */ +public final class CameraGestureLauncherHook { + + private static final String TAG = "LockscreenCamera"; + private static final String EXTRA_START_BY_KEYGUARD = "com.miui.camera.extra.START_BY_KEYGUARD"; + private static final int FLAG_ACTIVITY_MATCH_EXTERNAL = 0x00040000; // hidden flag + private static final int BACKGROUND_ACTIVITY_START_ALLOWED = 2; + + private CameraGestureLauncherHook() {} + + public static void install(XposedModule module, SystemServerStartingParam param) { + try { + Class gestureClass = Class.forName( + "com.android.server.GestureLauncherService", true, param.getClassLoader()); + Method handleCameraGesture = gestureClass.getDeclaredMethod("handleCameraGesture", boolean.class, int.class); + + module.hook(handleCameraGesture).intercept(chain -> { + try { + Method getContextMethod = chain.getThisObject().getClass().getMethod("getContext"); + Context context = (Context) getContextMethod.invoke(chain.getThisObject()); + launchSecureCamera(context); + return true; + } catch (Throwable t) { + module.log(Log.ERROR, TAG, "Failed to launch", t); + } + return chain.proceed(); + }); + } catch (Throwable t) { + module.log(Log.WARN, TAG, "System hook skipped", t); + } + } + + private static void launchSecureCamera(Context context) { + KeyguardManager km = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); + boolean isLocked = km != null && km.isKeyguardLocked(); + + ComponentName target = resolveCameraComponent(context); + + Intent intent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE); + if (target != null) { + intent.setComponent(target); + } + + int flags = FLAG_ACTIVITY_MATCH_EXTERNAL | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP; + + if (isLocked) { + flags |= Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS; + intent.putExtra(EXTRA_START_BY_KEYGUARD, true); + intent.putExtra("StartActivityWhenLocked", true); + intent.putExtra("is_secure_camera", true); + } else { + intent.putExtra(EXTRA_START_BY_KEYGUARD, false); + intent.putExtra("StartActivityWhenLocked", false); + } + + intent.addFlags(flags); + intent.putExtra("android.intent.extra.CAMERA_OPEN_ONLY", true); + intent.putExtra("com.android.systemui.camera_launch_source", "lockscreen_affordance"); + + ActivityOptions options = ActivityOptions.makeBasic(); + if (Build.VERSION.SDK_INT >= 34) { + options.setPendingIntentBackgroundActivityStartMode(BACKGROUND_ACTIVITY_START_ALLOWED); + } + + // SessionManager.start() はカメラアプリ側の onCreate (CameraActivityLifecycleHook) で + // 実行されるため、ここでは呼ばない + context.startActivity(intent, options.toBundle()); + } + + private static ComponentName resolveCameraComponent(Context context) { + PackageManager pm = context.getPackageManager(); + + Intent resolveIntent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE); + ResolveInfo info = pm.resolveActivity(resolveIntent, PackageManager.MATCH_DEFAULT_ONLY); + + // SECURE アクションを解決できない場合は通常アクションにフォールバック + if (info == null || info.activityInfo.name.contains("Resolver")) { + resolveIntent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA); + info = pm.resolveActivity(resolveIntent, PackageManager.MATCH_DEFAULT_ONLY); + } + + if (info != null && !info.activityInfo.name.contains("Resolver")) { + return new ComponentName(info.activityInfo.packageName, info.activityInfo.name); + } + // 解決に失敗した場合の最終フォールバック(AOSP 標準カメラ) + return new ComponentName("com.android.camera", "com.android.camera.Camera"); + } +} diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/DecorViewProtectionHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/DecorViewProtectionHook.java new file mode 100644 index 0000000..7b5784b --- /dev/null +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/DecorViewProtectionHook.java @@ -0,0 +1,46 @@ +package com.github.droserasprout.lockscreencamera.hook; + +import android.util.Log; +import android.view.View; + +import java.util.List; + +import com.github.droserasprout.lockscreencamera.util.CameraPackageUtil; + +import io.github.libxposed.api.XposedModule; + +/** + * DecorView(Activity のルートビュー)が setAlpha(0) や setVisibility(GONE) で + * 非表示化・透明化されるのを物理的に防ぐ。 + */ +public final class DecorViewProtectionHook { + + private static final String TAG = "LockscreenCamera"; + + private DecorViewProtectionHook() {} + + public static void install(XposedModule module) { + try { + module.hook(View.class.getDeclaredMethod("setAlpha", float.class)).intercept(chain -> { + View view = (View) chain.getThisObject(); + if (CameraPackageUtil.isDecorView(view) && CameraPackageUtil.isCameraContext(view.getContext())) { + List args = chain.getArgs(); + float alpha = (float) args.get(0); + if (alpha < 1.0f) args.set(0, 1.0f); + } + return chain.proceed(); + }); + module.hook(View.class.getDeclaredMethod("setVisibility", int.class)).intercept(chain -> { + View view = (View) chain.getThisObject(); + if (CameraPackageUtil.isDecorView(view) && CameraPackageUtil.isCameraContext(view.getContext())) { + List args = chain.getArgs(); + int vis = (int) args.get(0); + if (vis != View.VISIBLE) args.set(0, View.VISIBLE); + } + return chain.proceed(); + }); + } catch (Throwable t) { + module.log(Log.WARN, TAG, "DecorViewProtectionHook install failed: " + t); + } + } +} diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/GalleryRedirectHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/GalleryRedirectHook.java new file mode 100644 index 0000000..94406f6 --- /dev/null +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/GalleryRedirectHook.java @@ -0,0 +1,109 @@ +package com.github.droserasprout.lockscreencamera.hook; + +import android.app.Activity; +import android.content.ClipData; +import android.content.ComponentName; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.util.Log; + +import java.lang.reflect.Method; +import java.util.ArrayList; + +import com.github.droserasprout.lockscreencamera.session.SessionManager; +import com.github.droserasprout.lockscreencamera.util.CameraPackageUtil; + +import io.github.libxposed.api.XposedModule; + +/** + * カメラアプリがシステムの「ギャラリー確認画面」を開こうとした際、 + * 独自の SecureViewerActivity にリダイレクトすることでロック中でも写真プレビューを可能にする。 + */ +public final class GalleryRedirectHook { + + private static final String TAG = "LockscreenCamera"; + private static final String VIEWER_PACKAGE = "com.github.droserasprout.lockscreencamera"; + private static final String VIEWER_CLASS = "com.github.droserasprout.lockscreencamera.SecureViewerActivity"; + + // ロック画面上に表示するための隠しフラグ (SHOW_WHEN_LOCKED 系) + private static final int FLAG_SHOW_WHEN_LOCKED_HIDDEN = 0x00080000 | 0x00400000 | 0x00200000; + + private GalleryRedirectHook() {} + + public static void install(XposedModule module) { + try { + Method startAct = Activity.class.getDeclaredMethod("startActivity", Intent.class); + module.hook(startAct).intercept(chain -> { + handleGalleryRedirect((Context) chain.getThisObject(), (Intent) chain.getArgs().get(0)); + return chain.proceed(); + }); + + Method startRes = Activity.class.getDeclaredMethod("startActivityForResult", Intent.class, int.class); + module.hook(startRes).intercept(chain -> { + handleGalleryRedirect((Context) chain.getThisObject(), (Intent) chain.getArgs().get(0)); + return chain.proceed(); + }); + + Method startCtx = ContextWrapper.class.getDeclaredMethod("startActivity", Intent.class); + module.hook(startCtx).intercept(chain -> { + handleGalleryRedirect((Context) chain.getThisObject(), (Intent) chain.getArgs().get(0)); + return chain.proceed(); + }); + } catch (Throwable t) { + module.log(Log.ERROR, TAG, "Failed to hook gallery redirect", t); + } + } + + private static void handleGalleryRedirect(Context ctx, Intent intent) { + if (ctx == null || intent == null || intent.getAction() == null) return; + if (!SessionManager.isActive) return; + + try { + if (!CameraPackageUtil.isCameraPackage(ctx.getPackageName())) return; + } catch (Exception e) { + return; + } + + String action = intent.getAction(); + boolean isGallery = Intent.ACTION_VIEW.equals(action) + || Intent.ACTION_PICK.equals(action) + || action.contains("REVIEW") + || action.contains("STILL_IMAGE_CAMERA"); + if (!isGallery) return; + + Log.i(TAG, "Redirecting to SecureViewer: Force hijacking intent"); + + ArrayList uriList = new ArrayList<>(SessionManager.SESSION_URIS); + if (uriList.isEmpty() && intent.getData() != null) { + uriList.add(intent.getData()); + } + + // 既存の intent を書き換えて再利用し、システムを騙す + intent.setComponent(new ComponentName(VIEWER_PACKAGE, VIEWER_CLASS)); + intent.setPackage(null); // Google フォトなどが候補に並ぶのを防ぐ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + intent.setSelector(null); + } + + if (!uriList.isEmpty()) { + ClipData clipData = ClipData.newRawUri("Photos", uriList.get(0)); + for (int i = 1; i < uriList.size(); i++) { + clipData.addItem(new ClipData.Item(uriList.get(i))); + } + intent.setClipData(clipData); + } + intent.putParcelableArrayListExtra("session_photos_list", uriList); + + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION + | Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_CLEAR_TOP + | Intent.FLAG_ACTIVITY_NO_ANIMATION); + intent.addFlags(FLAG_SHOW_WHEN_LOCKED_HIDDEN); + + Log.d(TAG, "Intent modification complete. Proceeding with hijacked intent."); + } +} diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/KeyguardDismissBlockHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/KeyguardDismissBlockHook.java new file mode 100644 index 0000000..e2a3513 --- /dev/null +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/KeyguardDismissBlockHook.java @@ -0,0 +1,30 @@ +package com.github.droserasprout.lockscreencamera.hook; + +import android.app.Activity; +import android.app.KeyguardManager; +import android.util.Log; + +import java.lang.reflect.Method; + +import io.github.libxposed.api.XposedModule; + +/** requestDismissKeyguard(PIN 画面表示要求)を完全にブロックする。 */ +public final class KeyguardDismissBlockHook { + + private static final String TAG = "LockscreenCamera"; + + private KeyguardDismissBlockHook() {} + + public static void install(XposedModule module) { + try { + Method dismissMethod = KeyguardManager.class.getDeclaredMethod( + "requestDismissKeyguard", Activity.class, KeyguardManager.KeyguardDismissCallback.class); + module.hook(dismissMethod).intercept(chain -> { + module.log(Log.WARN, TAG, "BLOCKED: requestDismissKeyguard (Preventing PIN screen)"); + return null; + }); + } catch (Throwable ignored) { + // このデバイス/OS バージョンに該当メソッドが存在しない場合はスキップ + } + } +} diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/KeyguardIntentRewriteHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/KeyguardIntentRewriteHook.java new file mode 100644 index 0000000..73595ce --- /dev/null +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/KeyguardIntentRewriteHook.java @@ -0,0 +1,31 @@ +package com.github.droserasprout.lockscreencamera.hook; + +import android.app.Activity; +import android.content.Intent; + +import com.github.droserasprout.lockscreencamera.util.CameraPackageUtil; + +import io.github.libxposed.api.XposedModule; + +/** getIntent() の動的書き換え:MIUI のキーガード起動フラグから is_secure_camera 等を補完する。 */ +public final class KeyguardIntentRewriteHook { + + private static final String EXTRA_START_BY_KEYGUARD = "com.miui.camera.extra.START_BY_KEYGUARD"; + + private KeyguardIntentRewriteHook() {} + + public static void install(XposedModule module) { + try { + module.hook(Activity.class.getDeclaredMethod("getIntent")).intercept(chain -> { + Intent intent = (Intent) chain.proceed(); + Activity act = (Activity) chain.getThisObject(); + if (CameraPackageUtil.isCameraActivity(act) && intent != null + && intent.getBooleanExtra(EXTRA_START_BY_KEYGUARD, false)) { + intent.putExtra("is_secure_camera", true); + intent.putExtra("ShowCameraWhenLocked", true); + } + return intent; + }); + } catch (Throwable ignored) {} + } +} diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/MediaStoreSessionTrackingHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/MediaStoreSessionTrackingHook.java new file mode 100644 index 0000000..48c26b8 --- /dev/null +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/MediaStoreSessionTrackingHook.java @@ -0,0 +1,67 @@ +package com.github.droserasprout.lockscreencamera.hook; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.net.Uri; +import android.os.Build; +import android.provider.MediaStore; + +import com.github.droserasprout.lockscreencamera.session.SessionManager; + +import io.github.libxposed.api.XposedModule; + +/** + * セッション中に MediaStore へ挿入・更新された画像 URI を追跡し、 + * ギャラリーリダイレクト({@link GalleryRedirectHook})でプレビュー一覧として使えるようにする。 + * + * 修正メモ: insert(Uri, ContentValues) の元の実装は、実際に挿入された行の URI + * (chain.proceed() の戻り値)ではなく第2引数の ContentValues を Uri にキャストしていたため、 + * 呼び出しのたびに ClassCastException が発生していた(フックの例外保護により黙って握りつぶされ、 + * 実質「撮影した写真がセッションに追加されない」状態になっていた)。 + */ +public final class MediaStoreSessionTrackingHook { + + private MediaStoreSessionTrackingHook() {} + + public static void install(XposedModule module) { + installInsertHook(module); + installUpdateHook(module); + } + + private static void installInsertHook(XposedModule module) { + try { + module.hook(ContentResolver.class.getDeclaredMethod("insert", Uri.class, ContentValues.class)) + .intercept(chain -> { + Uri returnedUri = (Uri) chain.proceed(); + if (SessionManager.isActive && returnedUri != null) { + SessionManager.add(returnedUri); + } + return returnedUri; + }); + } catch (Throwable ignored) {} + } + + private static void installUpdateHook(XposedModule module) { + try { + module.hook(ContentResolver.class.getDeclaredMethod( + "update", Uri.class, ContentValues.class, String.class, String[].class)) + .intercept(chain -> { + if (SessionManager.isActive) { + Uri uri = (Uri) chain.getArgs().get(0); + ContentValues values = (ContentValues) chain.getArgs().get(1); + if (uri != null && values != null && isWriteFinished(values)) { + SessionManager.add(uri); + } + } + return chain.proceed(); + }); + } catch (Throwable ignored) {} + } + + private static boolean isWriteFinished(ContentValues values) { + if (Build.VERSION.SDK_INT >= 29 && values.containsKey(MediaStore.MediaColumns.IS_PENDING)) { + return (Integer) values.get(MediaStore.MediaColumns.IS_PENDING) == 0; + } + return values.containsKey(MediaStore.Images.Media.DATA); + } +} diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/MiscSystemHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/MiscSystemHook.java new file mode 100644 index 0000000..ac59849 --- /dev/null +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/MiscSystemHook.java @@ -0,0 +1,35 @@ +package com.github.droserasprout.lockscreencamera.hook; + +import android.util.Log; + +import io.github.libxposed.api.XposedModule; +import io.github.libxposed.api.XposedModuleInterface.PackageReadyParam; + +/** + * その他の細かいシステムフック群: + * - CameraManager.AvailabilityCallback.onCameraUnavailable を無効化 + * - BiometricManager.canAuthenticate を常に「利用可能」として返す + */ +public final class MiscSystemHook { + + private static final String TAG = "LockscreenCamera"; + + private MiscSystemHook() {} + + public static void install(XposedModule module, PackageReadyParam param) { + try { + Class callbackClass = Class.forName( + "android.hardware.camera2.CameraManager$AvailabilityCallback", true, param.getClassLoader()); + module.hook(callbackClass.getDeclaredMethod("onCameraUnavailable", String.class)).intercept(chain -> { + module.log(Log.INFO, TAG, "Blocked onCameraUnavailable for ID: " + chain.getArgs().get(0)); + return null; + }); + } catch (Throwable ignored) {} + + try { + Class biometricClass = Class.forName( + "android.hardware.biometrics.BiometricManager", true, param.getClassLoader()); + module.hook(biometricClass.getDeclaredMethod("canAuthenticate", int.class)).intercept(chain -> 0); + } catch (Throwable ignored) {} + } +} diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/ViewVisibilityProtectionHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/ViewVisibilityProtectionHook.java new file mode 100644 index 0000000..13c8482 --- /dev/null +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/ViewVisibilityProtectionHook.java @@ -0,0 +1,29 @@ +package com.github.droserasprout.lockscreencamera.hook; + +import android.view.View; + +import java.util.List; + +import com.github.droserasprout.lockscreencamera.util.CameraPackageUtil; + +import io.github.libxposed.api.XposedModule; + +/** カメラアプリ内の SurfaceView 等(DecorView 以外)が非表示化されるのを阻止する。 */ +public final class ViewVisibilityProtectionHook { + + private ViewVisibilityProtectionHook() {} + + public static void install(XposedModule module) { + try { + module.hook(View.class.getMethod("setVisibility", int.class)).intercept(chain -> { + View v = (View) chain.getThisObject(); + if (CameraPackageUtil.isCameraContext(v.getContext()) && !CameraPackageUtil.isDecorView(v)) { + List args = chain.getArgs(); + int vis = (int) args.get(0); + if (vis != View.VISIBLE) args.set(0, View.VISIBLE); + } + return chain.proceed(); + }); + } catch (Throwable ignored) {} + } +} diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/WindowSecurityBypass.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/WindowSecurityBypass.java new file mode 100644 index 0000000..07b9e9f --- /dev/null +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/WindowSecurityBypass.java @@ -0,0 +1,86 @@ +package com.github.droserasprout.lockscreencamera.hook; + +import android.app.Activity; +import android.graphics.PixelFormat; +import android.os.Build; +import android.util.Log; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; + +import com.github.droserasprout.lockscreencamera.util.ReflectionFieldUtil; + +/** カメラ Activity のウィンドウ属性・内部フラグをロック画面上で描画可能な状態に強制する。 */ +public final class WindowSecurityBypass { + + private static final String TAG = "LockscreenCamera"; + + // 書き換え対象の内部フィールド名リスト(各 OEM のカメラ実装で使われがちな boolean 系フィールド) + private static final String[] TARGET_BOOLEAN_FIELDS = { + "mIsSecure", "mIsSecureCamera", "mKeyguardLocked", + "mInLockScreen", "mIgnoreKeyguard", "mIsScreenOn", + "mSecureCamera", "mIsKeyguardLocked", "mIsHideForeground", + "mIsGalleryLock", "mIsCaptureIntent", "mIsPortraitIntent", "mIsVideoIntent", + "mUserAuthenticationFlag", "mIgnoreKeyguardCheck", + "mIsCameraApp", "mPrivacyAuthorized", "mIsForeground" + }; + + private WindowSecurityBypass() {} + + public static void apply(Activity activity) { + try { + activity.setShowWhenLocked(true); + activity.setTurnScreenOn(true); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + activity.setInheritShowWhenLocked(true); + } + + Window window = activity.getWindow(); + if (window != null) { + applyWindowFlags(window); + } + + for (String fieldName : TARGET_BOOLEAN_FIELDS) { + ReflectionFieldUtil.setFieldFast(activity, fieldName, true); + } + ReflectionFieldUtil.setFieldFast(activity, "mIsNormalIntent", false); + ReflectionFieldUtil.setFieldFast(activity, "mShowEnteringAnimation", false); + ReflectionFieldUtil.setFieldFast(activity, "mKeyguardStatus", 1); + ReflectionFieldUtil.setFieldFast(activity, "mIsSecureCameraId", 0); + } catch (Throwable t) { + Log.d(TAG, "UI Fixes failed: " + t); + } + } + + private static void applyWindowFlags(Window window) { + window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE); + window.setFormat(PixelFormat.TRANSLUCENT); + + WindowManager.LayoutParams lp = window.getAttributes(); + lp.flags |= WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED + | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON + | WindowManager.LayoutParams.FLAG_FULLSCREEN + | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON + | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; + lp.flags &= ~WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + } + window.setAttributes(lp); + window.addFlags(lp.flags); + window.setFlags( + WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, + WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED + ); + + View decorView = window.getDecorView(); + if (decorView != null) { + decorView.setAlpha(1.0f); + decorView.setVisibility(View.VISIBLE); + decorView.requestFocus(); + } + } +} diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/session/SessionManager.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/session/SessionManager.java new file mode 100644 index 0000000..c86be63 --- /dev/null +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/session/SessionManager.java @@ -0,0 +1,43 @@ +package com.github.droserasprout.lockscreencamera.session; + +import android.net.Uri; +import android.util.Log; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * ロック画面から起動した「セキュアカメラセッション」中に撮影された写真の URI を追跡する。 + * カメラアプリのプロセス内でのみ動作する(アプリプロセスごとに独立したインスタンス)。 + */ +public final class SessionManager { + + private static final String TAG = "LockscreenCamera.Session"; + + public static volatile boolean isActive = false; + public static final List SESSION_URIS = new CopyOnWriteArrayList<>(); + + private SessionManager() {} + + public static void start() { + isActive = true; + SESSION_URIS.clear(); + Log.i(TAG, "Secure Camera Session Started"); + } + + public static void end() { + isActive = false; + SESSION_URIS.clear(); + Log.i(TAG, "Secure Camera Session Cleared"); + } + + public static void add(Uri uri) { + if (!isActive || uri == null) return; + if (SESSION_URIS.contains(uri)) { + Log.d(TAG, "Duplicate URI skipped: " + uri); + return; + } + SESSION_URIS.add(uri); + Log.d(TAG, "Added to Session: " + uri + " | Total: " + SESSION_URIS.size()); + } +} diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/ui/PhotoAdapter.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/ui/PhotoAdapter.java new file mode 100644 index 0000000..69f9b6e --- /dev/null +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/ui/PhotoAdapter.java @@ -0,0 +1,116 @@ +package com.github.droserasprout.lockscreencamera.ui; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Bitmap; +import android.net.Uri; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.github.chrisbanes.photoview.PhotoView; +import com.github.droserasprout.lockscreencamera.util.BitmapDecoder; + +import java.lang.ref.WeakReference; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; + +/** + * セッション中の写真 URI 一覧をズーム可能な PhotoView として表示するアダプタ。 + * Activity への強参照は持たず、WeakReference 経由でのみアクセスする。 + */ +public class PhotoAdapter extends RecyclerView.Adapter { + + private final List uris; + private final WeakReference activityRef; + private final ExecutorService executor; + private final ConcurrentHashMap> pendingTasks; + + public PhotoAdapter( + List uris, + Activity activity, + ExecutorService executor, + ConcurrentHashMap> pendingTasks) { + this.uris = uris; + this.activityRef = new WeakReference<>(activity); + this.executor = executor; + this.pendingTasks = pendingTasks; + } + + @NonNull + @Override + public PhotoViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + PhotoView photoView = new PhotoView(parent.getContext()); + photoView.setBackgroundColor(0xFF000000); + photoView.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + photoView.setZoomable(true); + return new PhotoViewHolder(photoView); + } + + @Override + public void onBindViewHolder(@NonNull PhotoViewHolder holder, int position) { + holder.photoView.setImageDrawable(null); + + // 同じポジションの前回タスクをキャンセルして競合を防ぐ + Future prev = pendingTasks.remove(position); + if (prev != null) prev.cancel(true); + + Activity activity = activityRef.get(); + if (activity == null || activity.isDestroyed()) return; + + int screenWidth = activity.getResources().getDisplayMetrics().widthPixels; + int screenHeight = activity.getResources().getDisplayMetrics().heightPixels; + // Context のみ渡し、アクティビティへの強参照をラムダに持ち込まない + Context appContext = activity.getApplicationContext(); + Uri uri = uris.get(position); + + Future task = executor.submit(() -> { + Bitmap bitmap = BitmapDecoder.decodeSampledBitmapFromUri( + appContext, uri, screenWidth, screenHeight); + if (bitmap == null) return; + + Activity act = activityRef.get(); + if (act == null || act.isDestroyed()) { + bitmap.recycle(); + return; + } + + act.runOnUiThread(() -> { + pendingTasks.remove(position); + // バインド時のポジションと現在のポジションが一致する場合のみ反映 + if (holder.getAdapterPosition() == position) { + holder.photoView.setImageBitmap(bitmap); + } else { + bitmap.recycle(); + } + }); + }); + + pendingTasks.put(position, task); + } + + @Override + public void onViewRecycled(@NonNull PhotoViewHolder holder) { + super.onViewRecycled(holder); + holder.photoView.setImageDrawable(null); + } + + @Override + public int getItemCount() { + return uris.size(); + } + + static class PhotoViewHolder extends RecyclerView.ViewHolder { + final PhotoView photoView; + + PhotoViewHolder(PhotoView pv) { + super(pv); + photoView = pv; + } + } +} diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/ui/SwipeDismissLayout.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/ui/SwipeDismissLayout.java new file mode 100644 index 0000000..ad7fe0a --- /dev/null +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/ui/SwipeDismissLayout.java @@ -0,0 +1,96 @@ +package com.github.droserasprout.lockscreencamera.ui; + +import android.content.Context; +import android.util.TypedValue; +import android.view.MotionEvent; +import android.view.View; +import android.widget.FrameLayout; + +/** + * 下方向スワイプで dismiss できる汎用コンテナ。 + * 内部の1 View(通常は ViewPager2)を追随させて動かし、閾値を超えたら {@link DismissListener#onDismiss()} を呼ぶ。 + */ +public class SwipeDismissLayout extends FrameLayout { + + /** dismiss ジェスチャーが確定した際のコールバック */ + public interface DismissListener { + void onDismiss(); + } + + private final View target; + private final DismissListener listener; + + private float initialY; + private float initialX; + private boolean isSwiping; + + // dp → px 変換でハードコードピクセル値を排除 + private final float swipeStartThresholdPx; + private final float swipeDismissThresholdPx; + + public SwipeDismissLayout(Context context, View target, DismissListener listener) { + super(context); + this.target = target; + this.listener = listener; + swipeDismissThresholdPx = + context.getResources().getDisplayMetrics().heightPixels * 0.2f; + swipeStartThresholdPx = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, 16, + context.getResources().getDisplayMetrics()); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + switch (ev.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + initialY = ev.getY(); + initialX = ev.getX(); + isSwiping = false; + break; + case MotionEvent.ACTION_MOVE: + float dy = ev.getY() - initialY; + float dx = Math.abs(ev.getX() - initialX); + // 下方向かつ水平移動より大きい場合のみ dismiss スワイプとして横取り + if (dy > swipeStartThresholdPx && dy > dx) { + isSwiping = true; + return true; + } + break; + default: + break; + } + return super.onInterceptTouchEvent(ev); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (!isSwiping) return super.onTouchEvent(ev); + + switch (ev.getActionMasked()) { + case MotionEvent.ACTION_MOVE: + float dy = ev.getY() - initialY; + float translationY = Math.max(0, dy); + float progress = getHeight() > 0 ? translationY / getHeight() : 0; + target.setTranslationY(translationY); + target.setAlpha(1.0f - (progress * 0.8f)); + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + if (target.getTranslationY() > swipeDismissThresholdPx) { + target.setAlpha(0f); + if (listener != null) listener.onDismiss(); + } else { + target.animate() + .translationY(0) + .alpha(1.0f) + .setDuration(200) + .withStartAction(() -> isSwiping = false) + .start(); + } + break; + default: + break; + } + return true; + } +} diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/util/BitmapDecoder.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/util/BitmapDecoder.java new file mode 100644 index 0000000..c7c2bac --- /dev/null +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/util/BitmapDecoder.java @@ -0,0 +1,55 @@ +package com.github.droserasprout.lockscreencamera.util; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.util.Log; + +import androidx.annotation.Nullable; + +import java.io.InputStream; + +/** Uri からダウンサンプリング済みの Bitmap をデコードするユーティリティ。 */ +public final class BitmapDecoder { + + private static final String TAG = "BitmapDecoder"; + + private BitmapDecoder() {} + + @Nullable + public static Bitmap decodeSampledBitmapFromUri( + Context context, Uri uri, int reqWidth, int reqHeight) { + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + try (InputStream is = context.getContentResolver().openInputStream(uri)) { + BitmapFactory.decodeStream(is, null, options); + } + options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); + options.inJustDecodeBounds = false; + try (InputStream is = context.getContentResolver().openInputStream(uri)) { + return BitmapFactory.decodeStream(is, null, options); + } + } catch (Exception e) { + Log.e(TAG, "Failed to decode: " + uri, e); + return null; + } + } + + public static int calculateInSampleSize( + BitmapFactory.Options options, int reqWidth, int reqHeight) { + final int height = options.outHeight; + final int width = options.outWidth; + int inSampleSize = 1; + if (height > reqHeight || width > reqWidth) { + final int halfHeight = height / 2; + final int halfWidth = width / 2; + while ((halfHeight / inSampleSize) >= reqHeight + && (halfWidth / inSampleSize) >= reqWidth) { + inSampleSize *= 2; + } + } + return inSampleSize; + } +} diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/util/CameraPackageUtil.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/util/CameraPackageUtil.java new file mode 100644 index 0000000..cc619ba --- /dev/null +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/util/CameraPackageUtil.java @@ -0,0 +1,48 @@ +package com.github.droserasprout.lockscreencamera.util; + +import android.app.Activity; +import android.content.Context; +import android.view.View; + +/** + * カメラアプリのパッケージ/コンテキスト/DecorView かどうかを判定するヘルパー群。 + * 各フッククラスから共通利用するため、ここに一本化している。 + */ +public final class CameraPackageUtil { + + private CameraPackageUtil() {} + + public static boolean isCameraPackage(String pkg) { + if (pkg == null) return false; + return pkg.equals("com.android.camera") + || pkg.contains("GoogleCamera") + || pkg.equals("org.codeaurora.snapcam") + || pkg.contains("camera"); + } + + public static boolean isCameraActivity(Activity act) { + if (act == null) return false; + try { + return isCameraPackage(act.getPackageName()); + } catch (Exception e) { + try { + return act.getClass().getName().contains("camera"); + } catch (Exception e2) { + return false; + } + } + } + + public static boolean isCameraContext(Context ctx) { + if (ctx == null) return false; + try { + return isCameraPackage(ctx.getPackageName()); + } catch (Exception e) { + return ctx.getClass().getName().contains("camera"); + } + } + + public static boolean isDecorView(View v) { + return v != null && v.getClass().getName().endsWith("DecorView"); + } +} diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/util/ReflectionFieldUtil.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/util/ReflectionFieldUtil.java new file mode 100644 index 0000000..dc903ec --- /dev/null +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/util/ReflectionFieldUtil.java @@ -0,0 +1,58 @@ +package com.github.droserasprout.lockscreencamera.util; + +import java.lang.reflect.Field; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * private フィールドをキャッシュ付きで高速に書き換えるためのユーティリティ。 + * + * 修正メモ: 元の実装は「見つからなかった」結果を + * {@code fieldCache.put(cacheKey, null)} でキャッシュしようとしていたが、 + * ConcurrentHashMap は null 値を許容しないため実際には NullPointerException が発生し + * (呼び出し側の try-catch で握りつぶされていた)、 + * 見つからないフィールドについては毎回クラス階層を走査し直す状態になっていた。 + * ここでは「探索済みかどうか」を別の Map で管理することで意図通りにキャッシュされるようにしている。 + */ +public final class ReflectionFieldUtil { + + private static final Map FIELD_CACHE = new ConcurrentHashMap<>(); + private static final Map SEARCHED = new ConcurrentHashMap<>(); + + private ReflectionFieldUtil() {} + + /** + * obj のクラス階層(android.app.Activity まで)を辿り、 + * fieldName という名前の private フィールドに value をセットする。 + * 見つからない/アクセスできない場合は何もしない(例外は握りつぶす)。 + */ + public static void setFieldFast(Object obj, String fieldName, Object value) { + try { + Class current = obj.getClass(); + String cacheKey = current.getName() + ":" + fieldName; + + Field f = FIELD_CACHE.get(cacheKey); + if (f == null && !SEARCHED.containsKey(cacheKey)) { + while (current != null && !current.getName().equals("android.app.Activity")) { + try { + f = current.getDeclaredField(fieldName); + f.setAccessible(true); + break; + } catch (NoSuchFieldException e) { + current = current.getSuperclass(); + } + } + SEARCHED.put(cacheKey, Boolean.TRUE); + if (f != null) { + FIELD_CACHE.put(cacheKey, f); + } + } + + if (f != null) { + f.set(obj, value); + } + } catch (Throwable ignored) { + // リフレクション失敗は致命的ではないため無視する + } + } +} From c11106b93fb9f3f1a5b63bd9d750f6c068b1e74e Mon Sep 17 00:00:00 2001 From: FyukMdaa Date: Fri, 31 Jul 2026 11:55:06 +0900 Subject: [PATCH 02/17] add output --- app/release/output-metadata.json | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 app/release/output-metadata.json diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json new file mode 100644 index 0000000..07b8210 --- /dev/null +++ b/app/release/output-metadata.json @@ -0,0 +1,20 @@ +{ + "version": 3, + "artifactType": { + "type": "APK", + "kind": "Directory" + }, + "applicationId": "com.github.droserasprout.lockscreencamera", + "variantName": "release", + "elements": [ + { + "type": "SINGLE", + "filters": [], + "attributes": [], + "versionCode": 1, + "versionName": "1.0.0", + "outputFile": "app-release.apk" + } + ], + "elementType": "File" +} From 105800a93fd39c25443834edce877b42337efb1c Mon Sep 17 00:00:00 2001 From: FyukMdaa Date: Fri, 31 Jul 2026 12:30:06 +0900 Subject: [PATCH 03/17] fix some hook method --- app/build.gradle | 0 app/proguard-rules.pro | 0 app/release/output-metadata.json | 0 app/src/main/AndroidManifest.xml | 0 .../lockscreencamera/LockscreenCamera.java | 5 ++- .../SecureViewerActivity.java | 19 +++++------ .../hook/ActivityVisibilitySpoofHook.java | 0 .../hook/CameraActivityLifecycleHook.java | 0 .../hook/CameraGestureLauncherHook.java | 0 .../hook/DecorViewProtectionHook.java | 23 ++++++++++--- .../hook/GalleryRedirectHook.java | 0 .../hook/KeyguardDismissBlockHook.java | 0 .../hook/KeyguardIntentRewriteHook.java | 0 .../hook/MediaStoreSessionTrackingHook.java | 0 .../lockscreencamera/hook/MiscSystemHook.java | 0 .../hook/ViewVisibilityProtectionHook.java | 32 ++++++++---------- .../hook/WindowSecurityBypass.java | 0 .../session/SessionManager.java | 0 .../lockscreencamera/ui/PhotoAdapter.java | 0 .../ui/SwipeDismissLayout.java | 0 .../lockscreencamera/util/BitmapDecoder.java | 0 .../util/CameraPackageUtil.java | 0 .../util/ReflectionFieldUtil.java | 0 app/src/main/res/values/strings.xml | 0 .../resources/META-INF/xposed/java_init.list | 0 .../resources/META-INF/xposed/module.prop | 0 .../main/resources/META-INF/xposed/scope.list | 0 build.gradle | 0 gradle.properties | 0 gradle/wrapper/gradle-wrapper.jar | Bin gradle/wrapper/gradle-wrapper.properties | 0 gradlew | 0 gradlew.bat | 0 settings.gradle | 0 34 files changed, 43 insertions(+), 36 deletions(-) mode change 100644 => 100755 app/build.gradle mode change 100644 => 100755 app/proguard-rules.pro mode change 100644 => 100755 app/release/output-metadata.json mode change 100644 => 100755 app/src/main/AndroidManifest.xml mode change 100644 => 100755 app/src/main/java/com/github/droserasprout/lockscreencamera/LockscreenCamera.java mode change 100644 => 100755 app/src/main/java/com/github/droserasprout/lockscreencamera/SecureViewerActivity.java mode change 100644 => 100755 app/src/main/java/com/github/droserasprout/lockscreencamera/hook/ActivityVisibilitySpoofHook.java mode change 100644 => 100755 app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraActivityLifecycleHook.java mode change 100644 => 100755 app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraGestureLauncherHook.java mode change 100644 => 100755 app/src/main/java/com/github/droserasprout/lockscreencamera/hook/DecorViewProtectionHook.java mode change 100644 => 100755 app/src/main/java/com/github/droserasprout/lockscreencamera/hook/GalleryRedirectHook.java mode change 100644 => 100755 app/src/main/java/com/github/droserasprout/lockscreencamera/hook/KeyguardDismissBlockHook.java mode change 100644 => 100755 app/src/main/java/com/github/droserasprout/lockscreencamera/hook/KeyguardIntentRewriteHook.java mode change 100644 => 100755 app/src/main/java/com/github/droserasprout/lockscreencamera/hook/MediaStoreSessionTrackingHook.java mode change 100644 => 100755 app/src/main/java/com/github/droserasprout/lockscreencamera/hook/MiscSystemHook.java mode change 100644 => 100755 app/src/main/java/com/github/droserasprout/lockscreencamera/hook/ViewVisibilityProtectionHook.java mode change 100644 => 100755 app/src/main/java/com/github/droserasprout/lockscreencamera/hook/WindowSecurityBypass.java mode change 100644 => 100755 app/src/main/java/com/github/droserasprout/lockscreencamera/session/SessionManager.java mode change 100644 => 100755 app/src/main/java/com/github/droserasprout/lockscreencamera/ui/PhotoAdapter.java mode change 100644 => 100755 app/src/main/java/com/github/droserasprout/lockscreencamera/ui/SwipeDismissLayout.java mode change 100644 => 100755 app/src/main/java/com/github/droserasprout/lockscreencamera/util/BitmapDecoder.java mode change 100644 => 100755 app/src/main/java/com/github/droserasprout/lockscreencamera/util/CameraPackageUtil.java mode change 100644 => 100755 app/src/main/java/com/github/droserasprout/lockscreencamera/util/ReflectionFieldUtil.java mode change 100644 => 100755 app/src/main/res/values/strings.xml mode change 100644 => 100755 app/src/main/resources/META-INF/xposed/java_init.list mode change 100644 => 100755 app/src/main/resources/META-INF/xposed/module.prop mode change 100644 => 100755 app/src/main/resources/META-INF/xposed/scope.list mode change 100644 => 100755 build.gradle mode change 100644 => 100755 gradle.properties mode change 100644 => 100755 gradle/wrapper/gradle-wrapper.jar mode change 100644 => 100755 gradle/wrapper/gradle-wrapper.properties mode change 100644 => 100755 gradlew mode change 100644 => 100755 gradlew.bat mode change 100644 => 100755 settings.gradle diff --git a/app/build.gradle b/app/build.gradle old mode 100644 new mode 100755 diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro old mode 100644 new mode 100755 diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json old mode 100644 new mode 100755 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml old mode 100644 new mode 100755 diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/LockscreenCamera.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/LockscreenCamera.java old mode 100644 new mode 100755 index 37b39dc..bfe9352 --- a/app/src/main/java/com/github/droserasprout/lockscreencamera/LockscreenCamera.java +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/LockscreenCamera.java @@ -13,7 +13,6 @@ import com.github.droserasprout.lockscreencamera.hook.KeyguardIntentRewriteHook; import com.github.droserasprout.lockscreencamera.hook.MediaStoreSessionTrackingHook; import com.github.droserasprout.lockscreencamera.hook.MiscSystemHook; -import com.github.droserasprout.lockscreencamera.hook.ViewVisibilityProtectionHook; import com.github.droserasprout.lockscreencamera.util.CameraPackageUtil; import io.github.libxposed.api.XposedModule; @@ -30,7 +29,7 @@ * 1. KeyguardDismissBlockHook - requestDismissKeyguard(PIN画面表示要求)をブロック * 2. ActivityVisibilitySpoofHook - hasWindowFocus/isResumed のスプーフィング * 3. KeyguardIntentRewriteHook - getIntent() の動的書き換え - * 4. ViewVisibilityProtectionHook - SurfaceView 等の非表示化を阻止 + * 4. (ViewVisibilityProtectionHook は DecorViewProtectionHook に統合済み) * 5. GalleryRedirectHook - ギャラリー確認画面を SecureViewerActivity へリダイレクト * 6. CameraActivityLifecycleHook - ライフサイクルフック(セッション開始・自動終了・ウィンドウ復元) * 7. MediaStoreSessionTrackingHook - 撮影された写真の URI をセッションに記録 @@ -58,7 +57,7 @@ public void onPackageReady(@NonNull PackageReadyParam param) { KeyguardDismissBlockHook.install(this); ActivityVisibilitySpoofHook.install(this); KeyguardIntentRewriteHook.install(this); - ViewVisibilityProtectionHook.install(this); + // ViewVisibilityProtectionHook は DecorViewProtectionHook に統合済み GalleryRedirectHook.install(this); CameraActivityLifecycleHook.install(this); MediaStoreSessionTrackingHook.install(this); diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/SecureViewerActivity.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/SecureViewerActivity.java old mode 100644 new mode 100755 index 3638cff..2757bcf --- a/app/src/main/java/com/github/droserasprout/lockscreencamera/SecureViewerActivity.java +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/SecureViewerActivity.java @@ -64,24 +64,21 @@ protected void onCreate(Bundle savedInstanceState) { registerScreenOffReceiver(); } + /** + * Intent から表示対象の URI リストを解決する。 + * 修正メモ: リファクタリング版では content:// 以外のスキームを厳格に除外していたが、 + * 一部のカメラアプリが file:// URI を渡すケースがあるため、 + * オリジナルの動作に合わせて null チェックのみとしスキーマ検証は行わない。 + */ private List resolveSafeUris() { List uris = getIntent().getParcelableArrayListExtra(EXTRA_SESSION_PHOTOS); if (uris == null || uris.isEmpty()) { uris = new ArrayList<>(); Uri singleUri = getIntent().getData(); if (singleUri != null) uris.add(singleUri); + Log.i(TAG, "Fallback to single image from Intent Data"); } - - // URI のスキームを検証し content:// 以外を除外する - List safeUris = new ArrayList<>(); - for (Uri uri : uris) { - if ("content".equals(uri.getScheme())) { - safeUris.add(uri); - } else { - Log.w(TAG, "Rejected non-content URI: " + uri.getScheme()); - } - } - return safeUris; + return uris != null ? uris : new ArrayList<>(); } private void setupViewPager(List safeUris) { diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/ActivityVisibilitySpoofHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/ActivityVisibilitySpoofHook.java old mode 100644 new mode 100755 diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraActivityLifecycleHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraActivityLifecycleHook.java old mode 100644 new mode 100755 diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraGestureLauncherHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraGestureLauncherHook.java old mode 100644 new mode 100755 diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/DecorViewProtectionHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/DecorViewProtectionHook.java old mode 100644 new mode 100755 index 7b5784b..d6d99d9 --- a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/DecorViewProtectionHook.java +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/DecorViewProtectionHook.java @@ -10,8 +10,15 @@ import io.github.libxposed.api.XposedModule; /** - * DecorView(Activity のルートビュー)が setAlpha(0) や setVisibility(GONE) で - * 非表示化・透明化されるのを物理的に防ぐ。 + * DecorView およびカメラアプリ内の全 View の透明化・非表示化を防ぐ。 + * + * 修正メモ: リファクタリング版では DecorViewProtectionHook と + * ViewVisibilityProtectionHook がそれぞれ独立して View.setVisibility をフックしており、 + * libxposed の実装によっては後から登録されたフックが前を上書きし、 + * DecorView 側の保護が失われる問題があった。 + * ここでは setVisibility のロジックを一本化し、DecorView かどうかに応じて + * 適切に処理する単一フックに統合する。ViewVisibilityProtectionHook 側の + * setVisibility フックは削除し、setAlpha 専用フックのみ残す。 */ public final class DecorViewProtectionHook { @@ -21,6 +28,7 @@ private DecorViewProtectionHook() {} public static void install(XposedModule module) { try { + // DecorView の透明化を防ぐ(setAlpha) module.hook(View.class.getDeclaredMethod("setAlpha", float.class)).intercept(chain -> { View view = (View) chain.getThisObject(); if (CameraPackageUtil.isDecorView(view) && CameraPackageUtil.isCameraContext(view.getContext())) { @@ -30,9 +38,16 @@ public static void install(XposedModule module) { } return chain.proceed(); }); + } catch (Throwable t) { + module.log(Log.WARN, TAG, "DecorView setAlpha hook failed: " + t); + } + + try { + // setVisibility は DecorView / 非 DecorView を問わず、 + // カメラコンテキスト内では VISIBLE を強制する単一フック module.hook(View.class.getDeclaredMethod("setVisibility", int.class)).intercept(chain -> { View view = (View) chain.getThisObject(); - if (CameraPackageUtil.isDecorView(view) && CameraPackageUtil.isCameraContext(view.getContext())) { + if (CameraPackageUtil.isCameraContext(view.getContext())) { List args = chain.getArgs(); int vis = (int) args.get(0); if (vis != View.VISIBLE) args.set(0, View.VISIBLE); @@ -40,7 +55,7 @@ public static void install(XposedModule module) { return chain.proceed(); }); } catch (Throwable t) { - module.log(Log.WARN, TAG, "DecorViewProtectionHook install failed: " + t); + module.log(Log.WARN, TAG, "DecorView setVisibility hook failed: " + t); } } } diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/GalleryRedirectHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/GalleryRedirectHook.java old mode 100644 new mode 100755 diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/KeyguardDismissBlockHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/KeyguardDismissBlockHook.java old mode 100644 new mode 100755 diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/KeyguardIntentRewriteHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/KeyguardIntentRewriteHook.java old mode 100644 new mode 100755 diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/MediaStoreSessionTrackingHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/MediaStoreSessionTrackingHook.java old mode 100644 new mode 100755 diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/MiscSystemHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/MiscSystemHook.java old mode 100644 new mode 100755 diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/ViewVisibilityProtectionHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/ViewVisibilityProtectionHook.java old mode 100644 new mode 100755 index 13c8482..e1471bb --- a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/ViewVisibilityProtectionHook.java +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/ViewVisibilityProtectionHook.java @@ -1,29 +1,25 @@ package com.github.droserasprout.lockscreencamera.hook; -import android.view.View; - -import java.util.List; - -import com.github.droserasprout.lockscreencamera.util.CameraPackageUtil; - import io.github.libxposed.api.XposedModule; -/** カメラアプリ内の SurfaceView 等(DecorView 以外)が非表示化されるのを阻止する。 */ +/** + * カメラアプリ内の SurfaceView 等が非表示化されるのを阻止する。 + * + * 修正メモ: このクラスの setVisibility フックは DecorViewProtectionHook に統合された。 + * View.setVisibility は単一のフックで DecorView / 非 DecorView を問わず + * カメラコンテキスト内で VISIBLE を強制するため、このクラスは空のまま残され、 + * LockscreenCamera からの install 呼び出しは削除される。 + */ public final class ViewVisibilityProtectionHook { private ViewVisibilityProtectionHook() {} + /** + * 何もしない。setVisibility の保護は {@link DecorViewProtectionHook} に統合済み。 + * 互換性のためクラス自体は残すが、新規コードからの呼び出しは不要。 + */ public static void install(XposedModule module) { - try { - module.hook(View.class.getMethod("setVisibility", int.class)).intercept(chain -> { - View v = (View) chain.getThisObject(); - if (CameraPackageUtil.isCameraContext(v.getContext()) && !CameraPackageUtil.isDecorView(v)) { - List args = chain.getArgs(); - int vis = (int) args.get(0); - if (vis != View.VISIBLE) args.set(0, View.VISIBLE); - } - return chain.proceed(); - }); - } catch (Throwable ignored) {} + // setVisibility フックは DecorViewProtectionHook に統合されたため、 + // ここでは何もしない。 } } diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/WindowSecurityBypass.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/WindowSecurityBypass.java old mode 100644 new mode 100755 diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/session/SessionManager.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/session/SessionManager.java old mode 100644 new mode 100755 diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/ui/PhotoAdapter.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/ui/PhotoAdapter.java old mode 100644 new mode 100755 diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/ui/SwipeDismissLayout.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/ui/SwipeDismissLayout.java old mode 100644 new mode 100755 diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/util/BitmapDecoder.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/util/BitmapDecoder.java old mode 100644 new mode 100755 diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/util/CameraPackageUtil.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/util/CameraPackageUtil.java old mode 100644 new mode 100755 diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/util/ReflectionFieldUtil.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/util/ReflectionFieldUtil.java old mode 100644 new mode 100755 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml old mode 100644 new mode 100755 diff --git a/app/src/main/resources/META-INF/xposed/java_init.list b/app/src/main/resources/META-INF/xposed/java_init.list old mode 100644 new mode 100755 diff --git a/app/src/main/resources/META-INF/xposed/module.prop b/app/src/main/resources/META-INF/xposed/module.prop old mode 100644 new mode 100755 diff --git a/app/src/main/resources/META-INF/xposed/scope.list b/app/src/main/resources/META-INF/xposed/scope.list old mode 100644 new mode 100755 diff --git a/build.gradle b/build.gradle old mode 100644 new mode 100755 diff --git a/gradle.properties b/gradle.properties old mode 100644 new mode 100755 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar old mode 100644 new mode 100755 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties old mode 100644 new mode 100755 diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/gradlew.bat b/gradlew.bat old mode 100644 new mode 100755 diff --git a/settings.gradle b/settings.gradle old mode 100644 new mode 100755 From 7ebdb02a3d63c956f17c4db54f4aba22d60ebb30 Mon Sep 17 00:00:00 2001 From: FyukMdaa Date: Fri, 31 Jul 2026 13:14:38 +0900 Subject: [PATCH 04/17] add setting ui --- app/build.gradle | 5 +- app/src/main/AndroidManifest.xml | 12 + .../lockscreencamera/LockscreenCamera.java | 41 +-- .../lockscreencamera/SettingsActivity.java | 290 ++++++++++++++++++ .../hook/ActivityVisibilitySpoofHook.java | 12 +- .../hook/CameraActivityLifecycleHook.java | 24 +- .../hook/DecorViewProtectionHook.java | 24 +- .../hook/GalleryRedirectHook.java | 29 +- .../hook/KeyguardIntentRewriteHook.java | 7 +- .../util/CameraPackageUtil.java | 74 ++++- .../lockscreencamera/util/ModulePrefs.java | 163 ++++++++++ app/src/main/res/values/arrays.xml | 6 + app/src/main/res/values/strings.xml | 27 ++ app/src/main/res/xml/preferences.xml | 54 ++++ .../main/resources/META-INF/xposed/scope.list | 11 +- 15 files changed, 701 insertions(+), 78 deletions(-) create mode 100644 app/src/main/java/com/github/droserasprout/lockscreencamera/SettingsActivity.java create mode 100644 app/src/main/java/com/github/droserasprout/lockscreencamera/util/ModulePrefs.java create mode 100644 app/src/main/res/values/arrays.xml create mode 100644 app/src/main/res/xml/preferences.xml diff --git a/app/build.gradle b/app/build.gradle index f87bcc8..7cd60ff 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,8 +10,8 @@ android { applicationId "com.github.droserasprout.lockscreencamera" minSdk 30 targetSdk 34 - versionCode 2 - versionName "2.0.0" + versionCode 3 + versionName "2.1.0" } signingConfigs { @@ -59,4 +59,5 @@ dependencies { implementation 'com.github.chrisbanes:PhotoView:2.3.0' implementation 'androidx.annotation:annotation:1.7.0' implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.preference:preference:1.2.1' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 24a9020..8e5b810 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -9,6 +9,18 @@ android:supportsRtl="true" tools:ignore="AllowBackup,MissingApplicationIcon"> + + + + + + + + + { + autoDetectAndSave(); + return true; + }); + } + } + + /** + * システムにインストール済みのカメラアプリを検出し、 + * 既存の選択状態をマージして保存する。 + */ + private void autoDetectAndSave() { + Set detected = detectCameraPackages(); + + // 既存の選択をマージ(ユーザーが手動追加したものも保持) + Set current = ModulePrefs.getPrefs(requireContext()) + .getStringSet(ModulePrefs.KEY_ENABLED_PACKAGES, new HashSet<>()); + Set merged = new HashSet<>(current); + merged.addAll(detected); + + ModulePrefs.setEnabledPackages(requireContext(), merged); + + // リストを再構築 + refreshPackageList(merged); + + String msg = getString(R.string.auto_detect_result, detected.size()); + Toast.makeText(requireContext(), msg, Toast.LENGTH_SHORT).show(); + Log.i(TAG, "Auto-detected cameras: " + detected); + } + + /** + * PackageManager を使ってカメラアプリを検出する。 + * 1. INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE / STILL_IMAGE_CAMERA を resolve + * 2. 既知のパッケージリストと照合してインストール済みのものを追加 + */ + private Set detectCameraPackages() { + Set found = new HashSet<>(); + PackageManager pm = requireContext().getPackageManager(); + + // resolve で見つかるもの + String[] actions = { + MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE, + MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA + }; + + for (String action : actions) { + try { + List list = pm.queryIntentActivities( + new Intent(action), PackageManager.MATCH_DEFAULT_ONLY); + for (ResolveInfo ri : list) { + if (ri.activityInfo != null) { + addIfInstalled(found, ri.activityInfo.packageName, pm); + } + } + } catch (Exception ignored) {} + } + + // 既知のカメラパッケージでインストール済みのもの + for (String pkg : ModulePrefs.getKnownCameraPackages()) { + addIfInstalled(found, pkg, pm); + } + + return found; + } + + private void addIfInstalled(Set target, String pkg, PackageManager pm) { + try { + PackageInfo pi = pm.getPackageInfo(pkg, 0); + if ((pi.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0 + || (pi.applicationInfo.flags & ApplicationInfo.FLAG_INSTALLED) != 0) { + target.add(pkg); + } + } catch (PackageManager.NameNotFoundException ignored) { + // 未インストール + } + } + + // ---- パッケージ一覧 ---- + + private void setupPackageList() { + MultiSelectListPreference pref = findPreference("pref_enabled_packages"); + if (pref == null) return; + pref.setOnPreferenceChangeListener((preference, newValue) -> { + @SuppressWarnings("unchecked") + Set selected = (Set) newValue; + ModulePrefs.setEnabledPackages(requireContext(), selected); + // サマリー更新 + pref.setSummary(buildSummary(selected)); + // Viewer 除外リストも最新化 + refreshViewerExclusions(selected); + return true; + }); + + // 初期表示 + Set current = ModulePrefs.getPrefs(requireContext()) + .getStringSet(ModulePrefs.KEY_ENABLED_PACKAGES, new HashSet<>()); + refreshPackageList(current); + } + + private void refreshPackageList(Set selected) { + MultiSelectListPreference pref = findPreference("pref_enabled_packages"); + if (pref == null) return; + + PackageManager pm = requireContext().getPackageManager(); + List entries = new ArrayList<>(); + List values = new ArrayList<>(); + + // 選択済み + 既知パッケージ + 自動検出結果をまとめて表示 + Set allCandidates = new HashSet<>(selected); + allCandidates.addAll(ModulePrefs.getKnownCameraPackages()); + allCandidates.addAll(detectCameraPackages()); + + for (String pkg : allCandidates) { + try { + ApplicationInfo ai = pm.getApplicationInfo(pkg, 0); + String label = pm.getApplicationLabel(ai).toString(); + entries.add(label + " (" + pkg + ")"); + values.add(pkg); + } catch (PackageManager.NameNotFoundException e) { + // アンインストール済みでも選択済みなら表示 + if (selected.contains(pkg)) { + entries.add(pkg); + values.add(pkg); + } + } + } + + Collections.sort(entries); + // values も entries に合わせてソート + List sortedValues = new ArrayList<>(); + for (String entry : entries) { + // "label (pkg)" 形式から pkg を抽出 + int lastParen = entry.lastIndexOf("("); + String pkg = lastParen >= 0 + ? entry.substring(lastParen + 1, entry.length() - 1) + : entry; + sortedValues.add(pkg); + } + + pref.setEntries(entries.toArray(new String[0])); + pref.setEntryValues(sortedValues.toArray(new String[0])); + pref.setValues(selected); + pref.setSummary(buildSummary(selected)); + } + + private String buildSummary(Set selected) { + if (selected == null || selected.isEmpty()) { + return "未選択(デフォルト使用)"; + } + return selected.size() + " 個のアプリを選択中"; + } + + // ---- SecureViewer 設定 ---- + + private void setupViewerSettings() { + // メインスイッチ + SwitchPreferenceCompat viewerToggle = findPreference("pref_secure_viewer_enabled"); + if (viewerToggle != null) { + viewerToggle.setOnPreferenceChangeListener((preference, newValue) -> { + ModulePrefs.setSecureViewerEnabled(requireContext(), (Boolean) newValue); + return true; + }); + } + + // 除外リスト + MultiSelectListPreference exclusions = findPreference("pref_secure_viewer_exclusions"); + if (exclusions != null) { + Set enabledPkgs = ModulePrefs.getPrefs(requireContext()) + .getStringSet(ModulePrefs.KEY_ENABLED_PACKAGES, new HashSet<>()); + refreshViewerExclusions(enabledPkgs); + + exclusions.setOnPreferenceChangeListener((preference, newValue) -> { + @SuppressWarnings("unchecked") + Set selected = (Set) newValue; + ModulePrefs.setSecureViewerExclusions(requireContext(), selected); + exclusions.setSummary(buildExclusionSummary(selected)); + return true; + }); + } + } + + private void refreshViewerExclusions(Set enabledPackages) { + MultiSelectListPreference exclusions = findPreference("pref_secure_viewer_exclusions"); + if (exclusions == null) return; + + PackageManager pm = requireContext().getPackageManager(); + List entries = new ArrayList<>(); + List values = new ArrayList<>(); + + Set currentExclusions = ModulePrefs.getPrefs(requireContext()) + .getStringSet(ModulePrefs.KEY_SECURE_VIEWER_EXCLUSIONS, new HashSet<>()); + + for (String pkg : enabledPackages) { + try { + ApplicationInfo ai = pm.getApplicationInfo(pkg, 0); + String label = pm.getApplicationLabel(ai).toString(); + entries.add(label + " (" + pkg + ")"); + values.add(pkg); + } catch (PackageManager.NameNotFoundException e) { + if (currentExclusions.contains(pkg)) { + entries.add(pkg); + values.add(pkg); + } + } + } + + exclusions.setEntries(entries.toArray(new String[0])); + exclusions.setEntryValues(values.toArray(new String[0])); + exclusions.setValues(currentExclusions); + exclusions.setSummary(buildExclusionSummary(currentExclusions)); + } + + private String buildExclusionSummary(Set exclusions) { + if (exclusions == null || exclusions.isEmpty()) { + return "全カメラアプリで有効"; + } + return exclusions.size() + " 個のアプリで無効化"; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/ActivityVisibilitySpoofHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/ActivityVisibilitySpoofHook.java index eeb547d..3fc92fe 100755 --- a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/ActivityVisibilitySpoofHook.java +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/ActivityVisibilitySpoofHook.java @@ -1,24 +1,28 @@ package com.github.droserasprout.lockscreencamera.hook; import android.app.Activity; +import android.content.Context; import com.github.droserasprout.lockscreencamera.util.CameraPackageUtil; import io.github.libxposed.api.XposedModule; -/** カメラ Activity の hasWindowFocus/isResumed を常に true として返す(フォーカス喪失の誤検知対策)。 */ +/** hasWindowFocus/isResumed を常に true として返す(フォーカス喪失の誤検知対策)。 */ public final class ActivityVisibilitySpoofHook { + private static Context settingsContext; + private ActivityVisibilitySpoofHook() {} - public static void install(XposedModule module) { + public static void install(XposedModule module, Context context) { + settingsContext = context; try { module.hook(Activity.class.getDeclaredMethod("hasWindowFocus")).intercept(chain -> { - if (CameraPackageUtil.isCameraActivity((Activity) chain.getThisObject())) return true; + if (CameraPackageUtil.isCameraActivity((Activity) chain.getThisObject(), settingsContext)) return true; return (Boolean) chain.proceed(); }); module.hook(Activity.class.getDeclaredMethod("isResumed")).intercept(chain -> { - if (CameraPackageUtil.isCameraActivity((Activity) chain.getThisObject())) return true; + if (CameraPackageUtil.isCameraActivity((Activity) chain.getThisObject(), settingsContext)) return true; return (Boolean) chain.proceed(); }); } catch (Throwable ignored) {} diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraActivityLifecycleHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraActivityLifecycleHook.java index f5d689b..c809667 100755 --- a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraActivityLifecycleHook.java +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraActivityLifecycleHook.java @@ -22,14 +22,7 @@ /** * カメラ Activity のライフサイクルをフックし、 - * - ロック画面起動時に SessionManager を開始 - * - 画面 OFF で自動終了させる BroadcastReceiver を登録 - * - onDestroy でセッション・レシーバーを後始末する - * - ライフサイクル中は常にウィンドウのロック画面バイパス属性を再適用する({@link WindowSecurityBypass}) - * - * 修正メモ: 元の実装は onCreate で毎回 BroadcastReceiver を register していたが、 - * unregister する場所がどこにも無かった(onDestroy は SessionManager.end() のみ呼んでいた)。 - * ここでは Activity ごとに登録したレシーバーを WeakHashMap で保持し、onDestroy で確実に解除する。 + * セッション開始・画面OFF時の自動終了・ウィンドウ属性の再適用を行う。 */ public final class CameraActivityLifecycleHook { @@ -39,10 +32,13 @@ public final class CameraActivityLifecycleHook { {"attachBaseContext", "onCreate", "onStart", "onResume", "onWindowFocusChanged", "onDestroy"}; private static final Map ACTIVE_RECEIVERS = new WeakHashMap<>(); + private static Context settingsContext; private CameraActivityLifecycleHook() {} - public static void install(XposedModule module) { + public static void install(XposedModule module, Context context) { + settingsContext = context; + for (String methodName : LIFECYCLE_METHODS) { try { Method method = resolveMethod(methodName); @@ -52,7 +48,7 @@ public static void install(XposedModule module) { if (!(thisObj instanceof Activity)) return chain.proceed(); Activity act = (Activity) thisObj; - if (!CameraPackageUtil.isCameraActivity(act)) return chain.proceed(); + if (!CameraPackageUtil.isCameraActivity(act, settingsContext)) return chain.proceed(); if ("onDestroy".equals(mName)) { if (SessionManager.isActive) { @@ -89,9 +85,7 @@ public static void install(XposedModule module) { WindowSecurityBypass.apply(act); return chain.proceed(); }); - } catch (Throwable ignored) { - // このメソッドがこの OS バージョンに存在しない場合はスキップ - } + } catch (Throwable ignored) {} } } @@ -137,8 +131,6 @@ private static void unregisterReceiver(Activity act) { if (receiver == null) return; try { act.unregisterReceiver(receiver); - } catch (Exception ignored) { - // 既に解除済み、または登録に失敗していた場合 - } + } catch (Exception ignored) {} } } diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/DecorViewProtectionHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/DecorViewProtectionHook.java index d6d99d9..803d1c0 100755 --- a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/DecorViewProtectionHook.java +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/DecorViewProtectionHook.java @@ -1,5 +1,6 @@ package com.github.droserasprout.lockscreencamera.hook; +import android.content.Context; import android.util.Log; import android.view.View; @@ -11,27 +12,22 @@ /** * DecorView およびカメラアプリ内の全 View の透明化・非表示化を防ぐ。 - * - * 修正メモ: リファクタリング版では DecorViewProtectionHook と - * ViewVisibilityProtectionHook がそれぞれ独立して View.setVisibility をフックしており、 - * libxposed の実装によっては後から登録されたフックが前を上書きし、 - * DecorView 側の保護が失われる問題があった。 - * ここでは setVisibility のロジックを一本化し、DecorView かどうかに応じて - * 適切に処理する単一フックに統合する。ViewVisibilityProtectionHook 側の - * setVisibility フックは削除し、setAlpha 専用フックのみ残す。 */ public final class DecorViewProtectionHook { private static final String TAG = "LockscreenCamera"; + private static Context settingsContext; private DecorViewProtectionHook() {} - public static void install(XposedModule module) { + public static void install(XposedModule module, Context context) { + settingsContext = context; + try { - // DecorView の透明化を防ぐ(setAlpha) module.hook(View.class.getDeclaredMethod("setAlpha", float.class)).intercept(chain -> { View view = (View) chain.getThisObject(); - if (CameraPackageUtil.isDecorView(view) && CameraPackageUtil.isCameraContext(view.getContext())) { + if (CameraPackageUtil.isDecorView(view) + && CameraPackageUtil.isCameraContext(view.getContext(), settingsContext)) { List args = chain.getArgs(); float alpha = (float) args.get(0); if (alpha < 1.0f) args.set(0, 1.0f); @@ -43,11 +39,9 @@ public static void install(XposedModule module) { } try { - // setVisibility は DecorView / 非 DecorView を問わず、 - // カメラコンテキスト内では VISIBLE を強制する単一フック module.hook(View.class.getDeclaredMethod("setVisibility", int.class)).intercept(chain -> { View view = (View) chain.getThisObject(); - if (CameraPackageUtil.isCameraContext(view.getContext())) { + if (CameraPackageUtil.isCameraContext(view.getContext(), settingsContext)) { List args = chain.getArgs(); int vis = (int) args.get(0); if (vis != View.VISIBLE) args.set(0, View.VISIBLE); @@ -58,4 +52,4 @@ public static void install(XposedModule module) { module.log(Log.WARN, TAG, "DecorView setVisibility hook failed: " + t); } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/GalleryRedirectHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/GalleryRedirectHook.java index 94406f6..fb4a265 100755 --- a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/GalleryRedirectHook.java +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/GalleryRedirectHook.java @@ -15,12 +15,16 @@ import com.github.droserasprout.lockscreencamera.session.SessionManager; import com.github.droserasprout.lockscreencamera.util.CameraPackageUtil; +import com.github.droserasprout.lockscreencamera.util.ModulePrefs; import io.github.libxposed.api.XposedModule; /** - * カメラアプリがシステムの「ギャラリー確認画面」を開こうとした際、 - * 独自の SecureViewerActivity にリダイレクトすることでロック中でも写真プレビューを可能にする。 + * カメラアプリが「ギャラリー確認画面」を開こうとした際、 + * 設定に応じて SecureViewerActivity にリダイレクトする。 + *

+ * GCam 等で SecureViewer が動作しない場合は設定画面で該当パッケージを + * 「ビューアー除外」に追加することで、カメラアプリ標準のレビューアが使われる。 */ public final class GalleryRedirectHook { @@ -28,12 +32,14 @@ public final class GalleryRedirectHook { private static final String VIEWER_PACKAGE = "com.github.droserasprout.lockscreencamera"; private static final String VIEWER_CLASS = "com.github.droserasprout.lockscreencamera.SecureViewerActivity"; - // ロック画面上に表示するための隠しフラグ (SHOW_WHEN_LOCKED 系) private static final int FLAG_SHOW_WHEN_LOCKED_HIDDEN = 0x00080000 | 0x00400000 | 0x00200000; + private static Context settingsContext; + private GalleryRedirectHook() {} - public static void install(XposedModule module) { + public static void install(XposedModule module, Context context) { + settingsContext = context; try { Method startAct = Activity.class.getDeclaredMethod("startActivity", Intent.class); module.hook(startAct).intercept(chain -> { @@ -61,9 +67,13 @@ private static void handleGalleryRedirect(Context ctx, Intent intent) { if (ctx == null || intent == null || intent.getAction() == null) return; if (!SessionManager.isActive) return; - try { - if (!CameraPackageUtil.isCameraPackage(ctx.getPackageName())) return; - } catch (Exception e) { + String pkg; + try { pkg = ctx.getPackageName(); } catch (Exception e) { return; } + if (!CameraPackageUtil.isCameraPackage(pkg, settingsContext)) return; + + // 設定で SecureViewer が無効化されているパッケージならスキップ + if (!ModulePrefs.shouldUseSecureViewer(settingsContext, pkg)) { + Log.d(TAG, "SecureViewer disabled for: " + pkg); return; } @@ -81,9 +91,8 @@ private static void handleGalleryRedirect(Context ctx, Intent intent) { uriList.add(intent.getData()); } - // 既存の intent を書き換えて再利用し、システムを騙す intent.setComponent(new ComponentName(VIEWER_PACKAGE, VIEWER_CLASS)); - intent.setPackage(null); // Google フォトなどが候補に並ぶのを防ぐ + intent.setPackage(null); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { intent.setSelector(null); } @@ -106,4 +115,4 @@ private static void handleGalleryRedirect(Context ctx, Intent intent) { Log.d(TAG, "Intent modification complete. Proceeding with hijacked intent."); } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/KeyguardIntentRewriteHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/KeyguardIntentRewriteHook.java index 73595ce..f531cca 100755 --- a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/KeyguardIntentRewriteHook.java +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/KeyguardIntentRewriteHook.java @@ -1,6 +1,7 @@ package com.github.droserasprout.lockscreencamera.hook; import android.app.Activity; +import android.content.Context; import android.content.Intent; import com.github.droserasprout.lockscreencamera.util.CameraPackageUtil; @@ -11,15 +12,17 @@ public final class KeyguardIntentRewriteHook { private static final String EXTRA_START_BY_KEYGUARD = "com.miui.camera.extra.START_BY_KEYGUARD"; + private static Context settingsContext; private KeyguardIntentRewriteHook() {} - public static void install(XposedModule module) { + public static void install(XposedModule module, Context context) { + settingsContext = context; try { module.hook(Activity.class.getDeclaredMethod("getIntent")).intercept(chain -> { Intent intent = (Intent) chain.proceed(); Activity act = (Activity) chain.getThisObject(); - if (CameraPackageUtil.isCameraActivity(act) && intent != null + if (CameraPackageUtil.isCameraActivity(act, settingsContext) && intent != null && intent.getBooleanExtra(EXTRA_START_BY_KEYGUARD, false)) { intent.putExtra("is_secure_camera", true); intent.putExtra("ShowCameraWhenLocked", true); diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/util/CameraPackageUtil.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/util/CameraPackageUtil.java index cc619ba..0347e28 100755 --- a/app/src/main/java/com/github/droserasprout/lockscreencamera/util/CameraPackageUtil.java +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/util/CameraPackageUtil.java @@ -4,35 +4,91 @@ import android.content.Context; import android.view.View; +import java.util.Set; + /** * カメラアプリのパッケージ/コンテキスト/DecorView かどうかを判定するヘルパー群。 - * 各フッククラスから共通利用するため、ここに一本化している。 + *

+ * リファクタリング版では pkg.contains("camera") という広すぎるワイルドカードで + * カメラ以外のパッケージ(オーバーレイAPK等)に誤マッチする問題があった。 + * 修正後は {@link ModulePrefs} に保存された設定済みパッケージリストと照合し、 + * 設定されていないパッケージは一律で拒否する。 */ public final class CameraPackageUtil { private CameraPackageUtil() {} + /** + * フックプロセス用: 設定済みパッケージリストと照合する。 + * context はターゲットアプリの Context(Activity 等)。 + */ + public static boolean isCameraPackage(String pkg, Context context) { + if (pkg == null || context == null) return false; + return ModulePrefs.isPackageEnabled(context, pkg); + } + + /** + * フォールバック用: context が取得できない場合にデフォルトリストで判定する。 + * 設定画面が未設定の場合もこちらにフォールバックする。 + */ public static boolean isCameraPackage(String pkg) { if (pkg == null) return false; + // ワイルドカードマッチは使わず、明示的なパッケージ名のみ return pkg.equals("com.android.camera") - || pkg.contains("GoogleCamera") + || pkg.equals("com.google.android.GoogleCamera") + || pkg.equals("com.android.camera2") || pkg.equals("org.codeaurora.snapcam") - || pkg.contains("camera"); + || pkg.equals("com.miui.camera"); + } + + /** + * 設定が利用可能かどうかをチェックし、 + * 可能なら設定リスト、不可ならフォールバックリストを使う。 + */ + public static boolean isCameraPackage(String pkg, Context context, boolean useSettings) { + if (!useSettings || context == null) return isCameraPackage(pkg); + try { + return isCameraPackage(pkg, context); + } catch (Exception e) { + return isCameraPackage(pkg); + } + } + + public static boolean isCameraActivity(Activity act, Context context) { + if (act == null) return false; + try { + return isCameraPackage(act.getPackageName(), context); + } catch (Exception e) { + return isCameraPackage(act.getPackageName()); + } } + /** + * @deprecated 設定連動版 {@link #isCameraActivity(Activity, Context)} を推奨。 + * フック内で Context が取れない場合のフォールバック用。 + */ public static boolean isCameraActivity(Activity act) { if (act == null) return false; try { return isCameraPackage(act.getPackageName()); } catch (Exception e) { - try { - return act.getClass().getName().contains("camera"); - } catch (Exception e2) { - return false; - } + try { return act.getClass().getName().contains("camera"); } + catch (Exception e2) { return false; } + } + } + + public static boolean isCameraContext(Context ctx, Context settingsContext) { + if (ctx == null) return false; + try { + return isCameraPackage(ctx.getPackageName(), settingsContext); + } catch (Exception e) { + return isCameraPackage(ctx.getPackageName()); } } + /** + * @deprecated 設定連動版 {@link #isCameraContext(Context, Context)} を推奨。 + */ public static boolean isCameraContext(Context ctx) { if (ctx == null) return false; try { @@ -45,4 +101,4 @@ public static boolean isCameraContext(Context ctx) { public static boolean isDecorView(View v) { return v != null && v.getClass().getName().endsWith("DecorView"); } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/util/ModulePrefs.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/util/ModulePrefs.java new file mode 100644 index 0000000..6a1b43a --- /dev/null +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/util/ModulePrefs.java @@ -0,0 +1,163 @@ +package com.github.droserasprout.lockscreencamera.util; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * モジュールの SharedPreferences への読み書きを一本化するヘルパー。 + *

+ * 設定画面(モジュール自身のプロセス)と + * フック側(ターゲットアプリのプロセス)の両方から使われる。 + */ +public final class ModulePrefs { + + private static final String TAG = "LockscreenCamera"; + public static final String PREFS_NAME = "secure_camera_prefs"; + + // キー名 + public static final String KEY_ENABLED_PACKAGES = "enabled_packages"; + public static final String KEY_SECURE_VIEWER_ENABLED = "secure_viewer_enabled"; + public static final String KEY_SECURE_VIEWER_EXCLUSIONS = "secure_viewer_exclusions"; + public static final String KEY_FIRST_RUN = "first_run"; + + private ModulePrefs() {} + + // ---- 書き込み(設定画面プロセスから呼ばれる) ---- + + public static SharedPreferences getPrefs(Context context) { + return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + } + + public static void setEnabledPackages(Context ctx, Set packages) { + getPrefs(ctx).edit() + .putStringSet(KEY_ENABLED_PACKAGES, packages) + .apply(); + } + + public static void setSecureViewerEnabled(Context ctx, boolean enabled) { + getPrefs(ctx).edit() + .putBoolean(KEY_SECURE_VIEWER_ENABLED, enabled) + .apply(); + } + + public static void setSecureViewerExclusions(Context ctx, Set packages) { + getPrefs(ctx).edit() + .putStringSet(KEY_SECURE_VIEWER_EXCLUSIONS, packages) + .apply(); + } + + public static void markFirstRunDone(Context ctx) { + getPrefs(ctx).edit() + .putBoolean(KEY_FIRST_RUN, false) + .apply(); + } + + // ---- 読み込み(フック側プロセスから呼ばれる) ---- + + /** + * ターゲットアプリプロセス内からモジュールの SharedPreferences を読む。 + * createPackageContext 経由でモジュール自身のコンテキストを取得する。 + */ + private static SharedPreferences getPrefsFromTarget(Context context) { + try { + Context moduleCtx = context.createPackageContext( + "com.github.droserasprout.lockscreencamera", + Context.CONTEXT_IGNORE_SECURITY); + return moduleCtx.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + } catch (Exception e) { + Log.w(TAG, "Failed to access module prefs: " + e.getMessage()); + return null; + } + } + + /** + * フックプロセス用: 有効なカメラパッケージセットを取得する。 + * 設定が空の場合はフォールバックとしてデフォルトリストを返す。 + */ + public static Set getEnabledPackages(Context context) { + SharedPreferences prefs = getPrefsFromTarget(context); + if (prefs == null) return getDefaultPackages(); + + Set packages = prefs.getStringSet(KEY_ENABLED_PACKAGES, null); + if (packages == null || packages.isEmpty()) return getDefaultPackages(); + return new HashSet<>(packages); + } + + /** + * フックプロセス用: SecureViewer が有効かどうか。 + * デフォルトは true(有効)。 + */ + public static boolean isSecureViewerEnabled(Context context) { + SharedPreferences prefs = getPrefsFromTarget(context); + if (prefs == null) return true; + return prefs.getBoolean(KEY_SECURE_VIEWER_ENABLED, true); + } + + /** + * フックプロセス用: SecureViewer を無効化するパッケージセット。 + * GCam 等で独自レビューアを使いたい場合にここに追加する。 + */ + public static Set getSecureViewerExclusions(Context context) { + SharedPreferences prefs = getPrefsFromTarget(context); + if (prefs == null) return Collections.emptySet(); + Set exclusions = prefs.getStringSet(KEY_SECURE_VIEWER_EXCLUSIONS, null); + if (exclusions == null) return Collections.emptySet(); + return new HashSet<>(exclusions); + } + + /** + * フックプロセス用: 指定パッケージで SecureViewer を使うべきか。 + */ + public static boolean shouldUseSecureViewer(Context context, String pkg) { + if (!isSecureViewerEnabled(context)) return false; + return !getSecureViewerExclusions(context).contains(pkg); + } + + /** + * フックプロセス用: 指定パッケージが設定済みのカメラパッケージか。 + */ + public static boolean isPackageEnabled(Context context, String pkg) { + return getEnabledPackages(context).contains(pkg); + } + + // ---- デフォルト値 ---- + + /** フォールバック: 設定が空の場合に使うデフォルトパッケージ */ + private static Set getDefaultPackages() { + Set defaults = new HashSet<>(); + defaults.add("com.android.camera"); + defaults.add("com.google.android.GoogleCamera"); + defaults.add("com.android.camera2"); + defaults.add("org.codeaurora.snapcam"); + return defaults; + } + + /** + * 設定画面用: フォールバック候補パッケージ(自動検出に使われる補助リスト)。 + * resolveActivity で見つからない場合の追加候補。 + */ + public static Set getKnownCameraPackages() { + Set known = new HashSet<>(); + known.add("com.android.camera"); + known.add("com.android.camera2"); + known.add("com.google.android.GoogleCamera"); + known.add("com.android.MGC"); + known.add("org.codeaurora.snapcam"); + known.add("com.miui.camera"); + known.add("com.samsung.android.camera"); + known.add("com.oneplus.camera"); + known.add("com.oplus.camera"); + known.add("com.sonysmartphone.camera"); + return known; + } + + /** 初回起動かどうか(設定画面用) */ + public static boolean isFirstRun(Context ctx) { + return getPrefs(ctx).getBoolean(KEY_FIRST_RUN, true); + } +} \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml new file mode 100644 index 0000000..d293eb2 --- /dev/null +++ b/app/src/main/res/values/arrays.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 148300b..7682eb2 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,4 +1,31 @@ Secure Lockscreen Camera Skip authentication on \"Quickly open camera\" gesture. + + + 設定 + + + 対象カメラアプリ + モジュールを注入するカメラアプリを選択します + カメラアプリを自動検出 + インストール済みのカメラアプリを検出して一覧に追加します + 有効なカメラアプリ + 選択されたパッケージにのみフックを適用します + カメラアプリを選択 + カメラアプリが見つかりませんでした + %d 個のカメラアプリを検出しました + + + 写真ビューアー + 撮影後のプレビュー画面の動作を設定します + SecureViewer を使用 + 有効時はロック画面上で独自ビューアーにリダイレクトします。無効時はカメラアプリ標準のレビューアを使用します + ビューアー除外アプリ + 選択したアプリでは SecureViewer を無効化し、標準のレビューアーを使用します(GCam 等) + 除外アプリを選択 + + + 情報 + バージョン diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml new file mode 100644 index 0000000..bd8423c --- /dev/null +++ b/app/src/main/res/xml/preferences.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/resources/META-INF/xposed/scope.list b/app/src/main/resources/META-INF/xposed/scope.list index f220653..301f0f8 100755 --- a/app/src/main/resources/META-INF/xposed/scope.list +++ b/app/src/main/resources/META-INF/xposed/scope.list @@ -1,2 +1,11 @@ com.android.camera -system +com.android.camera2 +com.google.android.GoogleCamera +com.android.MGC +org.codeaurora.snapcam +com.miui.camera +com.samsung.android.camera +com.oneplus.camera +com.oplus.camera +com.sonysmartphone.camera +system \ No newline at end of file From e57a48331cf429d8193deeca3f283f61d1b3ed33 Mon Sep 17 00:00:00 2001 From: FyukMdaa Date: Fri, 31 Jul 2026 13:24:20 +0900 Subject: [PATCH 05/17] fix import --- .../droserasprout/lockscreencamera/LockscreenCamera.java | 4 ++-- .../droserasprout/lockscreencamera/SettingsActivity.java | 1 + .../lockscreencamera/util/CameraPackageUtil.java | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/LockscreenCamera.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/LockscreenCamera.java index ef9f935..53de47f 100755 --- a/app/src/main/java/com/github/droserasprout/lockscreencamera/LockscreenCamera.java +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/LockscreenCamera.java @@ -40,7 +40,7 @@ public LockscreenCamera() { @Override public void onPackageReady(@NonNull PackageReadyParam param) { String pkg = param.getPackageName(); - Context context = (Context) param.getAndroidContext(); + Context context = (Context) param.getApplication(); // 設定ベースの判定(フォールバック付き) boolean enabled; @@ -57,7 +57,7 @@ public void onPackageReady(@NonNull PackageReadyParam param) { log(Log.INFO, TAG, "Targeting Camera App: " + pkg); - DecorViewProtectionHook.install(this); + DecorViewProtectionHook.install(this, context); KeyguardDismissBlockHook.install(this); ActivityVisibilitySpoofHook.install(this, context); KeyguardIntentRewriteHook.install(this, context); diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/SettingsActivity.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/SettingsActivity.java index dca5d55..6312041 100644 --- a/app/src/main/java/com/github/droserasprout/lockscreencamera/SettingsActivity.java +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/SettingsActivity.java @@ -1,5 +1,6 @@ package com.github.droserasprout.lockscreencamera; +import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/util/CameraPackageUtil.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/util/CameraPackageUtil.java index 0347e28..d7585d6 100755 --- a/app/src/main/java/com/github/droserasprout/lockscreencamera/util/CameraPackageUtil.java +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/util/CameraPackageUtil.java @@ -4,8 +4,6 @@ import android.content.Context; import android.view.View; -import java.util.Set; - /** * カメラアプリのパッケージ/コンテキスト/DecorView かどうかを判定するヘルパー群。 *

@@ -67,6 +65,7 @@ public static boolean isCameraActivity(Activity act, Context context) { * @deprecated 設定連動版 {@link #isCameraActivity(Activity, Context)} を推奨。 * フック内で Context が取れない場合のフォールバック用。 */ + @Deprecated public static boolean isCameraActivity(Activity act) { if (act == null) return false; try { @@ -89,6 +88,7 @@ public static boolean isCameraContext(Context ctx, Context settingsContext) { /** * @deprecated 設定連動版 {@link #isCameraContext(Context, Context)} を推奨。 */ + @Deprecated public static boolean isCameraContext(Context ctx) { if (ctx == null) return false; try { From 5825d0c404a4422e10f7a5c417e77940b6a78ec5 Mon Sep 17 00:00:00 2001 From: FyukMdaa Date: Fri, 31 Jul 2026 13:46:11 +0900 Subject: [PATCH 06/17] fix api hook --- .../lockscreencamera/LockscreenCamera.java | 30 ++++++++---- .../hook/ActivityVisibilitySpoofHook.java | 14 +++--- .../hook/CameraActivityLifecycleHook.java | 9 ++-- .../hook/DecorViewProtectionHook.java | 14 +++--- .../hook/GalleryRedirectHook.java | 11 +++-- .../hook/KeyguardIntentRewriteHook.java | 12 ++--- .../util/CameraPackageUtil.java | 38 +++++++-------- .../lockscreencamera/util/ModulePrefs.java | 46 ++++++------------- 8 files changed, 85 insertions(+), 89 deletions(-) diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/LockscreenCamera.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/LockscreenCamera.java index 53de47f..034afea 100755 --- a/app/src/main/java/com/github/droserasprout/lockscreencamera/LockscreenCamera.java +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/LockscreenCamera.java @@ -1,6 +1,6 @@ package com.github.droserasprout.lockscreencamera; -import android.content.Context; +import android.content.SharedPreferences; import android.util.Log; import androidx.annotation.NonNull; @@ -28,6 +28,8 @@ * パッケージ判定について: * 設定画面で選択されたパッケージリストを SharedPreferences から読み出し、 * 一致する場合のみフックを適用する。設定が空の場合はフォールバックリストを使う。 + * libxposed API 101 では Context が取得できないため、 + * {@code getRemotePreferences()} で設定を読み取る。 */ public class LockscreenCamera extends XposedModule { @@ -40,13 +42,21 @@ public LockscreenCamera() { @Override public void onPackageReady(@NonNull PackageReadyParam param) { String pkg = param.getPackageName(); - Context context = (Context) param.getApplication(); - // 設定ベースの判定(フォールバック付き) - boolean enabled; + // libxposed API 101: Context は取得できないため getRemotePreferences を使用 + SharedPreferences prefs; try { - enabled = ModulePrefs.isPackageEnabled(context, pkg); + prefs = getRemotePreferences(ModulePrefs.PREFS_NAME); } catch (Exception e) { + log(Log.WARN, TAG, "Failed to get remote preferences, using fallback"); + prefs = null; + } + + // 設定ベースの判定(フォールバック付き) + boolean enabled; + if (prefs != null) { + enabled = ModulePrefs.isPackageEnabled(prefs, pkg); + } else { enabled = CameraPackageUtil.isCameraPackage(pkg); } @@ -57,12 +67,12 @@ public void onPackageReady(@NonNull PackageReadyParam param) { log(Log.INFO, TAG, "Targeting Camera App: " + pkg); - DecorViewProtectionHook.install(this, context); + DecorViewProtectionHook.install(this, prefs); KeyguardDismissBlockHook.install(this); - ActivityVisibilitySpoofHook.install(this, context); - KeyguardIntentRewriteHook.install(this, context); - GalleryRedirectHook.install(this, context); - CameraActivityLifecycleHook.install(this, context); + ActivityVisibilitySpoofHook.install(this, prefs); + KeyguardIntentRewriteHook.install(this, prefs); + GalleryRedirectHook.install(this, prefs); + CameraActivityLifecycleHook.install(this, prefs); MediaStoreSessionTrackingHook.install(this); MiscSystemHook.install(this, param); } diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/ActivityVisibilitySpoofHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/ActivityVisibilitySpoofHook.java index 3fc92fe..1226b14 100755 --- a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/ActivityVisibilitySpoofHook.java +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/ActivityVisibilitySpoofHook.java @@ -1,7 +1,7 @@ package com.github.droserasprout.lockscreencamera.hook; import android.app.Activity; -import android.content.Context; +import android.content.SharedPreferences; import com.github.droserasprout.lockscreencamera.util.CameraPackageUtil; @@ -10,21 +10,21 @@ /** hasWindowFocus/isResumed を常に true として返す(フォーカス喪失の誤検知対策)。 */ public final class ActivityVisibilitySpoofHook { - private static Context settingsContext; + private static SharedPreferences prefs; private ActivityVisibilitySpoofHook() {} - public static void install(XposedModule module, Context context) { - settingsContext = context; + public static void install(XposedModule module, SharedPreferences prefs) { + ActivityVisibilitySpoofHook.prefs = prefs; try { module.hook(Activity.class.getDeclaredMethod("hasWindowFocus")).intercept(chain -> { - if (CameraPackageUtil.isCameraActivity((Activity) chain.getThisObject(), settingsContext)) return true; + if (CameraPackageUtil.isCameraActivity((Activity) chain.getThisObject(), ActivityVisibilitySpoofHook.prefs)) return true; return (Boolean) chain.proceed(); }); module.hook(Activity.class.getDeclaredMethod("isResumed")).intercept(chain -> { - if (CameraPackageUtil.isCameraActivity((Activity) chain.getThisObject(), settingsContext)) return true; + if (CameraPackageUtil.isCameraActivity((Activity) chain.getThisObject(), ActivityVisibilitySpoofHook.prefs)) return true; return (Boolean) chain.proceed(); }); } catch (Throwable ignored) {} } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraActivityLifecycleHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraActivityLifecycleHook.java index c809667..26cf28b 100755 --- a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraActivityLifecycleHook.java +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraActivityLifecycleHook.java @@ -6,6 +6,7 @@ import android.content.ContextWrapper; import android.content.Intent; import android.content.IntentFilter; +import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; import android.util.Log; @@ -32,12 +33,12 @@ public final class CameraActivityLifecycleHook { {"attachBaseContext", "onCreate", "onStart", "onResume", "onWindowFocusChanged", "onDestroy"}; private static final Map ACTIVE_RECEIVERS = new WeakHashMap<>(); - private static Context settingsContext; + private static SharedPreferences prefs; private CameraActivityLifecycleHook() {} - public static void install(XposedModule module, Context context) { - settingsContext = context; + public static void install(XposedModule module, SharedPreferences prefs) { + CameraActivityLifecycleHook.prefs = prefs; for (String methodName : LIFECYCLE_METHODS) { try { @@ -48,7 +49,7 @@ public static void install(XposedModule module, Context context) { if (!(thisObj instanceof Activity)) return chain.proceed(); Activity act = (Activity) thisObj; - if (!CameraPackageUtil.isCameraActivity(act, settingsContext)) return chain.proceed(); + if (!CameraPackageUtil.isCameraActivity(act, CameraActivityLifecycleHook.prefs)) return chain.proceed(); if ("onDestroy".equals(mName)) { if (SessionManager.isActive) { diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/DecorViewProtectionHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/DecorViewProtectionHook.java index 803d1c0..946543d 100755 --- a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/DecorViewProtectionHook.java +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/DecorViewProtectionHook.java @@ -1,6 +1,6 @@ package com.github.droserasprout.lockscreencamera.hook; -import android.content.Context; +import android.content.SharedPreferences; import android.util.Log; import android.view.View; @@ -16,18 +16,18 @@ public final class DecorViewProtectionHook { private static final String TAG = "LockscreenCamera"; - private static Context settingsContext; + private static SharedPreferences prefs; private DecorViewProtectionHook() {} - public static void install(XposedModule module, Context context) { - settingsContext = context; + public static void install(XposedModule module, SharedPreferences prefs) { + DecorViewProtectionHook.prefs = prefs; try { module.hook(View.class.getDeclaredMethod("setAlpha", float.class)).intercept(chain -> { View view = (View) chain.getThisObject(); if (CameraPackageUtil.isDecorView(view) - && CameraPackageUtil.isCameraContext(view.getContext(), settingsContext)) { + && CameraPackageUtil.isCameraContext(view.getContext(), DecorViewProtectionHook.prefs)) { List args = chain.getArgs(); float alpha = (float) args.get(0); if (alpha < 1.0f) args.set(0, 1.0f); @@ -41,7 +41,7 @@ public static void install(XposedModule module, Context context) { try { module.hook(View.class.getDeclaredMethod("setVisibility", int.class)).intercept(chain -> { View view = (View) chain.getThisObject(); - if (CameraPackageUtil.isCameraContext(view.getContext(), settingsContext)) { + if (CameraPackageUtil.isCameraContext(view.getContext(), DecorViewProtectionHook.prefs)) { List args = chain.getArgs(); int vis = (int) args.get(0); if (vis != View.VISIBLE) args.set(0, View.VISIBLE); @@ -52,4 +52,4 @@ public static void install(XposedModule module, Context context) { module.log(Log.WARN, TAG, "DecorView setVisibility hook failed: " + t); } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/GalleryRedirectHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/GalleryRedirectHook.java index fb4a265..24b2ca0 100755 --- a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/GalleryRedirectHook.java +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/GalleryRedirectHook.java @@ -6,6 +6,7 @@ import android.content.Context; import android.content.ContextWrapper; import android.content.Intent; +import android.content.SharedPreferences; import android.net.Uri; import android.os.Build; import android.util.Log; @@ -34,12 +35,12 @@ public final class GalleryRedirectHook { private static final int FLAG_SHOW_WHEN_LOCKED_HIDDEN = 0x00080000 | 0x00400000 | 0x00200000; - private static Context settingsContext; + private static SharedPreferences prefs; private GalleryRedirectHook() {} - public static void install(XposedModule module, Context context) { - settingsContext = context; + public static void install(XposedModule module, SharedPreferences prefs) { + GalleryRedirectHook.prefs = prefs; try { Method startAct = Activity.class.getDeclaredMethod("startActivity", Intent.class); module.hook(startAct).intercept(chain -> { @@ -69,10 +70,10 @@ private static void handleGalleryRedirect(Context ctx, Intent intent) { String pkg; try { pkg = ctx.getPackageName(); } catch (Exception e) { return; } - if (!CameraPackageUtil.isCameraPackage(pkg, settingsContext)) return; + if (!CameraPackageUtil.isCameraPackage(pkg, GalleryRedirectHook.prefs)) return; // 設定で SecureViewer が無効化されているパッケージならスキップ - if (!ModulePrefs.shouldUseSecureViewer(settingsContext, pkg)) { + if (!ModulePrefs.shouldUseSecureViewer(GalleryRedirectHook.prefs, pkg)) { Log.d(TAG, "SecureViewer disabled for: " + pkg); return; } diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/KeyguardIntentRewriteHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/KeyguardIntentRewriteHook.java index f531cca..1f693ad 100755 --- a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/KeyguardIntentRewriteHook.java +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/KeyguardIntentRewriteHook.java @@ -1,8 +1,8 @@ package com.github.droserasprout.lockscreencamera.hook; import android.app.Activity; -import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import com.github.droserasprout.lockscreencamera.util.CameraPackageUtil; @@ -12,17 +12,17 @@ public final class KeyguardIntentRewriteHook { private static final String EXTRA_START_BY_KEYGUARD = "com.miui.camera.extra.START_BY_KEYGUARD"; - private static Context settingsContext; + private static SharedPreferences prefs; private KeyguardIntentRewriteHook() {} - public static void install(XposedModule module, Context context) { - settingsContext = context; + public static void install(XposedModule module, SharedPreferences prefs) { + KeyguardIntentRewriteHook.prefs = prefs; try { module.hook(Activity.class.getDeclaredMethod("getIntent")).intercept(chain -> { Intent intent = (Intent) chain.proceed(); Activity act = (Activity) chain.getThisObject(); - if (CameraPackageUtil.isCameraActivity(act, settingsContext) && intent != null + if (CameraPackageUtil.isCameraActivity(act, KeyguardIntentRewriteHook.prefs) && intent != null && intent.getBooleanExtra(EXTRA_START_BY_KEYGUARD, false)) { intent.putExtra("is_secure_camera", true); intent.putExtra("ShowCameraWhenLocked", true); @@ -31,4 +31,4 @@ public static void install(XposedModule module, Context context) { }); } catch (Throwable ignored) {} } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/util/CameraPackageUtil.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/util/CameraPackageUtil.java index d7585d6..6939ee0 100755 --- a/app/src/main/java/com/github/droserasprout/lockscreencamera/util/CameraPackageUtil.java +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/util/CameraPackageUtil.java @@ -2,6 +2,7 @@ import android.app.Activity; import android.content.Context; +import android.content.SharedPreferences; import android.view.View; /** @@ -18,20 +19,18 @@ private CameraPackageUtil() {} /** * フックプロセス用: 設定済みパッケージリストと照合する。 - * context はターゲットアプリの Context(Activity 等)。 + * prefs は {@code XposedModule.getRemotePreferences()} で取得したもの。 */ - public static boolean isCameraPackage(String pkg, Context context) { - if (pkg == null || context == null) return false; - return ModulePrefs.isPackageEnabled(context, pkg); + public static boolean isCameraPackage(String pkg, SharedPreferences prefs) { + if (pkg == null || prefs == null) return false; + return ModulePrefs.isPackageEnabled(prefs, pkg); } /** - * フォールバック用: context が取得できない場合にデフォルトリストで判定する。 - * 設定画面が未設定の場合もこちらにフォールバックする。 + * フォールバック用: prefs が取得できない場合にデフォルトリストで判定する。 */ public static boolean isCameraPackage(String pkg) { if (pkg == null) return false; - // ワイルドカードマッチは使わず、明示的なパッケージ名のみ return pkg.equals("com.android.camera") || pkg.equals("com.google.android.GoogleCamera") || pkg.equals("com.android.camera2") @@ -43,27 +42,26 @@ public static boolean isCameraPackage(String pkg) { * 設定が利用可能かどうかをチェックし、 * 可能なら設定リスト、不可ならフォールバックリストを使う。 */ - public static boolean isCameraPackage(String pkg, Context context, boolean useSettings) { - if (!useSettings || context == null) return isCameraPackage(pkg); + public static boolean isCameraPackage(String pkg, SharedPreferences prefs, boolean useSettings) { + if (!useSettings || prefs == null) return isCameraPackage(pkg); try { - return isCameraPackage(pkg, context); + return isCameraPackage(pkg, prefs); } catch (Exception e) { return isCameraPackage(pkg); } } - public static boolean isCameraActivity(Activity act, Context context) { + public static boolean isCameraActivity(Activity act, SharedPreferences prefs) { if (act == null) return false; try { - return isCameraPackage(act.getPackageName(), context); + return isCameraPackage(act.getPackageName(), prefs); } catch (Exception e) { return isCameraPackage(act.getPackageName()); } } /** - * @deprecated 設定連動版 {@link #isCameraActivity(Activity, Context)} を推奨。 - * フック内で Context が取れない場合のフォールバック用。 + * @deprecated SharedPreferences 版 {@link #isCameraActivity(Activity, SharedPreferences)} を推奨。 */ @Deprecated public static boolean isCameraActivity(Activity act) { @@ -76,17 +74,21 @@ public static boolean isCameraActivity(Activity act) { } } - public static boolean isCameraContext(Context ctx, Context settingsContext) { + /** + * フックプロセス用: View の Context のパッケージがカメラか判定する。 + * prefs は {@code XposedModule.getRemotePreferences()} で取得したもの。 + */ + public static boolean isCameraContext(Context ctx, SharedPreferences prefs) { if (ctx == null) return false; try { - return isCameraPackage(ctx.getPackageName(), settingsContext); + return isCameraPackage(ctx.getPackageName(), prefs); } catch (Exception e) { return isCameraPackage(ctx.getPackageName()); } } /** - * @deprecated 設定連動版 {@link #isCameraContext(Context, Context)} を推奨。 + * @deprecated SharedPreferences 版 {@link #isCameraContext(Context, SharedPreferences)} を推奨。 */ @Deprecated public static boolean isCameraContext(Context ctx) { @@ -101,4 +103,4 @@ public static boolean isCameraContext(Context ctx) { public static boolean isDecorView(View v) { return v != null && v.getClass().getName().endsWith("DecorView"); } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/util/ModulePrefs.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/util/ModulePrefs.java index 6a1b43a..94fcd8b 100644 --- a/app/src/main/java/com/github/droserasprout/lockscreencamera/util/ModulePrefs.java +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/util/ModulePrefs.java @@ -11,8 +11,10 @@ /** * モジュールの SharedPreferences への読み書きを一本化するヘルパー。 *

- * 設定画面(モジュール自身のプロセス)と - * フック側(ターゲットアプリのプロセス)の両方から使われる。 + * 設定画面(モジュール自身のプロセス)では Context ベースのメソッドを、 + * フック側(ターゲットアプリのプロセス)では SharedPreferences ベースのメソッドを使う。 + * フック側では {@code XposedModule.getRemotePreferences(PREFS_NAME)} で取得した + * SharedPreferences を渡す。 */ public final class ModulePrefs { @@ -59,30 +61,14 @@ public static void markFirstRunDone(Context ctx) { // ---- 読み込み(フック側プロセスから呼ばれる) ---- - /** - * ターゲットアプリプロセス内からモジュールの SharedPreferences を読む。 - * createPackageContext 経由でモジュール自身のコンテキストを取得する。 - */ - private static SharedPreferences getPrefsFromTarget(Context context) { - try { - Context moduleCtx = context.createPackageContext( - "com.github.droserasprout.lockscreencamera", - Context.CONTEXT_IGNORE_SECURITY); - return moduleCtx.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); - } catch (Exception e) { - Log.w(TAG, "Failed to access module prefs: " + e.getMessage()); - return null; - } - } - /** * フックプロセス用: 有効なカメラパッケージセットを取得する。 * 設定が空の場合はフォールバックとしてデフォルトリストを返す。 + * + * @param prefs {@code XposedModule.getRemotePreferences(PREFS_NAME)} で取得したもの */ - public static Set getEnabledPackages(Context context) { - SharedPreferences prefs = getPrefsFromTarget(context); + public static Set getEnabledPackages(SharedPreferences prefs) { if (prefs == null) return getDefaultPackages(); - Set packages = prefs.getStringSet(KEY_ENABLED_PACKAGES, null); if (packages == null || packages.isEmpty()) return getDefaultPackages(); return new HashSet<>(packages); @@ -92,18 +78,15 @@ public static Set getEnabledPackages(Context context) { * フックプロセス用: SecureViewer が有効かどうか。 * デフォルトは true(有効)。 */ - public static boolean isSecureViewerEnabled(Context context) { - SharedPreferences prefs = getPrefsFromTarget(context); + public static boolean isSecureViewerEnabled(SharedPreferences prefs) { if (prefs == null) return true; return prefs.getBoolean(KEY_SECURE_VIEWER_ENABLED, true); } /** * フックプロセス用: SecureViewer を無効化するパッケージセット。 - * GCam 等で独自レビューアを使いたい場合にここに追加する。 */ - public static Set getSecureViewerExclusions(Context context) { - SharedPreferences prefs = getPrefsFromTarget(context); + public static Set getSecureViewerExclusions(SharedPreferences prefs) { if (prefs == null) return Collections.emptySet(); Set exclusions = prefs.getStringSet(KEY_SECURE_VIEWER_EXCLUSIONS, null); if (exclusions == null) return Collections.emptySet(); @@ -113,16 +96,16 @@ public static Set getSecureViewerExclusions(Context context) { /** * フックプロセス用: 指定パッケージで SecureViewer を使うべきか。 */ - public static boolean shouldUseSecureViewer(Context context, String pkg) { - if (!isSecureViewerEnabled(context)) return false; - return !getSecureViewerExclusions(context).contains(pkg); + public static boolean shouldUseSecureViewer(SharedPreferences prefs, String pkg) { + if (!isSecureViewerEnabled(prefs)) return false; + return !getSecureViewerExclusions(prefs).contains(pkg); } /** * フックプロセス用: 指定パッケージが設定済みのカメラパッケージか。 */ - public static boolean isPackageEnabled(Context context, String pkg) { - return getEnabledPackages(context).contains(pkg); + public static boolean isPackageEnabled(SharedPreferences prefs, String pkg) { + return getEnabledPackages(prefs).contains(pkg); } // ---- デフォルト値 ---- @@ -139,7 +122,6 @@ private static Set getDefaultPackages() { /** * 設定画面用: フォールバック候補パッケージ(自動検出に使われる補助リスト)。 - * resolveActivity で見つからない場合の追加候補。 */ public static Set getKnownCameraPackages() { Set known = new HashSet<>(); From 79db1169708fd6f85686fcab3e983fa347d36781 Mon Sep 17 00:00:00 2001 From: FyukMdaa Date: Fri, 31 Jul 2026 14:01:39 +0900 Subject: [PATCH 07/17] add QUERY_ALL_PACKAGES parmission --- app/src/main/AndroidManifest.xml | 4 ++++ .../droserasprout/lockscreencamera/SettingsActivity.java | 8 ++------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8e5b810..3e3b517 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,10 @@ + + + detectCameraPackages() { private void addIfInstalled(Set target, String pkg, PackageManager pm) { try { - PackageInfo pi = pm.getPackageInfo(pkg, 0); - if ((pi.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0 - || (pi.applicationInfo.flags & ApplicationInfo.FLAG_INSTALLED) != 0) { - target.add(pkg); - } + pm.getPackageInfo(pkg, 0); + target.add(pkg); } catch (PackageManager.NameNotFoundException ignored) { // 未インストール } From fc2309fcd1f814ad2dfbdc69a605b59ae0ba7383 Mon Sep 17 00:00:00 2001 From: FyukMdaa Date: Fri, 31 Jul 2026 14:04:30 +0900 Subject: [PATCH 08/17] update version --- app/src/main/res/xml/preferences.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index bd8423c..14e4f39 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -48,7 +48,7 @@ + android:summary="3.1.0" /> From b63dc62ff856f0f2beb4392e7bff40d11a7222be Mon Sep 17 00:00:00 2001 From: FyukMdaa Date: Fri, 31 Jul 2026 14:33:05 +0900 Subject: [PATCH 09/17] fix some hook --- app/build.gradle | 2 +- .../SecureViewerActivity.java | 64 +++++-- .../lockscreencamera/SettingsActivity.java | 14 +- .../hook/CameraActivityLifecycleHook.java | 38 +++- .../hook/CameraGestureLauncherHook.java | 170 ++++++++++++++---- .../hook/GalleryRedirectHook.java | 11 +- .../lockscreencamera/util/ModulePrefs.java | 16 ++ app/src/main/res/values/strings.xml | 6 + app/src/main/res/xml/preferences.xml | 13 ++ 9 files changed, 276 insertions(+), 58 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 7cd60ff..bf6900c 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,7 +11,7 @@ android { minSdk 30 targetSdk 34 versionCode 3 - versionName "2.1.0" + versionName "3.1.0" } signingConfigs { diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/SecureViewerActivity.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/SecureViewerActivity.java index 2757bcf..7f03f7b 100755 --- a/app/src/main/java/com/github/droserasprout/lockscreencamera/SecureViewerActivity.java +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/SecureViewerActivity.java @@ -9,6 +9,7 @@ import android.os.Build; import android.os.Bundle; import android.util.Log; +import android.view.KeyEvent; import android.view.ViewGroup; import android.view.WindowManager; @@ -33,9 +34,7 @@ public class SecureViewerActivity extends Activity { private static final String TAG = "SecureViewer"; private static final String EXTRA_SESSION_PHOTOS = "session_photos_list"; - // 並列デコードのためスレッド数を2に確保 private final ExecutorService executor = Executors.newFixedThreadPool(2); - // position → 実行中タスク のマップ(高速スワイプ時の古いタスクをキャンセルするため) private final ConcurrentHashMap> pendingTasks = new ConcurrentHashMap<>(); private BroadcastReceiver screenOffReceiver; @@ -45,38 +44,71 @@ public class SecureViewerActivity extends Activity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + // ロック画面上に確実に表示するための設定 setShowWhenLocked(true); setTurnScreenOn(true); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { setInheritShowWhenLocked(true); } - // LockscreenCamera 側の clearFlags フックとは異なり、 - // ここではスクリーンショット禁止のため FLAG_SECURE を意図的に維持する - getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE); + + // Window レベルでもフラグを設定(API 27 以降は Activity API で十分だが、 + // 一部 OEM では Window フラグも必要) + Window window = getWindow(); + if (window != null) { + WindowManager.LayoutParams lp = window.getAttributes(); + lp.flags |= WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED + | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON; + window.setAttributes(lp); + // スクリーンショット禁止 + window.setFlags( + WindowManager.LayoutParams.FLAG_SECURE, + WindowManager.LayoutParams.FLAG_SECURE); + } List safeUris = resolveSafeUris(); if (safeUris.isEmpty()) { + Log.w(TAG, "No URIs to display, finishing"); finish(); return; } + Log.i(TAG, "Displaying " + safeUris.size() + " photos"); setupViewPager(safeUris); registerScreenOffReceiver(); } - /** - * Intent から表示対象の URI リストを解決する。 - * 修正メモ: リファクタリング版では content:// 以外のスキームを厳格に除外していたが、 - * 一部のカメラアプリが file:// URI を渡すケースがあるため、 - * オリジナルの動作に合わせて null チェックのみとしスキーマ検証は行わない。 - */ + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + if (hasFocus) { + // フォーカス取得時に再度ロック画面上への表示を確保 + setShowWhenLocked(true); + getWindow().addFlags( + WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED + | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); + } + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + // バックキーで閉じる + if (keyCode == KeyEvent.KEYCODE_BACK) { + finish(); + return true; + } + return super.onKeyDown(keyCode, event); + } + private List resolveSafeUris() { List uris = getIntent().getParcelableArrayListExtra(EXTRA_SESSION_PHOTOS); if (uris == null || uris.isEmpty()) { uris = new ArrayList<>(); Uri singleUri = getIntent().getData(); if (singleUri != null) uris.add(singleUri); - Log.i(TAG, "Fallback to single image from Intent Data"); + if (!uris.isEmpty()) { + Log.i(TAG, "Fallback to single image from Intent Data: " + uris.get(0)); + } } return uris != null ? uris : new ArrayList<>(); } @@ -117,17 +149,13 @@ public void onReceive(Context context, Intent intent) { @Override protected void onStop() { super.onStop(); - // onDestroy より早い段階で解除することで、プロセスが強制終了する前にも対応 unregisterScreenOffReceiver(); } @Override protected void onDestroy() { super.onDestroy(); - // onStop で解除済みでも念のためガードする unregisterScreenOffReceiver(); - - // 実行中タスクを全キャンセル for (Future task : pendingTasks.values()) { task.cancel(true); } @@ -139,9 +167,7 @@ private void unregisterScreenOffReceiver() { if (screenOffReceiver == null) return; try { unregisterReceiver(screenOffReceiver); - } catch (Exception ignored) { - // 既に解除済み - } + } catch (Exception ignored) {} screenOffReceiver = null; } } diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/SettingsActivity.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/SettingsActivity.java index 467a769..9161984 100644 --- a/app/src/main/java/com/github/droserasprout/lockscreencamera/SettingsActivity.java +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/SettingsActivity.java @@ -20,7 +20,6 @@ import com.github.droserasprout.lockscreencamera.util.ModulePrefs; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -61,6 +60,7 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setupAutoDetect(); setupPackageList(); setupViewerSettings(); + setupLockscreenSettings(); } // ---- 自動検出 ---- @@ -283,5 +283,17 @@ private String buildExclusionSummary(Set exclusions) { } return exclusions.size() + " 個のアプリで無効化"; } + + // ---- ロック画面動作設定 ---- + + private void setupLockscreenSettings() { + SwitchPreferenceCompat showAboveLock = findPreference("pref_show_above_lock"); + if (showAboveLock != null) { + showAboveLock.setOnPreferenceChangeListener((preference, newValue) -> { + ModulePrefs.setShowAboveLock(requireContext(), (Boolean) newValue); + return true; + }); + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraActivityLifecycleHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraActivityLifecycleHook.java index 26cf28b..aa87000 100755 --- a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraActivityLifecycleHook.java +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraActivityLifecycleHook.java @@ -18,6 +18,7 @@ import com.github.droserasprout.lockscreencamera.session.SessionManager; import com.github.droserasprout.lockscreencamera.util.CameraPackageUtil; +import com.github.droserasprout.lockscreencamera.util.ModulePrefs; import io.github.libxposed.api.XposedModule; @@ -32,6 +33,7 @@ public final class CameraActivityLifecycleHook { private static final String[] LIFECYCLE_METHODS = {"attachBaseContext", "onCreate", "onStart", "onResume", "onWindowFocusChanged", "onDestroy"}; + private static final Map LOCKSCREEN_LAUNCHES = new WeakHashMap<>(); private static final Map ACTIVE_RECEIVERS = new WeakHashMap<>(); private static SharedPreferences prefs; @@ -56,13 +58,16 @@ public static void install(XposedModule module, SharedPreferences prefs) { SessionManager.end(); } unregisterReceiver(act); + LOCKSCREEN_LAUNCHES.remove(act); return chain.proceed(); } if ("onWindowFocusChanged".equals(mName)) { boolean hasFocus = (boolean) ((List) chain.getArgs()).get(0); if (!hasFocus) return chain.proceed(); - WindowSecurityBypass.apply(act); + if (shouldApplyWindowBypass(act)) { + WindowSecurityBypass.apply(act); + } return chain.proceed(); } @@ -71,7 +76,18 @@ public static void install(XposedModule module, SharedPreferences prefs) { Intent intent = act.getIntent(); boolean isLockscreenLaunch = - intent != null && intent.getBooleanExtra(EXTRA_START_BY_KEYGUARD, false); + intent != null && (intent.getBooleanExtra(EXTRA_START_BY_KEYGUARD, false) + || intent.getBooleanExtra("is_secure_camera", false) + || intent.getBooleanExtra("StartActivityWhenLocked", false)); + + // INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE で起動された場合もロック画面起動とみなす + if (!isLockscreenLaunch && intent != null + && intent.getAction() != null + && intent.getAction().contains("SECURE")) { + isLockscreenLaunch = true; + } + + LOCKSCREEN_LAUNCHES.put(act, isLockscreenLaunch); if (isLockscreenLaunch) { SessionManager.start(); @@ -79,17 +95,31 @@ public static void install(XposedModule module, SharedPreferences prefs) { registerScreenOffReceiver(act); } - WindowSecurityBypass.apply(act); + if (shouldApplyWindowBypass(act)) { + WindowSecurityBypass.apply(act); + } return res; } - WindowSecurityBypass.apply(act); + if (shouldApplyWindowBypass(act)) { + WindowSecurityBypass.apply(act); + } return chain.proceed(); }); } catch (Throwable ignored) {} } } + /** + * ウィンドウセキュリティバイパスを適用すべきか判定する。 + * ロック画面起動時は常に適用。通常起動時は設定に依存。 + */ + private static boolean shouldApplyWindowBypass(Activity act) { + Boolean isLockscreen = LOCKSCREEN_LAUNCHES.get(act); + if (isLockscreen != null && isLockscreen) return true; + return ModulePrefs.shouldShowAboveLock(prefs); + } + private static Method resolveMethod(String methodName) throws NoSuchMethodException { switch (methodName) { case "attachBaseContext": diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraGestureLauncherHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraGestureLauncherHook.java index 433d352..07f946e 100755 --- a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraGestureLauncherHook.java +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraGestureLauncherHook.java @@ -17,42 +17,157 @@ import io.github.libxposed.api.XposedModuleInterface.SystemServerStartingParam; /** - * 電源ボタン二度押し等の「カメラジェスチャー」ハンドラ (GestureLauncherService) をフックし、 + * 電源ボタン二度押し等の「カメラジェスチャー」ハンドラをフックし、 * ロック中でも Secure Camera Intent を明示的な Component 指定で起動する。 - * MIUI の Resolver 経由起動やサードパーティ製カメラアプリが優先されない問題を回避する。 + * AOSP の GestureLauncherService と MIUI 固有クラスの両方に対応する。 */ public final class CameraGestureLauncherHook { - private static final String TAG = "LockscreenCamera"; + private static final String TAG = "LockscreenCamera.Gesture"; private static final String EXTRA_START_BY_KEYGUARD = "com.miui.camera.extra.START_BY_KEYGUARD"; - private static final int FLAG_ACTIVITY_MATCH_EXTERNAL = 0x00040000; // hidden flag private static final int BACKGROUND_ACTIVITY_START_ALLOWED = 2; private CameraGestureLauncherHook() {} public static void install(XposedModule module, SystemServerStartingParam param) { + boolean hooked = false; + + // 1. AOSP GestureLauncherService + hooked |= tryHookGestureService(module, param.getClassLoader(), + "com.android.server.GestureLauncherService"); + + // 2. MIUI GestureLauncherService(一部 MIUI/HyperOS バージョン) + hooked |= tryHookGestureService(module, param.getClassLoader(), + "com.miui.server.GestureLauncherService"); + + // 3. MIUI のキーガード カメラ起動 (KeyguardCameraLauncher) + hooked |= tryHookMiuiKeyguardCamera(module, param.getClassLoader()); + + if (!hooked) { + module.log(Log.WARN, TAG, "No camera gesture hook point found on this device"); + } else { + module.log(Log.INFO, TAG, "Camera gesture hook installed successfully"); + } + } + + /** + * 指定された GestureLauncherService クラスの handleCameraGesture をフックする。 + * メソッドが見つからない場合は何もしない(false を返す)。 + */ + private static boolean tryHookGestureService(XposedModule module, ClassLoader cl, String className) { try { - Class gestureClass = Class.forName( - "com.android.server.GestureLauncherService", true, param.getClassLoader()); - Method handleCameraGesture = gestureClass.getDeclaredMethod("handleCameraGesture", boolean.class, int.class); - - module.hook(handleCameraGesture).intercept(chain -> { - try { - Method getContextMethod = chain.getThisObject().getClass().getMethod("getContext"); - Context context = (Context) getContextMethod.invoke(chain.getThisObject()); - launchSecureCamera(context); - return true; - } catch (Throwable t) { - module.log(Log.ERROR, TAG, "Failed to launch", t); + Class gestureClass = Class.forName(className, true, cl); + + // (boolean, int) シグネチャ + try { + Method m = gestureClass.getDeclaredMethod("handleCameraGesture", boolean.class, int.class); + module.hook(m).intercept(chain -> { + try { + Context ctx = getContextFromService(chain.getThisObject()); + launchSecureCamera(ctx); + return true; + } catch (Throwable t) { + module.log(Log.WARN, TAG, "Custom launch failed, falling back to original"); + return chain.proceed(); + } + }); + module.log(Log.INFO, TAG, "Hooked " + className + ".handleCameraGesture(boolean, int)"); + return true; + } catch (NoSuchMethodException ignored) {} + + // 引数なしシグネチャ(一部デバイス) + try { + Method m = gestureClass.getDeclaredMethod("handleCameraGesture"); + module.hook(m).intercept(chain -> { + try { + Context ctx = getContextFromService(chain.getThisObject()); + launchSecureCamera(ctx); + return null; + } catch (Throwable t) { + module.log(Log.WARN, TAG, "Custom launch failed, falling back to original"); + return chain.proceed(); + } + }); + module.log(Log.INFO, TAG, "Hooked " + className + ".handleCameraGesture()"); + return true; + } catch (NoSuchMethodException ignored) {} + + // (boolean) シグネチャ + try { + Method m = gestureClass.getDeclaredMethod("handleCameraGesture", boolean.class); + module.hook(m).intercept(chain -> { + try { + Context ctx = getContextFromService(chain.getThisObject()); + launchSecureCamera(ctx); + return null; + } catch (Throwable t) { + module.log(Log.WARN, TAG, "Custom launch failed, falling back to original"); + return chain.proceed(); + } + }); + module.log(Log.INFO, TAG, "Hooked " + className + ".handleCameraGesture(boolean)"); + return true; + } catch (NoSuchMethodException ignored) {} + + } catch (ClassNotFoundException ignored) {} + return false; + } + + /** + * MIUI 固有のキーガードカメラ起動をフックする。 + * launchCamera / startCameraFromKeyguard 等のメソッド名を試行する。 + */ + private static boolean tryHookMiuiKeyguardCamera(XposedModule module, ClassLoader cl) { + String[] classNames = { + "com.android.keyguard.KeyguardCameraLauncher", + "com.miui.keyguard.camera.KeyguardCameraLauncher", + "com.android.systemui.camera.CameraLauncher" + }; + String[] methodNames = { + "launchCamera", "startCamera", "startCameraFromKeyguard", "launchSecureCamera" + }; + + for (String cn : classNames) { + try { + Class clazz = Class.forName(cn, true, cl); + for (String mn : methodNames) { + for (Method m : clazz.getDeclaredMethods()) { + if (m.getName().equals(mn) && m.getParameterCount() == 0) { + module.hook(m).intercept(chain -> { + try { + Context ctx = getContextFromService(chain.getThisObject()); + if (ctx != null) launchSecureCamera(ctx); + } catch (Throwable t) { + module.log(Log.WARN, TAG, "MIUI camera launch failed: " + t.getMessage()); + } + return chain.proceed(); + }); + module.log(Log.INFO, TAG, "Hooked " + cn + "." + mn + "()"); + return true; + } + } } - return chain.proceed(); - }); - } catch (Throwable t) { - module.log(Log.WARN, TAG, "System hook skipped", t); + } catch (ClassNotFoundException ignored) {} + } + return false; + } + + private static Context getContextFromService(Object service) { + try { + Method m = service.getClass().getMethod("getContext"); + return (Context) m.invoke(service); + } catch (Exception e) { + Log.w(TAG, "getContext failed: " + e.getMessage()); + return null; } } private static void launchSecureCamera(Context context) { + if (context == null) { + Log.w(TAG, "launchSecureCamera: context is null"); + return; + } + KeyguardManager km = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); boolean isLocked = km != null && km.isKeyguardLocked(); @@ -63,19 +178,15 @@ private static void launchSecureCamera(Context context) { intent.setComponent(target); } - int flags = FLAG_ACTIVITY_MATCH_EXTERNAL | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP; + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); if (isLocked) { - flags |= Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS; + intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); intent.putExtra(EXTRA_START_BY_KEYGUARD, true); intent.putExtra("StartActivityWhenLocked", true); intent.putExtra("is_secure_camera", true); - } else { - intent.putExtra(EXTRA_START_BY_KEYGUARD, false); - intent.putExtra("StartActivityWhenLocked", false); } - intent.addFlags(flags); intent.putExtra("android.intent.extra.CAMERA_OPEN_ONLY", true); intent.putExtra("com.android.systemui.camera_launch_source", "lockscreen_affordance"); @@ -84,18 +195,18 @@ private static void launchSecureCamera(Context context) { options.setPendingIntentBackgroundActivityStartMode(BACKGROUND_ACTIVITY_START_ALLOWED); } - // SessionManager.start() はカメラアプリ側の onCreate (CameraActivityLifecycleHook) で - // 実行されるため、ここでは呼ばない + Log.i(TAG, "Launching secure camera: " + target + ", locked=" + isLocked); context.startActivity(intent, options.toBundle()); } private static ComponentName resolveCameraComponent(Context context) { PackageManager pm = context.getPackageManager(); + // SECURE アクションを試す Intent resolveIntent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE); ResolveInfo info = pm.resolveActivity(resolveIntent, PackageManager.MATCH_DEFAULT_ONLY); - // SECURE アクションを解決できない場合は通常アクションにフォールバック + // Resolver に解決された場合は通常アクションにフォールバック if (info == null || info.activityInfo.name.contains("Resolver")) { resolveIntent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA); info = pm.resolveActivity(resolveIntent, PackageManager.MATCH_DEFAULT_ONLY); @@ -104,7 +215,6 @@ private static ComponentName resolveCameraComponent(Context context) { if (info != null && !info.activityInfo.name.contains("Resolver")) { return new ComponentName(info.activityInfo.packageName, info.activityInfo.name); } - // 解決に失敗した場合の最終フォールバック(AOSP 標準カメラ) return new ComponentName("com.android.camera", "com.android.camera.Camera"); } } diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/GalleryRedirectHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/GalleryRedirectHook.java index 24b2ca0..33d97d6 100755 --- a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/GalleryRedirectHook.java +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/GalleryRedirectHook.java @@ -11,6 +11,8 @@ import android.os.Build; import android.util.Log; +import android.view.WindowManager; + import java.lang.reflect.Method; import java.util.ArrayList; @@ -33,7 +35,9 @@ public final class GalleryRedirectHook { private static final String VIEWER_PACKAGE = "com.github.droserasprout.lockscreencamera"; private static final String VIEWER_CLASS = "com.github.droserasprout.lockscreencamera.SecureViewerActivity"; - private static final int FLAG_SHOW_WHEN_LOCKED_HIDDEN = 0x00080000 | 0x00400000 | 0x00200000; + private static final int FLAG_SHOW_WHEN_LOCKED = WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED; + private static final int FLAG_TURN_SCREEN_ON = WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON; + private static final int FLAG_ALLOW_LOCK_WHILE_SCREEN_ON = WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON; private static SharedPreferences prefs; @@ -111,8 +115,9 @@ private static void handleGalleryRedirect(Context ctx, Intent intent) { | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP - | Intent.FLAG_ACTIVITY_NO_ANIMATION); - intent.addFlags(FLAG_SHOW_WHEN_LOCKED_HIDDEN); + | FLAG_SHOW_WHEN_LOCKED + | FLAG_TURN_SCREEN_ON + | FLAG_ALLOW_LOCK_WHILE_SCREEN_ON); Log.d(TAG, "Intent modification complete. Proceeding with hijacked intent."); } diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/util/ModulePrefs.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/util/ModulePrefs.java index 94fcd8b..97fbcd1 100644 --- a/app/src/main/java/com/github/droserasprout/lockscreencamera/util/ModulePrefs.java +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/util/ModulePrefs.java @@ -25,6 +25,7 @@ public final class ModulePrefs { public static final String KEY_ENABLED_PACKAGES = "enabled_packages"; public static final String KEY_SECURE_VIEWER_ENABLED = "secure_viewer_enabled"; public static final String KEY_SECURE_VIEWER_EXCLUSIONS = "secure_viewer_exclusions"; + public static final String KEY_SHOW_ABOVE_LOCK = "show_above_lock"; public static final String KEY_FIRST_RUN = "first_run"; private ModulePrefs() {} @@ -53,6 +54,12 @@ public static void setSecureViewerExclusions(Context ctx, Set packages) .apply(); } + public static void setShowAboveLock(Context ctx, boolean enabled) { + getPrefs(ctx).edit() + .putBoolean(KEY_SHOW_ABOVE_LOCK, enabled) + .apply(); + } + public static void markFirstRunDone(Context ctx) { getPrefs(ctx).edit() .putBoolean(KEY_FIRST_RUN, false) @@ -108,6 +115,15 @@ public static boolean isPackageEnabled(SharedPreferences prefs, String pkg) { return getEnabledPackages(prefs).contains(pkg); } + /** + * フックプロセス用: 通常起動時もカメラをロック画面上に表示するか。 + * デフォルトは false(ロック画面起動時のみ表示)。 + */ + public static boolean shouldShowAboveLock(SharedPreferences prefs) { + if (prefs == null) return false; + return prefs.getBoolean(KEY_SHOW_ABOVE_LOCK, false); + } + // ---- デフォルト値 ---- /** フォールバック: 設定が空の場合に使うデフォルトパッケージ */ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7682eb2..4c7298c 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -25,6 +25,12 @@ 選択したアプリでは SecureViewer を無効化し、標準のレビューアーを使用します(GCam 等) 除外アプリを選択 + + ロック画面動作 + ロック画面上でのカメラ表示動作を設定します + 通常起動時もロック画面上に表示 + オンにすると、ロック画面から起動していない場合でもカメラがロック画面上に表示されます。オフ(推奨)ではロック画面起動時のみ表示します + 情報 バージョン diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 14e4f39..d144e78 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -43,6 +43,19 @@ + + + + + + + Date: Fri, 31 Jul 2026 14:37:35 +0900 Subject: [PATCH 10/17] add import --- .../droserasprout/lockscreencamera/SecureViewerActivity.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/SecureViewerActivity.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/SecureViewerActivity.java index 7f03f7b..d763252 100755 --- a/app/src/main/java/com/github/droserasprout/lockscreencamera/SecureViewerActivity.java +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/SecureViewerActivity.java @@ -9,6 +9,8 @@ import android.os.Build; import android.os.Bundle; import android.util.Log; +import android.view.View; +import android.view.Window; import android.view.KeyEvent; import android.view.ViewGroup; import android.view.WindowManager; From d2bed1a33198da5c6b78c57bb838e6068e865a70 Mon Sep 17 00:00:00 2001 From: FyukMdaa Date: Fri, 31 Jul 2026 14:51:01 +0900 Subject: [PATCH 11/17] fix hook --- .../droserasprout/lockscreencamera/SecureViewerActivity.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/SecureViewerActivity.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/SecureViewerActivity.java index d763252..7f03f7b 100755 --- a/app/src/main/java/com/github/droserasprout/lockscreencamera/SecureViewerActivity.java +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/SecureViewerActivity.java @@ -9,8 +9,6 @@ import android.os.Build; import android.os.Bundle; import android.util.Log; -import android.view.View; -import android.view.Window; import android.view.KeyEvent; import android.view.ViewGroup; import android.view.WindowManager; From ba3d1bfdeae25cab0c15117f87f2cda599ab95a4 Mon Sep 17 00:00:00 2001 From: FyukMdaa Date: Fri, 31 Jul 2026 14:56:15 +0900 Subject: [PATCH 12/17] fix import --- .../droserasprout/lockscreencamera/SecureViewerActivity.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/SecureViewerActivity.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/SecureViewerActivity.java index 7f03f7b..891e6f2 100755 --- a/app/src/main/java/com/github/droserasprout/lockscreencamera/SecureViewerActivity.java +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/SecureViewerActivity.java @@ -11,8 +11,9 @@ import android.util.Log; import android.view.KeyEvent; import android.view.ViewGroup; -import android.view.WindowManager; +import android.view.WindowManager; +import android.view.Window; import androidx.viewpager2.widget.ViewPager2; import com.github.droserasprout.lockscreencamera.ui.PhotoAdapter; From 73efd027ebf92fb7587c87d09cc10a7b91d022ed Mon Sep 17 00:00:00 2001 From: FyukMdaa Date: Fri, 31 Jul 2026 15:08:22 +0900 Subject: [PATCH 13/17] remove wrong hook --- .../lockscreencamera/hook/GalleryRedirectHook.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/GalleryRedirectHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/GalleryRedirectHook.java index 33d97d6..80985fd 100755 --- a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/GalleryRedirectHook.java +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/GalleryRedirectHook.java @@ -86,7 +86,6 @@ private static void handleGalleryRedirect(Context ctx, Intent intent) { boolean isGallery = Intent.ACTION_VIEW.equals(action) || Intent.ACTION_PICK.equals(action) || action.contains("REVIEW") - || action.contains("STILL_IMAGE_CAMERA"); if (!isGallery) return; Log.i(TAG, "Redirecting to SecureViewer: Force hijacking intent"); @@ -121,4 +120,4 @@ private static void handleGalleryRedirect(Context ctx, Intent intent) { Log.d(TAG, "Intent modification complete. Proceeding with hijacked intent."); } -} \ No newline at end of file +} From 5113f755a1730ff6226e1d4065bd7fa253fae4dc Mon Sep 17 00:00:00 2001 From: FyukMdaa Date: Fri, 31 Jul 2026 15:12:12 +0900 Subject: [PATCH 14/17] fix syntax error --- .../lockscreencamera/hook/GalleryRedirectHook.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/GalleryRedirectHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/GalleryRedirectHook.java index 80985fd..fad94b3 100755 --- a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/GalleryRedirectHook.java +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/GalleryRedirectHook.java @@ -84,8 +84,8 @@ private static void handleGalleryRedirect(Context ctx, Intent intent) { String action = intent.getAction(); boolean isGallery = Intent.ACTION_VIEW.equals(action) - || Intent.ACTION_PICK.equals(action) - || action.contains("REVIEW") + || Intent.ACTION_PICK.equals(action) + || action.contains("REVIEW"); if (!isGallery) return; Log.i(TAG, "Redirecting to SecureViewer: Force hijacking intent"); From ea5dadfe993f5075ba8528521bb0f956dc0ba6c8 Mon Sep 17 00:00:00 2001 From: FyukMdaa Date: Fri, 31 Jul 2026 15:26:34 +0900 Subject: [PATCH 15/17] fix session hook --- .../lockscreencamera/hook/CameraActivityLifecycleHook.java | 0 .../droserasprout/lockscreencamera/session/SessionManager.java | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraActivityLifecycleHook.java mode change 100755 => 100644 app/src/main/java/com/github/droserasprout/lockscreencamera/session/SessionManager.java diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraActivityLifecycleHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraActivityLifecycleHook.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/session/SessionManager.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/session/SessionManager.java old mode 100755 new mode 100644 From d537f5abc16ca2da0a33d17408d56775ae2952cb Mon Sep 17 00:00:00 2001 From: FyukMdaa Date: Fri, 31 Jul 2026 15:47:29 +0900 Subject: [PATCH 16/17] fix session hook --- .../hook/CameraActivityLifecycleHook.java | 2 +- .../session/SessionManager.java | 35 ++++++++++--------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraActivityLifecycleHook.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraActivityLifecycleHook.java index aa87000..b1901df 100644 --- a/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraActivityLifecycleHook.java +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/hook/CameraActivityLifecycleHook.java @@ -55,7 +55,7 @@ public static void install(XposedModule module, SharedPreferences prefs) { if ("onDestroy".equals(mName)) { if (SessionManager.isActive) { - SessionManager.end(); + SessionManager.release(); } unregisterReceiver(act); LOCKSCREEN_LAUNCHES.remove(act); diff --git a/app/src/main/java/com/github/droserasprout/lockscreencamera/session/SessionManager.java b/app/src/main/java/com/github/droserasprout/lockscreencamera/session/SessionManager.java index c86be63..7470e67 100644 --- a/app/src/main/java/com/github/droserasprout/lockscreencamera/session/SessionManager.java +++ b/app/src/main/java/com/github/droserasprout/lockscreencamera/session/SessionManager.java @@ -2,42 +2,45 @@ import android.net.Uri; import android.util.Log; - import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; -/** - * ロック画面から起動した「セキュアカメラセッション」中に撮影された写真の URI を追跡する。 - * カメラアプリのプロセス内でのみ動作する(アプリプロセスごとに独立したインスタンス)。 - */ public final class SessionManager { - private static final String TAG = "LockscreenCamera.Session"; - public static volatile boolean isActive = false; public static final List SESSION_URIS = new CopyOnWriteArrayList<>(); - + private static int refCount = 0; private SessionManager() {} public static void start() { + if (!isActive) { SESSION_URIS.clear(); } isActive = true; - SESSION_URIS.clear(); - Log.i(TAG, "Secure Camera Session Started"); + refCount++; + Log.i(TAG, "Secure Camera Session Started (refCount=" + refCount + ")"); + } + + public static void release() { + if (refCount > 0) refCount--; + if (refCount <= 0) { + refCount = 0; + isActive = false; + SESSION_URIS.clear(); + Log.i(TAG, "Secure Camera Session Cleared"); + } else { + Log.d(TAG, "Session release (refCount=" + refCount + "), stays active"); + } } public static void end() { + refCount = 0; isActive = false; SESSION_URIS.clear(); - Log.i(TAG, "Secure Camera Session Cleared"); + Log.i(TAG, "Secure Camera Session Cleared (forced)"); } public static void add(Uri uri) { if (!isActive || uri == null) return; - if (SESSION_URIS.contains(uri)) { - Log.d(TAG, "Duplicate URI skipped: " + uri); - return; - } + if (SESSION_URIS.contains(uri)) return; SESSION_URIS.add(uri); - Log.d(TAG, "Added to Session: " + uri + " | Total: " + SESSION_URIS.size()); } } From 135237cd9b44220b1fbe6e2afe5ebbf43ce91a10 Mon Sep 17 00:00:00 2001 From: FyukMdaa Date: Fri, 31 Jul 2026 16:12:28 +0900 Subject: [PATCH 17/17] add icon --- app/src/main/AndroidManifest.xml | 12 +-- app/src/main/ic_launcher-playstore.png | Bin 0 -> 25150 bytes .../res/drawable/ic_launcher_background.xml | 74 ++++++++++++++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 ++ .../mipmap-anydpi-v26/ic_launcher_round.xml | 6 ++ app/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1650 bytes .../mipmap-hdpi/ic_launcher_foreground.webp | Bin 0 -> 1802 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 3064 bytes app/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 1112 bytes .../mipmap-mdpi/ic_launcher_foreground.webp | Bin 0 -> 1220 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 2034 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 2094 bytes .../mipmap-xhdpi/ic_launcher_foreground.webp | Bin 0 -> 2316 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 4106 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 3014 bytes .../mipmap-xxhdpi/ic_launcher_foreground.webp | Bin 0 -> 3566 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 6372 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 4160 bytes .../ic_launcher_foreground.webp | Bin 0 -> 4636 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 9030 bytes 20 files changed, 93 insertions(+), 5 deletions(-) create mode 100644 app/src/main/ic_launcher-playstore.png create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3e3b517..e97e2af 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,11 +7,13 @@ tools:ignore="QueryAllPackagesPermission" /> + android:allowBackup="true" + android:icon="@mipmap/ic_launcher" + android:roundIcon="@mipmap/ic_launcher_round" + android:label="@string/app_name" + android:description="@string/xposed_description" + android:supportsRtl="true" + tools:ignore="AllowBackup"> -64$GihXw)a{i4IlY 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..ca3826a --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..82cd4a2 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..82cd4a2 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..e9c4ed195a11e6720ffeb6ad0b1bb526259c8e96 GIT binary patch literal 1650 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..cc464cf4b097c729bc745dae052e1da5d724ac47 GIT binary patch literal 3064 zcmV zhvt^7(bh4bBjMi>;O=g-VQo=YN1{t|t0Z?h0e9D|Z65&+O(S`q0dfGOui@Tz+Kujz z$Pgt-GHultKHIi!+qP}nxb}UvZ5s^RMp7Ki@iYH1u!(Iu%1j(2Jry<8W`jXRYE*zI zuo4X{<#r|j>`j$CRRzoQ5Q=%KrWyroR0M?&6^3AdHPurm>P-W8`hKIk!P*cEM^gj< zppG=3?x8p7Z`wkKEi;ydC6>1g-~Q`|boU9D0Dz!4lE4E{SGu0QpaT{}Gd7Fj1@!q% z#{lqxqbLRdO{Z7rs3@XYHVNG-Gq zO?M)SS3U(G*v~M4`qPK1h`NM~B^^!u1ZC&)M+%~2uq!wq#Y*?i`6Ns0q#s?W%vUSL=W5U|dIj2xD)hzj^DCh-}c zye0Ft1E7|2hK(RtDc+>dM4f}HLyBQM!V&St%BhOvBmo^Gzac6G0C<~=<6vlx`0RKO1{7RTW10aCUZc%4; zi!=qa34aiMuY~Ozfy)yg@ybWA@RQGiH=T%6E%rr9oLQVP;}Nd_prj7Ci4G9+hY_^1 zsY)IOL6O3;66+0OodHp5&}q38#<77*_aX9HON{QC^L}atc%G<}{Ip-(r{p!e(ho?G zv(%)s%z1?!TFk$|O^ouF=y;xoDh$b*Q0DNUHK+|bf-6D*yI(1g90pVg|LAX#^^uu& zm{@I>%x!2F_bzae)mSaXTLW*vz%Hf%)NI?i19-O}CHpAhWg@n1kk{4CGk?(!3KyTr z9z(503o*^2*zQkBZb;0 z*NZZGyoY3oUT2o_^OMf}3C)9uB+u*$V@v#)OIhzPYi;rMsJPe06fnVbHFduS!4i@Etn4o zxn86(;ygsTdE-91J0R*>iMmlfiFuUXq(S_b5Ou44^2y4f`<7|4C($2pZjZ?wK^N!# zPi%M|6k?2fn~l&3OaTO;?2$F2O@R{K&c5!({0XtI(#=3*{)OmUCH&NgdlP^jd7EyeLx=bu z0pN3+l1Cq8pXR-|qF5Dv5YLOcRRGYX`qM{!)(8MTbwj>MdQr1!c)(?#@&^luE2e{WXQ8<^dpr*v5H@x=p@d9Pk)@iozF%-L%~b zO#SF2<_9s4ibeKEqSuLa2Zx5sat_XKYRL+gmcJU3BiTx@&Dui+z@KeKV99H_n}> z*f(jV@KeJmBDi=X=kC*+G}CwwfPr64NGG-XMFa<}>WN#_GBc~Xob0~&bEtWLm- zH61Jcz-UuMU9V&{plswmx&!zUr};_T7l05Wh;=V@Nf9L`YS+F4{zr-Xl$^UxaL+^4GEes5RciOyWC9Rd6`)=m!EP*0_I=vl`O7T zSTtyF|&&8;8bA3Vivh2Yf?A7D095`wYBw*b%?hHkT_`&G6FP#D<{@nEh26 za_}&PUotnR)P+JeE#@1lrC+^c6qui=5Al5^%hZ+Fhc&jLqKQ(eR;mkz^}v-?|1rsx z;xWE?B{gmCjqcn?VmQ2~~-yVDfR9Btv1lJIdm#PlwEG56Gt9B&*4b=4A`b_U~ zZ}Kyo`%MSF0DzRhrHR+{Y73X{gWK@IN3;k#vX~zLz&*X-JJ}RUOPG#WNCZL2ck1dL zh;O9iNj~_Znx#F^&im~7>cMlsmK%m&*EvhczqcA&;z^?VI~`^G40YE%?6J?Dub%7- z$3(D-p!sdr_my*|w`^14?j)qj(s5p;3Rj&nE7_^KQjPVxin4{WF69L~6HR%J#D_$- zxpva;apfIUDm&#pxa=Y8+o)&UcXONTGp)K;Rb26^_>#wV3JZGh!x65@(Y6!L7)KzH8*4C)6!m!JQ`|O?C G3w8#G&A5pG literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..56d3f8db82b921b52ff26df11928cdd44375fe97 GIT binary patch literal 1112 zcmV-e1gHB_Nk&Fc1ONb6MM6+kP&iCO1ONapFTe{BwaCH#|BR&Usk$rK8U0nRY}>Z2 ziluwo_Kjz4+qMyGo@er(O#XRp`~P}>r#1fzNn!vHV3QZtwr!nk+xBeRwr$&{14)w8 zHrEi?jljkVeA-f&9(qKq7JY*6{gfLt9)JmO3S za!`O**o{Lt4C`8V;4z48_c*uZ3pT(7DM*8Lsqn^nyal$?FazZC#bFcx*EvMP1{Md9 z16=130~k3-C};IA+5htxi}552)xVPJ;qmz-_4LE-nH9uHzOY z$37I|Z@k3qYeaAUi`&&mzw(m*wj}&RErlPV@D-g`1qV$?q6x?-)dnRF*_V*%LH_H4 z@E;6i_Sk`?xP$igwb%gNe==^pI29bIb@myTlye{a|3JB_F<^i;{`!j{;YKOd1ZJ9` ztg^C>QmT(il1_g8QEnJe1K*VHXP%@zXlxX}|0n)HBYuA)Y_X#aPAH|O#9Q|L3YG!b zi9zV~FW56Rklw#*`L5_|1OVKxz`J=tE%~%BxZeSeLC|0vhQ2I4Nxf);Gw+r~*ot0# z`jr=uYN@aH>zV1o5<~PO>rM^p%ue*j)MFo-fW*CWllQWecJWR44{$$&)QfL1JN}Z- zd=!0;0D%94f6qh{kVS25DK;Dy{ZO-csYe5cM!C;QvAY|8*zbXV-$Wnr(1v7XUX825 zkC6a?h}9kH*E3V92p2FPQddVuG;nwX0C9U8WdTEzw99Xy^S!e0l^u(SH67|%x5E4% z1y2nS{{0hk@+J30ML^+)$f@AuUb#Vdw=6KlZ2Ln5PYn#wk52wC*$*q`l$&?!0ueGh zz?6CCMIZi zR@+hpS6&&C*6SuicoJtWe5A0+V4u#8U{yv^**Neyfr~1cg}I4gIGxL_OxH8TtZ^Dw ef0|?${^i6N(hNE5%B#DGY(1_-vFjlvBmw}L5+0NQ literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..0803ecedd1d921aeddb037b4422c6811ba8cfe41 GIT binary patch literal 1220 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..ad6a351e2cd53875cc5845ba8ae93a9f0dfe4d5e GIT binary patch literal 2094 zcmV+}2+{XaNk&E{2mkgDWg_5813+N8U)i>8d+d4v<7v9a_U5K~8e5ZX^P2!V+W#+-ly!Hv z3v8(AJ{!8yK6JLu6UUlOd_iQrHjA916LC zyAFVE)tjmXW%lthEMQuJ*(eb zKjnk#i^xSJ+X$?Kdk`NF4v+BnS~ZYu2riNqY5e^oyf5w#?-zpuzC6zGiB?Kz5Yt@tWul%I5!BI2Ye*+H7MD({#KXn5wv5my3o%9BvoKu@ zs)lX(DHl)&RG3kykP_SCm{}eb>Pt_8Vp|N_H6@&OjPZwcx-}ylu}R%oNIof{xT+)= ztHd`0)KNvD5X^5 zkwRhM$%)6gP=TeqW5Yx&nWj=@f|}FdM15E?MYWd%wbaM3x;hCij9#j7kTS+_+@{*Cde#pCgK%nZ^1{9kpZMCkyqE%zWL^x6~|_O6ODkzw0bX!vJ}8QilWRcjXD$qQITf*63&0Hy(2MC zwZ4nT9KYb=-kVw%^k@b`EuS#s0OkNFin6@<170kcI@R3K`=zp7`TU{a3nf zRYuL73XuQ6-lbbsX3yy&r6K2fD>Au|^-UBlpN>ByqS`0u*m zb6t|s&@HnqVYzW{DT#pgPDYE%>Utj0<2DfP==LAv(K6ed%;?0VqLbnV8&*eHJTEN*q*mse{x5Of!oL7f#~PV>q+TiFNdOJXF4Kf_3A0 z7tNQq^2RQeaot6@^=Yvvcldtqy~4$lMb>wb(cMKR;-TZ_TiP@P1 zJ21@gVowKxEFw}>;?C-MntZu)H_@nM)gJPyr6|_kOO=CL{V}`e@nJ1JerGg063&}i z7xr#4v&dikpDC>`XBzN~=;&tq6mnYE6dHOVKD9JL`cU&%eWgf7am_r7i0Yn2Y+`+0 zvb!pd49O`!OSME=-iEfcdVC(??w6<}H~Nh>N_}shH9b&H0l5FlS2j7- zma-Ti{q?x5!)$k&9?L%L_25rV1gPGkKtGAZCHoNXzBIrt07Tx36LU;be!4Nj)5G0w zRJbK%<%||rx?$}Jp%+E6I_@1# z>f8rzX9k1Q^=G!*;^Y3};_D;j_ci8EPZnZAqDDY+*?9^7oBNLk&i}_PWm@R*m@oqg zRy%4Qi$CW5yutZn{Tl;57MDIX)&mm#L@@&a>j@y=`*_*Y=O~21BQ2CXb3gsNg~WTz z2negl02PJ?Q!SQ0s{UB}t@i6BR{wgb#?!~imkbQOq%89vg76-QR+;w_Xp+-!b=c8_ zgK?Jkz9MJfzn9jHzK4QGw$YrF(o22LJ#7 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..f28f1537ef9d55ee553a958ad1ea182910a65a59 GIT binary patch literal 2316 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..e350dec38acdb2cd75d7c4a258e07c9dc4039a60 GIT binary patch literal 4106 zcmV+l5cTg;Nk&Ej5C8yIMM6+kP&iEW4*&o!U%(d-6$gX1jU>hVVefxOAR;E9pTfd) z;uB4p8Y{H6M*qJdY0iDKv2EM7ZQHi(tZm!2z5Cd1;d8JlnQyEzh}wZ+`QgbLJ=5UZ+f*>@v3b#db2=)HY`9q;%5Sw%s{zP_s&xoC=~O z+or4ag|TfX&$eybwr$(C&DajxNK)i%&*H(~FEg=;BwLDuZ{WK)^BguizgTH$q6LDTMevzP#cppOsyCKCbZeun9ND8}M7(yH;{YQZVTE zOTwdHb_%kkUxTmS+IxQa*Tcbh6u#%3@%y8a%ry;1{eJJn7ygHX^QU8k0B{JNi@(@H zlBNG@Ig(UKn2Qaz!65)4gY`iGunmvHUv1$Jh9xKs2mM@pdc&OnLQH4p1HditBU=Rh zoQeJ*7azeb0p?>j8Mgp?;7ztDiE?H}OSo;m0<1WzqXFRRTiQIxnd+H*iVk40+J`jU z7A0ZM*l>vuc_WY$gqRd7XH8?uF|SkxQp-}6WXL-%iHqE|WmF7eKImZ@=I}8&pjhib zPJDeAN9m)(f9KD2I|xAzz)L7zRRzW&qB`5jhCQ<;l5_KSxB) zxej&+QI_9d_RLF;DNR5UoWeZ5baq~Hd>TNp%1!3kGFGavz?$yRp*gBlol;I`7cRts z*+8e+McR=u)D*{w14yec#lfYKW!l1dLk`a|=m^CT-IAWP&Ve|2n>WlpmN<^w9!~FN zIV@k|6yX+M>@teDnGfNP}U(z+hu&O?yy;eIX(m$?X>0n>9B zsNgP#NP1D#muLru;kY*1g`5pDKnV|)cG-p$hC$NiG`D%!HwfDqG;2rDSdeIOKM*m_ z63VHBM>DyCrHPX>#*T4cfx8{qw4CBtnr(8h`?GKf-yvcrd?^V+*zL1ci{eq zw_N0eG`8sy=>8}ozC=Xy{4`S})KshIvxeJIPDBpvn4E#R9-~u>beO}j_(h`$s_8hC zF-o^YjEF>rW10Qo@qk>4MldJFJKyc!4so@7Y6H)6j)Z4F_$4K$;L==~vvN50&k5+K zGsY#`sZ?vFk~R#a$XX`~IROtJo+S>+Lp!4L6z1hveD}2cfH6Gy#pYX)(lC_cMxN43 z5h0-|N1|k8xXhXyo&7NptcloNUyl|DQz{iN*vKiNiDyj0gzS$~(E-J~W&eaPNIDv< zFV%|eokkkP)@H_hPfNnJ2wx86d)%Cae{dVd@aXFrtuQUc_{i$q3xr&W@UOUnYJ^Wn zD_9fZZLjQ055JRd-84e_!kJU+1NFTrnOcn?v~(f#Wr ziOy@~bZ%(#C|d@1k>!rG^mZdABMI?utm4VY+6?r~NMFpn#i8vYqKl+*uVCTj{o>?S z>=fo{>xAT)#K9se$qad-=25b=8VhS19d*$YHyz;+OGiBPl*7bc;xdDhty{oC709XY$__886n2Mam-lxpFVls%-tvex{r16Q320+v=F1IGnXKZW z{7{n!5xdL#L1JZ?b49tnkWhuaK|m7+nWn;9ow6xJb7;EiTSLAnR1qcxqQh*oesD`yA90lEQ5K6&roTEvAA64<<0>j9=f_oufR!UNF9 zukjff$S&1Ch3sbk3J?9Okg?lJDh{6x5CR~tcnd2tOJahEHra3i<6Ok|&q4PiU?JXb z&iBvoE`US8&cx>h8lssymF-#frT{6;1^C3zl^ z4p#A0x_eQ@RGup*6GDS4V6Bc{u9){qS!EzKa!b8HKz;Uw1``i{Xqc#%dW7`U!=diX zf(41rbiWA_*c5{p-rySYS>vhTbwn+)wOcH-(FfrW8~|0P?)a0y^(35(*f+V_@&0mb zx8kjbT{)2AMIdCL2)D>$p7kqyA7sp`{nOm-1;OXKOj^(wRYN)q39<)2h+2GMMEUyS z^&4rls|VgqU`)JC{9Tv87=8XUo=(p1kn_2tGoSN_3S?&W|49&=9g)29LSX`&lSIX5 zh~Iw*W0g+gp60Jkp8UD~RUr42TIAk#BQK<+VBrO$M#dM2M&|Yhqm)$UAin!A%qR41 zz5C|#?m`}K0tNkRkahZYxM9b(hL`JXPy?sW#_(!1h zV){o^pQuN}RO}rYo59pPNKYbupWbS?UM0>>d`7Nx@8-~V8rG%MSPbOJlIU{HLC@AwKWaHIeaISXgD|Oo(`V)vW@vq327W zM<)aC>GGGltX2(YW#nCJQE8O!d+UqegRos9q6-a0GI5uF3?`}6p+;|94OkY~?zgf7 zYTg*x&Xb+|PDH5SJ>(z6#1fAIa=!}k`GJwO(VufpL)VXY13<;O_T+kxbNR`I&Xa&| z0-&#O0UfB~9NUC^%TLcHO833HVDa@m#-t~pH|89qX!Q}B8QG-6879^e8dPQW8m?D~ zK`Pn39g5LONx(v={MXsNJYTL?1?t{P<5q99m>%vgT*;X zLQ$X{T5+b)9xV*Gx&i6fH-QGu=}gFH>|SZuQTgu%q))wV^~5bQCUd<=yc3vwsXil@ zGx|AHPudcTy(g%)e88N*Tn~t21Hl0;k!nA76jFWr!;7il-H_4#_q=9TcJT#4YQ2@( zV98cWl@%UQ^wklLkbp+8PB{ylyqi!4DS@fCFLSef=^Y@`Jct?jptvE99Fn6sYRFXK zw=g+FpOZCt=&46kQdWH@sX{!a3NZT#6#wtPnm}scu(k#JOFY}@C63`3SDv3H0)Ukt zl=l;OtOe}-r7U<@TL6f0Hb0^p3fxu7>CPOOLyA*~t3v+2 z!1RDA0Yd=>v5~3Hw2<8BHUnjY*D-Ua@US9x{Nwo+7&T-6D!^L6(Hv->{Nd&fP{bO4 zLqRC%GDnZffuQ|ggY%G@CngP4+Eu!Qoxm(7Q0nvkhSZ)y z6R$js2MR_BGi<3ydCEu>|I2*FXa%g~mhwZbbmvmKf2+Juyg%F%Oe9^B>{X`PjuP*F z|5thnWgNuqc}3$-Yx_T!;;7D9DQYd&TmBz?R?+irqbyX(eY@;F2*q5hOYO%$u!Bq^ zc*bi#sP^Pa-M{tD)x22-u}8}R0#Ve#a%bMvi7(#NlG7HXnc;naJasuZJz_MkpZx#d`V5Et32lUum2#VfQ z=7jSazJ0d9vgCL;sPumC@ZW2dfEeIj5Ewgc@VX5)Hi4ba8vPFB-wVYZw_@^@4c~mV z^tHQxKTCE)@T}kOel1uA_#9Tu4OJ>@85Qd`Do)yDg9+kX&>*z)M*pffLzNNNp-?Z2ti^U=l&pLncO>NuOL*1zN^v?h3FQaIkJAAlca|iN46R8U}=-caC z{Usc{=(>ZZT37kfwq3=uKbnIsY?(s;VkYnDE?T^=a%r>YF7hoD)2r_W1#8ImnLiz* zUv1yMyANIm#rBubU)ss)uGN3+=`(CDbF#*jh7X&*YW1$&$CobE?A0G$Q1;}Eo5q=b z?S3b#TjE=9LlKYYyV{EhdW;W;w8*&36ZRYO0TlVRE&EICFXapS%|A-niRw;U|Bb8u zcE5RFSnF+DqS$3Vu-Oy#;r6G+0uJP}j=A|_1-rMs{F?^|d;|*VwGN70=JAhz{4(qG z`t2k7e)HvP6fCy6sp$DY>Xn}>_{_rla^2wN9ybUq{F#E!y;3#jBPqu;kutEk$3Va| I9d=+qP}nwpk`)*fx^l@a{1_9RAUM{~frI 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..00da4f2f997ef8e36eb87a2424228c9d7795ba92 GIT binary patch literal 6372 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 Kc{u{&DY^(X9lRa@ literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..743f5a9bbc469933f3c5713e2840cfa79db0528f GIT binary patch literal 4636 zcmV+%665VsNk&E#5&!^KMM6+kP&iBo5&!@%ufb~oRSV-bk`%e~4|^F7o)Iwt{@To~ zWQ9oynQg_P%1LU_r#nl%Q7RSythyB3-QC^Y-QC^Y-QC^Y-QC^Y-QC??#%KQT%76dw zo^vmDS;C9cu;#FZ#|B=wMlg6CG1}T$&>$+uy>|&{)S7Vb7l=_ z^yH8{eX|0n?i&~q?e%G|s*!rQ>Gg<%2uHZZSGMElzS&7=2;HxnJ!07no2g3Wy}b}}aQ#Lad)xj)cq_Ybx) z*<{!@lH#17nVIhUKLJ*>|DWx)SyHl;>>w*jwo=-sNO#bDEo&a`y_)BFp6|Wxpz}ZH z{&enhenx%F0*xGGSfkU?(8#QF%d&(ZBQ2pwf<|G6W?sEreU3TV3Y&PiUiGyYGf$Z9Ym}nH4Ezs!VJxXyn|m*V1hbh6A1zU zkZp7iyri~m+jeK$wr$(CZPjHUNmA1|M-<6HGH3rMz&V2d1^)~F7yK{yU+}-+f5HEP z{{{aG{_j7n6C49&r*KrgQXj>UF-a01@dD+9NSWNJjJ9wF$J8%%k_k~08IVr^(@x-! zjA|k=5F18Ao^%OU)HZ2>*fJ){#hwcit0Y5<&a6k5i5;=&p92LJlP#*Ib$UX?khSQXMktXqh!PSgHd=$*5sL&7%jr=MeS={wc1*FnLWJU? zLpYsUk%?aNB4VibNFf*ypycTb4yRLM0f7xnEim+3CUFSRNZgHRg1|9TFPIH`CQ}Fu znuD`x7#S2j&4Ag_B3nSnzn-Jf`$xdr8UV9pKV%3zV- z^N1jhn9)~h6_MYVxj+mhs*#W!zghDay<@DEXib)>`HHFIL89Ac$U*oBeFfM`YV`Xa z|Daz*Qtw3_z&jL%MAjyD(j^Zb$d?>R0xcQoqM3Py9VC(gP20s912?K0lSmXSjNptWC|ND~&y?%nJiq&FAAXNj8@Tx>Oq&22-#BB*;Wh35TF)hI! zdQ7W_aY53nyunBkI8CfE%?&XX$lMK2;MM_Tw?C%606y}x8nA1S&0@TOer?bVSe>0A1EkYG8E81H z-p4@k!wa|ueWj+UK>_XgfKmWWqNbJrd9vaI?gXWen(hE;HySXXWl8di%R@T8Si`ZZ zn0X6Yy(d4X2WqmVB`AkWG~@X*j=N$xJC3*F1` z*OQ+bPvx=%LA>^N=BbM>qHpeoGC@AdMvOlKNY`{JvoebviQHYIC?~HzbbL^RYrYa0 z!7{K!se4#2lT5wk zhND>fJ4^Rh$@*zWV185ZM*+R8ss+89^>usmjXl{irp?liHn>TOX77h{i${BfVPK6X?GJ`c{RxHt0WoS$ik? zMu0yYSvq8~C+U@@;lR(By}7lq^??VXgfzE&VcKvccY@)bmRpWH;oW9+CWFG%X!>co z6cp^TF*5~)6I8<=1=*gi0e zKgOHT?vq+_A7jG)7(k$wYd!^qlg!;jn_Pz%g|Ssjnqa!~GZ?O%d$A_6)2xB;&zL2O z8At+^6C7gcu@_Qun7W%LLj1v7^>VzkllG13@@O?DW4EIAxtPle33O6`oS!b&oHRg zd%g%}{V-O9f?Fc2rI5?WqCl002(~*XgOQc{zQX>1i27i<2q6wV-gg@1lIP% zwm{)zbF+{_cPP=9I`YXawF+Z567-c|Y!K|nim>Pi(3p`zp^Vy-4jIGPj~uN(P|a7O z<%X*cgY*5tsOq=WZL5+&`?LdtTJXoPLTBSqr4<}>&>l=1ElZmuPoWt)D1NrOWd%ba8B7(7o~~vDMPv+h>@(^ zqKb2>6Q+cFm##p?24Lxsr4VQyRu#uvn3BRmt5D5MFUyi76j0|Mjb$!1v;BxtXy-;{ zd>w_x$E6Cs8Bc=zj%v-mRI)1)g-FvSo@&xOA68@q#@Go%kc;9t-<^UD#HLHB)Sq3J z&Zc?4%GtLsSXSo`t=Z5Eo`Rg>Y-ba7{n5#s%J@X%i5!4Mh3#HVH0_)~iZIw7i5(*GjmpE4kREbZI7(5%`epsI}}-TJk|Ye_Wc5+rYk8`$d-G76uMCG z|7hS)$Zue!@?q-QABB7P6=nH^2}1p$nzX}_)z4&?r?wXk#}}qf>{Y>K;|43JpHp$g zO~sv;6?Sshsjg46yV88$7;D>;TOAKVF=Lopjkh>qNzk8B{gFT=yEG}BQ%KZyjm)Ah z=RjT7K9;>d1cV+_lVt2z68TB4(6u<$RaREfrt_OV&q1}rFuys3VZ?EknrO-nBS~VL z4QNa}@bhFKg1W^^xDh!JqU; zp&Rg8B*|@PrjPm>&t%!Cx#3>Y@^^89#SqW*b%Xs#QcUxq2F-`hGR%Tz-wld7oiVcr z8^)Dec`Mm^a?XS$DS2Ta- z&dUzuRkN!7g+UuJXt;4ghFQVC#dReD!@@4{WbrX?aoR_sqlt#Df^*6vv_xGuYS_b? zfy1P+>xl=3)owm4zIc>W2IAl;$Wkndc=iFiWZ1CY+izxYt9XhVjs}K(L~P=5QXTdb zW-%_GGQgfQZm{pLBM_D^1T$j!O@TCgHqp?<&I8+yBb~o=FRYzX=)jk>pynGfFXY7C zVOSr;ku^QAF0k-M(soElu7&j|j(lkb)=8ll`VJ&OqsB8iE5fBC7S{7PVjYJSFI+h>PZa;u}z?XU{l zJ(9R{GN>bVI0`kMNX_yagHW=y!K#=sNOBz1m+lm*KTw+G)dryyM}yUHtdOL9+<>A; z{1G~&Sypxt%19}oCfZ*ak~E%5Set}$Dxy|aSjbbDK}C^|i6s6IOn@2@a>=HpQR^eO z!%(QW+k#3mG(r;VAgG+`C?wZvbdiPYwiPH(QN|KF4CiA^SLUO+rR#r}7;Y58WP@`z_o(SNFL^la>B=HVvaH9=Cv7&AVYIh(;(8M2s zcY-RPFk&3T+=HsVXNMyTFP;XK>ih^z=oF}d-C`VxKNYj4Pv}TaRkVS~;%_8S#k~$u zOi)zN>Q(UUGk*#sLBSt`@<|gfn5?pQC8!ASzk(=N#z5_hp>Gw;Ll0$Fq(A;?W!;V{ z>9e~YX4P+8mL#Co6QcCFq!ScXOubS~my)#{KSrRk%M+&1MOfi9Pt`NI-$a!` z35q(t*aaXN!4ih`nPF6+ufw9o6Y0cW5MN(Iu=WtH6!$w|RJ-v^19+_1|T%rn&$gt5atV7d>{@4 z?ZUI@V%;R*Da=5~I{HtohR*(sfcC|*mOx>fCv>5j4K0->p%Bd#w)@X(A-DfFkN|=@ zFr&2XUkF1Rlw6_-hiGBs?j9BE+`0LTMR41mnGHmx{Kh~SbIXvz-`id}AR_wLExA>3 z^8Zo994i|H6+UwWX=s-M7c~3OU?%E2D^^Uugf$<#tT2BjB;+>* zR!lN?6K$w;*edXfrV9}QN{4K(!r~Re8*{Y+Z>wI_dN-Wc5PDg0Dd^P40`v6YqpeTe7ouiZe^2aaz7oYfZo?QWuNoxW>6;W_=ArRF z#L%|>MpO2*WKmhtZwYzmj>m*T;b$%Mp~e$=XGn<`tsR6vR61yfLddTd$x!_OD=ViW zhK{Kh$XI!X6$Rpu?yI3<=l~h4!=Od~SREqr>Vjpol179U?b{FvWOjM7$UrZz5;bn% zMV;R?K|Vr&wI7o6RA2RZz2TYc*_(3$^&Nrqg19{`z84LR&*oP#0rP~(Xt zquwYo0L92Uki+=M)|>ZhATpI*k&cjI7c&Ed%xgo09DcUj&Bix0w$GB0A2=pJ$adkY zAcwJ|=cKNQsewpzx2M%Y|*@yGl!H-`>|a2NJW()4gQ$MPnj_ z{I2yswDeDUejFhb`M)Igic(wl_cdyI%dLcf`ure)kVB4`P2D|4uz9yV>fn#JWJH+c zZeBv?)?g@Tj1zagCFvO9pc7V+NvLY=%#VSpp(h!U->=oXbioZZLB{iEQJ1@y&`>lp zBnSJqzn;h!qPLCo0$Y`tQR;S8AP$6zr>6sJIFwE2KSEtTT-@hI>~I8g`KSBW5IP$D zwk9jwcJ2sR^k3&5S^s3v<`WbnwCBk2b!@-4G;Q9d)ldv; z-~E%?^{3;T5o*3OpJ_%Mzu$~dUc~RYW*$ULd&fJi+g4@cHoqyj^YZybU4JTuxi3aj z&*^u^(Dy%Gl9^=c>A~tPPK6!5V#d5@j*!&I9?BwSrWHuS@CmbzUg>|q|APMo{|o*X S{4e-l@W0@H!T*B)`^OBYR?C(E literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..e74853e2315437c1ed8209d48def6fc537ad5677 GIT binary patch literal 9030 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Ð91)k3O+qLa6^&@{#|*|;9Eyv0 zQm8Ib_Dc^8J_?yZSY?k zZb_MXqcsT#7Np~3#1V5Sc(M`j=TMKR!)WkKn@w?|@i#pxiorJr=7Pt8UH* zsu6K>ewvR-glzxti8&SSjrc(wA8O2n^P5R=c;s|hl{pj@Y)4E52A8otf{Xcn;NWAI z&c)wDq9A6^aAgO<&-auHX@|UDz^!X+3f&L)$OPf9~@eX>`=~awx?S(}5wdCehfDIJh``Jb-=st!JVP zU@hA!975CplUjVD?vJ%12;rwYqPyChawzHo<02h-F3B+3v}F7s{LS+(Fr?xZ(~=pd z{wxA)&aEQMT#`G;P8BA)>oku>tC^(=kiXxSw=tw-$~~l$}(V5US^6pIrdM$8@%01QSWnnNeDQwnWJ zXEm9yzX}3^NvmfxIOZam1|l$TIWJu&WLWmD5E24l>=-^kav0nj@*UlfD$KTlWp&Wr zcn6jb5oMu+&znWuXX=aYx*k}azXn_*a&7Y6XZ%V(H%)g{40mn!ul&MBpQH=Fzl<0H zs^VO1W`GK?cz)vu0Z~5*|AAKuXa3Xa`|h<_-}%ZN#h|eq{U_ z@(ppIsizN9rNPW$aTqxTnf<3W<}i60YMxsn38=}CsJ~`u{_Kyi@XZAf(l7jMfc4V6 zM;s_J_~rflF^F(bp!feQRR&Ea1U0_%;_%9@kdlv^luUqqcHiiS5Tgs)Dn{CJ?8gcu za8bk;Ri$7^>O>*O?ViPipsFL0XGZfaDjtS5Eop@Laa4ko++`dtYTqvi>qUHUXBg%e zVoW>R<^S_lf*#)Z@ZWPtV**+uNU(7)<~2kj#?sP z7*Ee2A!89y%Rg-5yyx#M zB4X;%h&a#`<6oLFL`(-^)LD$yj(bhWOmW*M24h#Er)ew|wVUlw~dqSeYt_5GJ zw}v^ut^`XO17xR`SvNzFaC`{2(2J4tH@U5}Ye-Kl6Kzcd8Z6T?A9no$;7n`@+$t7o z{I$TQ{bCfn3uH@CqrV}w{6kA11esr2bWQZoH**k)+*g4M(9@_OC=gs+nVNB$jvw14 zIJh5-aa{o~0ivy76*&i|8UrP@{Olqg2&@SZUsJ-z!U_Z|5j35gz*Pxe$Fay}0i2k+cKz2%5!6(09d`rY+C>F9<^wL#PggGL@M}r2)=AI8yJOj6p}He4n5^)&xZ;26)AEi>a41u(G5O zwmGA)aA}C%KqZfOXH^XBk>m?^aVfpDB>mOC$uI=gycEkNof9$$>v9TWC-IU7R+gk1+FY3 zN`qp5c0br8pOL9G>1FLyvh4kW?eQ*y${fOXJu}-M#p3Iop1OSU%KYZ2U{GRKt|oFznF>)E<<|~bd~;tk?{oM@ILnHx09VGAoFmeIF&a~ENF|mS zdK#O+ZcM{)vZi2#M;K1#u-MynP6kxvzfc_4w6q+MkvZ7vH4UCHKnO7DA18o&363oZ zc#sdL5P73%Z4dD~$F59Dt?*K*Z7S)nmOM_z219c(t3RAGbMc+MRhIOEbxx1?&reZZ za78I~uoVJpvY^X2?ZHY);_2~+%^rmGMw#qb?pBz}9#G`;jEw|W_ugWW`L2<5My6Ln zn`);JmTlH9l3^Q;pOqZW?Q9+*XKx*e1Rnv;bt&?D%}b#@K8v!}@RbqOF|o(m1I}Sn za{Zb&lv2o>35SOuM=;)9vFm5nkIr?d^_lnQ!=e^%uZ$`%Z~sYr#vnfb$UF?|9v3Uo zJ;z&3)Zsm02XO5h`P@|02L4j&U~*7nw&nP2lPuR@nR&}M6M1>hvg2mNx19A0Q`aX! zm*^4ua5e6p=n1=3UjI|xS@f{f442;-Ap7o?yC0D0=&Bp%a!HcUYkwufR}_L zZv$`x_?05xiB*u|FbIgh*_Ra#vClBC%wjOaHJmdW%05QP4H(cWP zO2Rz+t_3<$`R^9WGdeJrq&N#Fc?we12bAF7?im>W#QX??Gu}UfSpciPabNBX%zwUH z-*As03WeMYsJH8kS^`Ok18b#RJMvB3YDP3CV%2h?!I+0TLzqR2IXYzx26$zWLZzq@cjzl0q`xxor+*kNFhj6 zA#gI#iL#M;@7z?qR1%bsy>`ZjVMHNn3+KT??QyJWz|ec_qrOZ0G;)C|Bhuur^V z55Vxf@Mi!}5M5N;1A+5PsoUfSq-iXSLz#A@Le%_CYpqBVs(@lB_rvJeX9$cV3Xz+M z2nE>phmYcbjM+L${@5uP7L*56wa)>-1%S?z(4txPSpN=7TXwqKIkKjq{mpYqYGW#) zDkx6F!I-4z7vH;uM&vnl4EgAsCJDgY_+5&PB$D?Oi(~;S3ZW z5Iia57IgT3D++?*wdkcQpfWp)=MNN4-+1|$^(+0=p2h%@iVf!-XdX6^cGNKZ6&Btv^T($w>WPd>35 zXUdh!{#W4d=}J{={>&Jp_f5E~cl-i^!({>|=}(V8umf($$KmcpYk`hZI{m*bg=vsw zhLWm988AGLU5aOpjEpLplrxXGOhgM+56KBsHjNfa%bjo@23!RU)-s zCgmehYwQ-U0{F(BghkWx>jxV{>^@_P4X;{+KgVKu?7G)AY7+%b3Y%qH0rp+s-aGzo z`0t#4laPMVKjA_de1h9*b}%McyI|nPy&?UZyIHf<7XGBgkmLsfT)z+C=Z0)J&X%U9 zGH3#Ar@afItAJ_g8I+4|$N_%ecZtA!3I3_lW0)iW(-?qDPx!hNM`C6PDmJJM5)8C< z6WX2IgQ_DjN8$SG=d z-~T7ax%e9yyG6B{A$1Kd;6GO+~X-QP=n~TM?a-?E$r^Ubws7B<)9vH6J z_=B<{ACMVg67VEx)FL<03b0EqfOl`mV%?A*ky#UvTo!Fb&|D>^(N5K=GAM<1Oi~ZW zTvu#T)8s&FB0xyogJvnU1T)Xp6Mo$wjddN5$q zT?#A!rlxgdULBq}F2%-4?~!~rt#5=Em%SnH;IiyEfVJ@wP5GO>F=A*g6s2Z1==Pm? zK!q7nDjXGR_vN!e6{dD@F72KX*zPV(FJXK@T(&F->q+hCZ1N9trrP8^5NPhX^PmOXc z5(i=LE*R=k#6G`Y*d>f525)D2RF%e8BUy0d*W-W20Y zS$fXD%Aa)Nm+W?f;IEpbIB_6$JFeEMI#>kEN>6dV{J|;Mxod{q+@yAGKst~6Q3^?0+bc25zzA+#%k2o`@$L8}J&P>-uKW){{YwFQslu9usyBCQuDV2{w zs8g#C@B|x_rmF;B9(5+p9F7G85bMI6HqDxzEg(=EN4yiPLaxNunr2MSYTE4ildyN- zoA*o1!I^%g;ki2=sl}F?8H@ecnm~p`?yA-iW&)@7lvXYHyR$oaj(axuBEP3h(EFOayVJ(xMKUSe|xvtz7 zk#BYc7P+%)P|l9X<3~+GZZRspcxryl$lsZ1C`EG_TFTP$*ClC~nZLg|IbVuX-f&^#m08CswJXD)z%(+B-0eo8a zh_p>o*U0!$m+R(N{;!^hd=mQYh#|XX)b1IxH^#k6$bEnKA5#A7$~Skp_}SHy+O%)E zVf-a>jA`1?a9ke1^t$trh7OAZxO^kd`hY;Jloi&r0KOZw?~8!`2uSJy`RWP*TK()v zU-u*JH(Ix8SQb_be1J8M?*w6a9iih><9q`kh{*Phvj)f9EixzhY+3PP0Iv<#ZPu(u z{QcTs^7CufY~2l)3yaS>TXIh1-C}~RalY9RGRz^$ivmNFrrA5i)h2hS(7e2CfujQs)9hlI%p^%(&2et5vJj z1_xs~t2I9~F4H$LTauEiHU;5o1dix1a3x$c&ty`|a&uE6A|yCPM5N^A%G4%P9xltT s5ji3#iaYAGSS%WiCI}~uMq{y9ocyzbvZAZRZ--Y1CLJbxKU&rp03=ZH%>V!Z literal 0 HcmV?d00001