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 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台1 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone1 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc1 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo1 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077001 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼1 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone1 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen1 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone1 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui