Framework之Launcher小窗开发

思路如下
  1. 使用DisplayManager的createVirtualDisplay接口创建虚拟屏
  2. 将指定应用和Surface都绑定到一个wm上
  3. 设置surface触摸事件,通过displayid控制窗口显示

第一步:我们先找到要修改的Android.bp

bash 复制代码
package {
    default_applicable_licenses: ["Android-Apache-2.0"],
}

android_library {
    name: "CarLauncher-core",
    platform_apis: true,

    srcs: ["src/**/*.java"],

    resource_dirs: ["res"],

    static_libs: [
        "androidx-constraintlayout_constraintlayout-solver",
        "androidx-constraintlayout_constraintlayout",
        "androidx.lifecycle_lifecycle-extensions",
        "car-media-common",
        "car-telephony-common",
        "car-ui-lib",
        "WindowManager-Shell",
    ],

    libs: ["android.car"],

    manifest: "AndroidManifest.xml",
}

android_app {
    name: "CarLauncher",

    resource_dirs: [],

    platform_apis: true,

    required: ["allowed_privapp_com.android.car.carlauncher"],

    certificate: "platform",

    privileged: true,

    overrides: [
        "Launcher2",
        "Launcher3",
        "Launcher3QuickStep",
    ],

    static_libs: ["CarLauncher-core"],

    libs: ["android.car"],

    optimize: {
        enabled: false,
    },

    dex_preopt: {
        enabled: false,
    },

    product_variables: {
        pdk: {
            enabled: false,
        },
    },
}
创建一个服务

是在Launcher的这个AndroidManifest中

bash 复制代码
        <service
            android:name=".virtualdisplay.SmallWindowService"
            android:enabled="true"
            android:exported="false" />

SmallWindowService作为后台服务 可以很好的处理新增新增弹窗的请求

新增一个白名单

修改AppLauncherUtils函数

bash 复制代码
    // 小窗白名单(可从资源文件或系统属性读取)
    public static final Set<String> SMALL_WINDOW_PACKAGES = new HashSet<>(
            Arrays.asList(
                    "com.tencent.mm",              // 微信(如支持)
                    "com.xiaokj.esp32camcar"           
            )
    );

    public static boolean shouldOpenInSmallWindow(String packageName) {
        return SMALL_WINDOW_PACKAGES.contains(packageName);
    }
修改launcher的launchapp函数

关键点在于shouldOpenInSmallWindow判断 是否在白名单,然后执行小窗指令

bash 复制代码
static void launchApp(Context context, Intent intent) {
       。。。
        // ✅ 关键:检查是否应开启小窗
        if (shouldOpenInSmallWindow(pkg)) {
            Log.i(TAG, "Launching " + pkg + " in small window");

            // 获取 className(必须有)
           。。。

            if (cls != null) {
                // 通过全局控制器开启小窗
                SmallWindowGlobalController.getInstance(context)
                        .openSmallWindow(pkg, cls);
                return; // ⚠️ 不再执行 startActivity
            }
        }else{
            Log.i(TAG, "Launching " + pkg + " in full screen");
            // ❌ 非小窗应用:正常启动到当前 Display
           。。。
        }
    }
SmallWindowGlobalController

实现核心如下

bash 复制代码
  public void openSmallWindow(String pkg, String cls) {
        if (mBound && mService != null) {
            boolean ret = mService.startSmallWindow(pkg, cls);
            Log.i("SmallWindowController", "openSmallWindow " + pkg + " cls: " + cls + " in small window"+ ", mBound= " + mBound + " mService= " + (mService != null)+" ret : "+ret);
        }
    }

    public void closeSmallWindow(String pkg) {
        if (mBound && mService != null) {
            mService.stopSmallWindow(pkg);
        }
    }
diff

git show 72e6020c8a8cc997e62f105b7e97df4649cb5b26 > xiaochuang.patch

shell 复制代码
commit 72e6020c8a8cc997e62f105b7e97df4649cb5b26
Author: Qm <383930056@qq.com>
Date:   Thu Mar 5 15:27:21 2026 +0800

    launcher新增小窗开启微信功能

diff --git a/packages/apps/Car/Launcher/AndroidManifest.xml b/packages/apps/Car/Launcher/AndroidManifest.xml
index 8c990c23c4..620b2acf2f 100644
--- a/packages/apps/Car/Launcher/AndroidManifest.xml
+++ b/packages/apps/Car/Launcher/AndroidManifest.xml
@@ -63,7 +63,8 @@
     <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
     <!-- Permission to send notifications -->
     <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
-
+    <!---->
+    <uses-permission android:name="android.permission.CAPTURE_SECURE_VIDEO_OUTPUT" />
     <!-- To connect to media browser services in other apps, media browser clients
     that target Android 11 need to add the following in their manifest -->
     <queries>
@@ -131,5 +132,9 @@
                 <action android:name="android.telecom.InCallService"/>
             </intent-filter>
         </service>
+        <service
+            android:name=".virtualdisplay.SmallWindowService"
+            android:enabled="true"
+            android:exported="false" />
     </application>
 </manifest>
diff --git a/packages/apps/Car/Launcher/src/com/android/car/carlauncher/AppLauncherUtils.java b/packages/apps/Car/Launcher/src/com/android/car/carlauncher/AppLauncherUtils.java
index 0d637bf290..2eafbc0e08 100644
--- a/packages/apps/Car/Launcher/src/com/android/car/carlauncher/AppLauncherUtils.java
+++ b/packages/apps/Car/Launcher/src/com/android/car/carlauncher/AppLauncherUtils.java
@@ -50,6 +50,8 @@ import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.car.carlauncher.virtualdisplay.SmallWindowGlobalController;
+
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
@@ -61,6 +63,7 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -71,7 +74,17 @@ import java.util.function.Predicate;
  */
 public class AppLauncherUtils {
     private static final String TAG = "AppLauncherUtils";
-
+    // 小窗白名单(可从资源文件或系统属性读取)
+    public static final Set<String> SMALL_WINDOW_PACKAGES = new HashSet<>(
+            Arrays.asList(
+                    "com.tencent.mm",              // 微信(如支持)
+                    "com.xiaokj.esp32camcar"           
+            )
+    );
+
+    public static boolean shouldOpenInSmallWindow(String packageName) {
+        return SMALL_WINDOW_PACKAGES.contains(packageName);
+    }
     @Retention(SOURCE)
     @IntDef({APP_TYPE_LAUNCHABLES, APP_TYPE_MEDIA_SERVICES})
     @interface AppTypes {}
@@ -104,9 +117,65 @@ public class AppLauncherUtils {
      * @param app the requesting app's AppMetaData
      */
     static void launchApp(Context context, Intent intent) {
-        ActivityOptions options = ActivityOptions.makeBasic();
-        options.setLaunchDisplayId(context.getDisplayId());
-        context.startActivity(intent, options.toBundle());
+        if (intent == null || intent.getComponent() == null) {
+            // Fallback to normal start
+            context.startActivity(intent);
+            return;
+        }
+
+        String pkg = intent.getComponent().getPackageName();
+
+        // ✅ 关键:检查是否应开启小窗
+        if (shouldOpenInSmallWindow(pkg)) {
+            Log.i(TAG, "Launching " + pkg + " in small window");
+
+            // 获取 className(必须有)
+            String cls = intent.getComponent().getClassName();
+            Log.i(TAG, "Launching className" + cls + " in small window");
+            if (cls == null) {
+                // 尝试从 PackageManager 解析主 Activity
+                try {
+                    Intent resolveIntent = new Intent(Intent.ACTION_MAIN);
+                    resolveIntent.addCategory(Intent.CATEGORY_LAUNCHER);
+                    resolveIntent.setPackage(pkg);
+                    var resolveInfo = context.getPackageManager()
+                            .resolveActivity(resolveIntent, PackageManager.MATCH_DEFAULT_ONLY);
+                    if (resolveInfo != null) {
+                        cls = resolveInfo.activityInfo.name;
+                    }
+                } catch (Exception e) {
+                    Log.w(TAG, "Failed to resolve main activity for " + pkg, e);
+                }
+            }
+
+            if (cls != null) {
+                // 通过全局控制器开启小窗
+                SmallWindowGlobalController.getInstance(context)
+                        .openSmallWindow(pkg, cls);
+                return; // ⚠️ 不再执行 startActivity
+            }
+        }else{
+            Log.i(TAG, "Launching " + pkg + " in full screen");
+            // ❌ 非小窗应用:正常启动到当前 Display
+            ActivityOptions options = ActivityOptions.makeBasic();
+            options.setLaunchDisplayId(context.getDisplayId());
+            context.startActivity(intent, options.toBundle());
+        }
+    }
+
+    public static void launchActivityOnDisplay(Context context, String pkg, String cls, int displayId) {
+        Intent intent = new Intent();
+        intent.setComponent(new ComponentName(pkg, cls));
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
+
+        ActivityOptions options = ActivityOptions.makeBasic()
+                .setLaunchDisplayId(displayId);
+        Log.i(TAG, "launchActivityOnDisplay " + displayId + " pkg: " + pkg);
+        try {
+            context.startActivity(intent, options.toBundle());
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to launch on display " + displayId, e);
+        }
     }
 
     /** Bundles application and services info. */
@@ -272,6 +341,7 @@ public class AppLauncherUtils {
             for (LauncherActivityInfo info : availableActivities) {
                 ComponentName componentName = info.getComponentName();
                 String packageName = componentName.getPackageName();
+                String className = componentName.getClassName();
                 mEnabledPackages.add(packageName);
                 if (shouldAddToLaunchables(componentName, appsToHide, customMediaComponents,
                         appTypes, APP_TYPE_LAUNCHABLES)) {
@@ -289,7 +359,11 @@ public class AppLauncherUtils {
                         componentName,
                         info.getBadgedIcon(0),
                         isDistractionOptimized,
-                        contextArg -> AppLauncherUtils.launchApp(contextArg, intent),
+                        contextArg -> {
+                            Log.d("AppLauncher", "=============APP_TYPE_LAUNCHABLES============= " + appTypes);
+                            Log.d("AppLauncher", "packageName : " + packageName + " className: " + className);
+                            AppLauncherUtils.launchApp(contextArg, intent);
+                        },
                         /* alternateLaunchCallback */ null);
                     launchablesMap.put(componentName, appMetaData);
                 }
diff --git a/packages/apps/Car/Launcher/src/com/android/car/carlauncher/virtualdisplay/SmallWindowController.java b/packages/apps/Car/Launcher/src/com/android/car/carlauncher/virtualdisplay/SmallWindowController.java
new file mode 100644
index 0000000000..fb543cb03b
--- /dev/null
+++ b/packages/apps/Car/Launcher/src/com/android/car/carlauncher/virtualdisplay/SmallWindowController.java
@@ -0,0 +1,60 @@
+package com.android.car.carlauncher.virtualdisplay;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.util.Log;
+
+public class SmallWindowController {
+    private SmallWindowService mService;
+    private boolean mBound = false;
+    private final Context mContext;
+    private final ServiceConnection mConnection = new ServiceConnection() {
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            SmallWindowService.LocalBinder binder = (SmallWindowService.LocalBinder) service;
+            mService = binder.getService();
+            mBound = true;
+            Log.i("SmallWindowController", "Service connected");
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            Log.i("SmallWindowController", "Service disconnected");
+            mBound = false;
+            mService = null;
+        }
+    };
+
+    public SmallWindowController(Context context) {
+        mContext = context;
+    }
+
+    public void bind() {
+        Intent intent = new Intent(mContext, SmallWindowService.class);
+        mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
+        Log.i("SmallWindowController", "bind service");
+    }
+
+    public void unbind() {
+        if (mBound) {
+            mContext.unbindService(mConnection);
+            mBound = false;
+        }
+    }
+
+    public void openSmallWindow(String pkg, String cls) {
+        if (mBound && mService != null) {
+            boolean ret = mService.startSmallWindow(pkg, cls);
+            Log.i("SmallWindowController", "openSmallWindow " + pkg + " cls: " + cls + " in small window"+ ", mBound= " + mBound + " mService= " + (mService != null)+" ret : "+ret);
+        }
+    }
+
+    public void closeSmallWindow(String pkg) {
+        if (mBound && mService != null) {
+            mService.stopSmallWindow(pkg);
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/apps/Car/Launcher/src/com/android/car/carlauncher/virtualdisplay/SmallWindowGlobalController.java b/packages/apps/Car/Launcher/src/com/android/car/carlauncher/virtualdisplay/SmallWindowGlobalController.java
new file mode 100644
index 0000000000..deac41f5eb
--- /dev/null
+++ b/packages/apps/Car/Launcher/src/com/android/car/carlauncher/virtualdisplay/SmallWindowGlobalController.java
@@ -0,0 +1,51 @@
+package com.android.car.carlauncher.virtualdisplay;
+
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+public class SmallWindowGlobalController {
+    private static final AtomicReference<SmallWindowGlobalController> sInstance =
+            new AtomicReference<>();
+
+    private final Context mContext;
+    private final Handler mHandler = new Handler(Looper.getMainLooper());
+    private SmallWindowController mController;
+
+    private SmallWindowGlobalController(Context context) {
+        mContext = context.getApplicationContext();
+    }
+
+    public static SmallWindowGlobalController getInstance(Context context) {
+        SmallWindowGlobalController instance = sInstance.get();
+        if (instance == null) {
+            instance = new SmallWindowGlobalController(context);
+            sInstance.compareAndSet(null, instance);
+        }
+        return instance;
+    }
+
+    public void openSmallWindow(String pkg, String cls) {
+        ensureController();
+        mController.openSmallWindow(pkg, cls);
+    }
+
+    private void ensureController() {
+        Log.i("Launching", " ensureController, mController = " + (mController == null));
+        if (mController == null) {
+            mController = new SmallWindowController(mContext);
+            mController.bind();
+            // 自动解绑(避免内存泄漏)
+            mHandler.postDelayed(this::maybeUnbind, 30_000); // 30秒后尝试解绑
+        }
+    }
+
+    private void maybeUnbind() {
+        // 可扩展:检查是否有活跃小窗,无则解绑
+        // 此处简化为不解绑,由 Service 自管理
+    }
+}
\ No newline at end of file
diff --git a/packages/apps/Car/Launcher/src/com/android/car/carlauncher/virtualdisplay/SmallWindowService.java b/packages/apps/Car/Launcher/src/com/android/car/carlauncher/virtualdisplay/SmallWindowService.java
new file mode 100644
index 0000000000..cd683d0ebb
--- /dev/null
+++ b/packages/apps/Car/Launcher/src/com/android/car/carlauncher/virtualdisplay/SmallWindowService.java
@@ -0,0 +1,373 @@
+package com.android.car.carlauncher.virtualdisplay;
+
+import static com.android.car.carlauncher.AppLauncherUtils.launchActivityOnDisplay;
+
+import android.annotation.SuppressLint;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Color;
+import android.graphics.PixelFormat;
+import android.graphics.Point;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.VirtualDisplay;
+import android.hardware.input.InputManager;
+import android.os.Binder;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.Looper;
+import android.util.Log;
+import android.view.Display;
+import android.view.Gravity;
+import android.view.InputChannel;
+import android.view.InputEvent;
+import android.view.InputEventReceiver;
+import android.view.MotionEvent;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import android.widget.ImageButton;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class SmallWindowService extends Service {
+    private static final String TAG = "SmallWindowService";
+    private static final int WINDOW_WIDTH = 480;
+    private static final int WINDOW_HEIGHT = 854;
+    private static final int DPI = 120;
+    private WindowManager mWm;
+    private DisplayManager mDm;
+    private final Map<String, WindowRecord> mRecords = new HashMap<>();
+    private final IBinder mBinder = new LocalBinder();
+    private SurfaceView mSurfaceView;
+
+    public class LocalBinder extends Binder {
+        SmallWindowService getService() {
+            return SmallWindowService.this;
+        }
+    }
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        mWm = (WindowManager) getSystemService(WINDOW_SERVICE);
+        mDm = (DisplayManager) getSystemService(DISPLAY_SERVICE);
+        Log.i(TAG, "Service created");
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return mBinder;
+    }
+
+    // ====== Public API ======
+    public boolean startSmallWindow(String pkg, String cls) {
+        Log.i(TAG, "Started small window for package: " + pkg + ", class: " + cls);
+        if (pkg == null || cls == null || mRecords.containsKey(pkg)) {
+            return false;
+        }
+
+        WindowRecord record = new WindowRecord(pkg, cls);
+        if (record.create()) {
+            mRecords.put(pkg, record);
+            Log.i(TAG, "Started small window for: " + pkg);
+            return true;
+        }
+        return false;
+    }
+
+    public void stopSmallWindow(String pkg) {
+        WindowRecord record = mRecords.remove(pkg);
+        if (record != null) {
+            record.release();
+            Log.i(TAG, "Closed small window for: " + pkg);
+        }
+    }
+
+    public void stopAll() {
+        for (WindowRecord r : mRecords.values()) r.release();
+        mRecords.clear();
+    }
+
+    @Override
+    public void onDestroy() {
+        stopAll();
+        super.onDestroy();
+    }
+
+    // ====== 内部类:窗口记录 ======
+    private class WindowRecord {
+        final String packageName;
+        final String className;
+        VirtualDisplay virtualDisplay;
+        private FrameLayout mContainerView;
+
+        WindowRecord(String pkg, String cls) {
+            this.packageName = pkg;
+            this.className = cls;
+        }
+
+        @SuppressLint("MissingPermission")
+        boolean create() {
+            Display display = ((WindowManager) getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
+            Point size = new Point();
+            display.getSize(size);
+
+            // 1. 创建合法 UI Context
+            DisplayManager dm = (DisplayManager) getSystemService(Context.DISPLAY_SERVICE);
+            Display defaultDisplay = dm.getDisplay(Display.DEFAULT_DISPLAY);
+            if (defaultDisplay == null) {
+                Log.e(TAG, "No default display");
+                return false;
+            }
+
+            Context windowContext;
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                windowContext = createWindowContext(
+                        defaultDisplay,
+                        WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
+                        null
+                );
+            } else {
+                Log.e(TAG, "Unsupported SDK version");
+                return false;
+            }
+
+            // 2. 创建 SurfaceView
+            mSurfaceView = new SurfaceView(windowContext);
+            mSurfaceView.setZOrderMediaOverlay(true);
+            mSurfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
+                @Override
+                public void surfaceCreated(SurfaceHolder holder) {
+                    Log.d(TAG, "Surface created for " + packageName);
+                    createVirtualDisplayAndLaunch(holder.getSurface());
+                }
+
+                @Override
+                public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+                }
+
+                @Override
+                public void surfaceDestroyed(SurfaceHolder holder) {
+//                    release();
+                }
+            });
+
+            // 4. 将 SurfaceView 添加到 WindowManager(触发 Surface 创建)
+//            WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
+//            WindowManager.LayoutParams params = new WindowManager.LayoutParams(
+//                    WINDOW_WIDTH, WINDOW_HEIGHT,
+//                    Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ?
+//                            WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY :
+//                            WindowManager.LayoutParams.TYPE_PHONE,
+//                    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
+//                            WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN |
+//                            WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
+//                    PixelFormat.TRANSLUCENT
+//            );
+//            params.gravity = Gravity.LEFT | Gravity.CENTER_VERTICAL;
+//            params.x = dpToPx(48);
+//            params.y = dpToPx(48);
+
+            // 4. 将 SurfaceView 添加到 WindowManager(触发 Surface 创建)
+            WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
+            WindowManager.LayoutParams params = new WindowManager.LayoutParams(
+                    WINDOW_WIDTH, WINDOW_HEIGHT,
+                    Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ?
+                            WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY :
+                            WindowManager.LayoutParams.TYPE_PHONE,
+                    // ✅ 允许触摸,但不抢焦点(车机安全)
+                    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
+                            WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN |
+                            WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED |
+                            // ✅ 必须:允许窗口外触摸(用于拖拽到边缘)
+                            WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
+                    PixelFormat.TRANSLUCENT
+            );
+//            params.gravity = Gravity.NO_GRAVITY; // 使用绝对 x/y
+            params.gravity = Gravity.LEFT | Gravity.CENTER_VERTICAL;
+            params.x = dpToPx(48);
+            params.y = size.y - WINDOW_HEIGHT - dpToPx(48); // 默认右下角
+
+            //create  framelayout container
+            // 1. 创建容器
+            mContainerView = new FrameLayout(windowContext);
+            // 2. SurfaceView 作为内容(不可交互)
+            mSurfaceView.setZOrderMediaOverlay(true);
+            mContainerView.addView(mSurfaceView, new FrameLayout.LayoutParams(
+                    WINDOW_WIDTH, WINDOW_HEIGHT
+            ));
+
+            // 3. 添加控制栏(顶部 48dp 高,带关闭按钮)
+            FrameLayout controlBar = new FrameLayout(windowContext);
+            controlBar.setBackgroundColor(Color.argb(128, 0, 0, 0)); // 半透黑
+            FrameLayout.LayoutParams barParams = new FrameLayout.LayoutParams(
+                    ViewGroup.LayoutParams.MATCH_PARENT,
+                    dpToPx(48)
+            );
+            controlBar.setTag("control_bar");
+            mContainerView.addView(controlBar, barParams);
+
+            // 4. 关闭按钮
+            ImageButton closeButton = new ImageButton(windowContext);
+            closeButton.setImageResource(android.R.drawable.ic_menu_close_clear_cancel);
+            closeButton.setBackground(null);
+            FrameLayout.LayoutParams closeParams = new FrameLayout.LayoutParams(
+                    dpToPx(48),
+                    dpToPx(48)
+            );
+            closeParams.gravity = Gravity.END;
+            controlBar.addView(closeButton, closeParams);
+
+
+            closeButton.setOnClickListener(v -> {
+                Log.i(TAG, "Close button clicked for " + packageName);
+                stopSmallWindow(packageName);
+            });
+
+            final float[] dragStart = new float[2];
+            final WindowManager.LayoutParams currentParams = params; // 保存引用
+
+            controlBar.setOnTouchListener(new View.OnTouchListener() {
+                private boolean isDragging = false;
+
+                @Override
+                public boolean onTouch(View v, MotionEvent event) {
+                    switch (event.getAction()) {
+                        case MotionEvent.ACTION_DOWN:
+                            dragStart[0] = event.getRawX();
+                            dragStart[1] = event.getRawY();
+                            // 长按 300ms 启动拖拽(避免误触)
+                            controlBar.postDelayed(() -> {
+                                if (!isDragging) {
+                                    isDragging = true;
+                                    // 可选:显示拖拽提示
+                                }
+                            }, 300);
+                            return true;
+
+                        case MotionEvent.ACTION_MOVE:
+                            if (isDragging) {
+                                int dx = (int) (event.getRawX() - dragStart[0]);
+                                int dy = (int) (event.getRawY() - dragStart[1]);
+
+                                // 更新窗口位置(相对初始位置)
+                                currentParams.x += dx;
+                                currentParams.y += dy;
+
+                                // 边界限制(可选)
+                                clampPosition(currentParams, windowContext);
+
+                                wm.updateViewLayout(mContainerView, currentParams);
+
+                                dragStart[0] = event.getRawX();
+                                dragStart[1] = event.getRawY();
+                            }
+                            return true;
+
+                        case MotionEvent.ACTION_UP:
+                        case MotionEvent.ACTION_CANCEL:
+                            if (isDragging) {
+                                isDragging = false;
+                                return true;
+                            }
+                            // 如果是短按,交给子 View(如关闭按钮)
+                            return false;
+                    }
+                    return false;
+                }
+            });
+            try {
+                wm.addView(mContainerView, params);
+                return true; // ✅ 成功添加窗口,等待 surfaceCreated
+            } catch (Exception e) {
+                Log.e(TAG, "Failed to add view", e);
+                return false;
+            }
+        }
+
+        private void clampPosition(WindowManager.LayoutParams params, Context context) {
+            Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE))
+                    .getDefaultDisplay();
+            Point size = new Point();
+            display.getSize(size);
+
+            // 限制 x: [0, screenWidth - windowWidth]
+            params.x = Math.max(0, Math.min(params.x, size.x - WINDOW_WIDTH));
+            // 限制 y: [0, screenHeight - windowHeight]
+            params.y = Math.max(0, Math.min(params.y, size.y - WINDOW_HEIGHT));
+        }
+
+        private int dpToPx(int dp) {
+            float density = getResources().getDisplayMetrics().density;
+            return (int) (dp * density + 0.5f); // +0.5f 用于四舍五入
+        }
+
+        @SuppressLint({"MissingPermission", "ClickableViewAccessibility"})
+        private void createVirtualDisplayAndLaunch(Surface surface) {
+            DisplayManager dm = (DisplayManager) getSystemService(Context.DISPLAY_SERVICE);
+            virtualDisplay = dm.createVirtualDisplay(
+                    "CarSmallWindow_" + packageName,
+                    WINDOW_WIDTH, WINDOW_HEIGHT, DPI,
+                    surface,
+                    DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION
+            );
+            if (virtualDisplay != null) {
+                int displayId = virtualDisplay.getDisplay().getDisplayId();
+                launchActivityOnDisplay(SmallWindowService.this, packageName, className, displayId);
+                Log.i(TAG, "VirtualDisplay created and app launched for " + packageName + " displayId: " + displayId);
+            } else {
+                Log.e(TAG, "Failed to create VirtualDisplay for " + packageName);
+            }
+
+            mSurfaceView.setOnTouchListener((View _, MotionEvent event) -> {
+                if (virtualDisplay == null || virtualDisplay.getDisplay().getDisplayId() == -1) {
+                    return false;
+                }
+                int displayId = virtualDisplay.getDisplay().getDisplayId();
+                // 获取触摸在 SurfaceView 内部的坐标(0 ~ width, 0 ~ height)
+                float x = event.getX();
+                float y = event.getY();
+
+                // 如果小窗有缩放或裁剪,这里需做映射(目前假设 1:1)
+                // 例如:x = x * (virtualWidth / surfaceViewWidth)
+
+                // 创建新事件,目标为 VirtualDisplay
+                MotionEvent mappedEvent = MotionEvent.obtain(event);
+                mappedEvent.setLocation(x, y); // 重置为局部坐标
+                mappedEvent.setDisplayId(displayId); // ★ 核心:指定目标 Display
+
+                // 注入事件
+                InputManager im = (InputManager) getSystemService(Context.INPUT_SERVICE);
+                try {
+                    // 使用 WAIT_FOR_FINISH 确保事件被处理
+                    im.injectInputEvent(mappedEvent, InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
+                } catch (Exception e) {
+                    Log.w(TAG, "Failed to inject touch event to display " + displayId, e);
+                }
+
+                mappedEvent.recycle();
+                return true; // 消费事件,防止穿透到 Launcher 主屏
+            });
+        }
+
+        private void release() {
+            if (mContainerView != null) {
+                try {
+                    WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
+                    wm.removeViewImmediate(mContainerView);
+                } catch (Exception ignored) {
+                }
+            }
+            if (virtualDisplay != null) {
+                virtualDisplay.release();
+                virtualDisplay = null;
+            }
+        }
+    }
+}
\ No newline at end of file
相关推荐
赏金术士1 小时前
第七章:状态管理实战与架构总结
android·ui·kotlin·compose
颂love3 小时前
MySQL的执行流程
android·数据库·mysql
云起SAAS7 小时前
抖音小游戏源码 - 消消乐 | 含激励广告+成就系统 | 开箱即用商业级消除游戏模板
android·游戏·广告联盟·看激励广告联盟流量主·抖音小游戏源码 - 消消乐
大貔貅喝啤酒8 小时前
基于Windows下载安装Android Studio 3.3.2版本教程(2026详细图文版)
android·java·windows·android studio
程序员码歌8 小时前
OpenSpec 到 Superpowers:AI 编码从说清到做对
android·前端·人工智能
2501_915106329 小时前
深入解析无源码iOS加固原理与方案,保护应用安全
android·安全·ios·小程序·uni-app·cocoa·iphone
黄林晴12 小时前
重磅官宣:Android UI 开发正式进入 Compose-first 时代
android·google io
Kapaseker12 小时前
搞懂变换!精通 Compose 绘制(二)
android·kotlin
美狐美颜SDK开放平台13 小时前
美颜SDK开发详解:如何优化美颜SDK在低端安卓机上的性能?
android·ios·音视频·直播美颜sdk·视频美颜sdk