
思路如下
- 使用DisplayManager的createVirtualDisplay接口创建虚拟屏
- 将指定应用和Surface都绑定到一个wm上
- 设置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