diff --git a/android-launcher/README.md b/android-launcher/README.md
new file mode 100644
index 00000000..9b6e0de4
--- /dev/null
+++ b/android-launcher/README.md
@@ -0,0 +1,3 @@
+Android launcher skeleton for MonkeyCode repository.
+
+Contains minimal Android Studio project structure with provided launcher skeleton code. Use Android Studio to import the project (open folder android-launcher) and sync Gradle. Build and run on device/emulator with SDK >=29.
diff --git a/android-launcher/app/build.gradle b/android-launcher/app/build.gradle
new file mode 100644
index 00000000..0eeb7c73
--- /dev/null
+++ b/android-launcher/app/build.gradle
@@ -0,0 +1,30 @@
+plugins {
+ id 'com.android.application'
+}
+
+android {
+ compileSdk 33
+
+ defaultConfig {
+ applicationId "com.x8.launcher"
+ minSdk 29
+ targetSdk 30
+ versionCode 1
+ versionName "0.1"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ implementation 'androidx.appcompat:appcompat:1.6.1'
+ implementation 'androidx.recyclerview:recyclerview:1.3.0'
+ implementation 'androidx.viewpager2:viewpager2:1.1.0'
+ implementation 'com.github.bumptech.glide:glide:4.16.0'
+ annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
+}
diff --git a/android-launcher/app/src/main/AndroidManifest.xml b/android-launcher/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..daa6ad69
--- /dev/null
+++ b/android-launcher/app/src/main/AndroidManifest.xml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android-launcher/app/src/main/java/com/x8/launcher/adapter/DesktopPageAdapter.java b/android-launcher/app/src/main/java/com/x8/launcher/adapter/DesktopPageAdapter.java
new file mode 100644
index 00000000..997f3a61
--- /dev/null
+++ b/android-launcher/app/src/main/java/com/x8/launcher/adapter/DesktopPageAdapter.java
@@ -0,0 +1,49 @@
+package com.x8.launcher.adapter;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.GridView;
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+import com.x8.launcher.R;
+import com.x8.launcher.bean.AppInfo;
+import java.util.List;
+
+public class DesktopPageAdapter extends RecyclerView.Adapter {
+ private Context mContext;
+ private List desktopAppList;
+
+ public DesktopPageAdapter(Context context, List list) {
+ this.mContext = context;
+ this.desktopAppList = list;
+ }
+
+ @NonNull
+ @Override
+ public PageViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ // 加载单页桌面图标网格布局
+ View v = LayoutInflater.from(mContext).inflate(R.layout.item_desktop_page, parent, false);
+ return new PageViewHolder(v);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull PageViewHolder holder, int position) {
+ // 简化:目前每页显示全部应用的一个 GridView,实际应按行列分页
+ GridView gv = holder.itemView.findViewById(R.id.gv_app_grid);
+ // adapter and drag logic to be implemented in full version
+ }
+
+ @Override
+ public int getItemCount() {
+ // 根据应用数量、桌面行列配置计算总页数,简化为 1
+ return 1;
+ }
+
+ public static class PageViewHolder extends RecyclerView.ViewHolder {
+ public PageViewHolder(@NonNull View itemView) {
+ super(itemView);
+ }
+ }
+}
diff --git a/android-launcher/app/src/main/java/com/x8/launcher/bean/AppInfo.java b/android-launcher/app/src/main/java/com/x8/launcher/bean/AppInfo.java
new file mode 100644
index 00000000..6d11f21a
--- /dev/null
+++ b/android-launcher/app/src/main/java/com/x8/launcher/bean/AppInfo.java
@@ -0,0 +1,28 @@
+package com.x8.launcher.bean;
+
+import android.graphics.drawable.Drawable;
+
+public class AppInfo {
+ private String appName;
+ private String packageName;
+ private Drawable appIcon;
+ private String versionName;
+ private boolean isSystemApp;
+ private boolean isHideApp;
+ private boolean isDockApp;
+
+ public String getAppName() { return appName; }
+ public void setAppName(String appName) { this.appName = appName; }
+ public String getPackageName() { return packageName; }
+ public void setPackageName(String packageName) { this.packageName = packageName; }
+ public Drawable getAppIcon() { return appIcon; }
+ public void setAppIcon(Drawable appIcon) { this.appIcon = appIcon; }
+ public String getVersionName() { return versionName; }
+ public void setVersionName(String versionName) { this.versionName = versionName; }
+ public boolean isSystemApp() { return isSystemApp; }
+ public void setSystemApp(boolean systemApp) { isSystemApp = systemApp; }
+ public boolean isHideApp() { return isHideApp; }
+ public void setHideApp(boolean hideApp) { isHideApp = hideApp; }
+ public boolean isDockApp() { return isDockApp; }
+ public void setDockApp(boolean dockApp) { isDockApp = dockApp; }
+}
diff --git a/android-launcher/app/src/main/java/com/x8/launcher/bean/CarStatusInfo.java b/android-launcher/app/src/main/java/com/x8/launcher/bean/CarStatusInfo.java
new file mode 100644
index 00000000..2348dcf6
--- /dev/null
+++ b/android-launcher/app/src/main/java/com/x8/launcher/bean/CarStatusInfo.java
@@ -0,0 +1,11 @@
+package com.x8.launcher.bean;
+
+public class CarStatusInfo {
+ private float speed;
+ private float fuelLevel;
+
+ public float getSpeed() { return speed; }
+ public void setSpeed(float speed) { this.speed = speed; }
+ public float getFuelLevel() { return fuelLevel; }
+ public void setFuelLevel(float fuelLevel) { this.fuelLevel = fuelLevel; }
+}
diff --git a/android-launcher/app/src/main/java/com/x8/launcher/car/VehicleControlManager.java b/android-launcher/app/src/main/java/com/x8/launcher/car/VehicleControlManager.java
new file mode 100644
index 00000000..8ffa58d0
--- /dev/null
+++ b/android-launcher/app/src/main/java/com/x8/launcher/car/VehicleControlManager.java
@@ -0,0 +1,39 @@
+package com.x8.launcher.car;
+
+import android.content.Context;
+import android.content.Intent;
+
+public class VehicleControlManager {
+ private Context mContext;
+
+ public VehicleControlManager(Context context){
+ this.mContext = context;
+ }
+
+ // 设置空调温度
+ public void setAcTemp(int temp){
+ Intent intent = new Intent("com.lynk.action.HVAC_CONTROL");
+ intent.putExtra("temp_main",temp);
+ mContext.sendBroadcast(intent);
+ }
+
+ // 控制电动尾门开关
+ public void openTailGate(boolean open){
+ Intent intent = new Intent("com.lynk.action.TAILGATE_CONTROL");
+ intent.putExtra("open", open);
+ mContext.sendBroadcast(intent);
+ }
+
+ // 切换驾驶模式 经济/舒适/运动
+ public void changeDriveMode(int mode){
+ Intent intent = new Intent("com.lynk.action.DRIVE_MODE_CHANGE");
+ intent.putExtra("mode", mode);
+ mContext.sendBroadcast(intent);
+ }
+
+ // 唤起360全景影像
+ public void open360Camera(){
+ Intent intent = new Intent("com.lynk.action.OPEN_360_CAMERA");
+ mContext.sendBroadcast(intent);
+ }
+}
diff --git a/android-launcher/app/src/main/java/com/x8/launcher/car/VehicleDataManager.java b/android-launcher/app/src/main/java/com/x8/launcher/car/VehicleDataManager.java
new file mode 100644
index 00000000..27a8f726
--- /dev/null
+++ b/android-launcher/app/src/main/java/com/x8/launcher/car/VehicleDataManager.java
@@ -0,0 +1,40 @@
+package com.x8.launcher.car;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import com.x8.launcher.bean.CarStatusInfo;
+
+public class VehicleDataManager {
+ private Context mContext;
+ private CarStatusInfo current;
+
+ public VehicleDataManager(Context context){
+ this.mContext = context;
+ current = new CarStatusInfo();
+ registerCarStatusReceiver();
+ }
+
+ // 注册车况广播监听,解析车速、油量、空调、门窗数据
+ private void registerCarStatusReceiver(){
+ IntentFilter filter = new IntentFilter();
+ // 示例私有广播,具体以车厂为准
+ filter.addAction("com.lynk.action.VEHICLE_STATUS");
+ mContext.registerReceiver(new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // 解析车况数据并填充 current(示例字段)
+ if ("com.lynk.action.VEHICLE_STATUS".equals(intent.getAction())){
+ current.setSpeed(intent.getFloatExtra("speed", 0f));
+ current.setFuelLevel(intent.getFloatExtra("fuel", 0f));
+ }
+ }
+ }, filter);
+ }
+
+ // 获取最新整车状态数据
+ public CarStatusInfo getCurrentCarInfo(){
+ return current;
+ }
+}
diff --git a/android-launcher/app/src/main/java/com/x8/launcher/floatview/FloatBarManager.java b/android-launcher/app/src/main/java/com/x8/launcher/floatview/FloatBarManager.java
new file mode 100644
index 00000000..35da14e1
--- /dev/null
+++ b/android-launcher/app/src/main/java/com/x8/launcher/floatview/FloatBarManager.java
@@ -0,0 +1,80 @@
+package com.x8.launcher.floatview;
+
+import android.content.Context;
+import android.graphics.PixelFormat;
+import android.os.Build;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.WindowManager;
+import com.x8.launcher.R;
+
+public class FloatBarManager {
+ private Context mContext;
+ private WindowManager windowManager;
+ private View floatSideView;
+ private WindowManager.LayoutParams layoutParams;
+
+ public FloatBarManager(Context context) {
+ this.mContext = context;
+ windowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
+ }
+
+ public void createSideFloatView() {
+ if (floatSideView != null) return;
+ floatSideView = LayoutInflater.from(mContext).inflate(R.layout.layout_float_sidebar, null);
+ initWindowParams();
+ try {
+ windowManager.addView(floatSideView, layoutParams);
+ } catch (Exception e) {
+ // ignore if cannot add (permission)
+ }
+ bindFloatEvent();
+ }
+
+ private void initWindowParams() {
+ layoutParams = new WindowManager.LayoutParams();
+ layoutParams.width = (int) (260 * mContext.getResources().getDisplayMetrics().density);
+ layoutParams.height = WindowManager.LayoutParams.MATCH_PARENT;
+ layoutParams.format = PixelFormat.TRANSLUCENT;
+ layoutParams.type = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) ? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : WindowManager.LayoutParams.TYPE_PHONE;
+ layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+ layoutParams.gravity = Gravity.START | Gravity.TOP;
+ layoutParams.x = 0;
+ layoutParams.y = 0;
+ }
+
+ private void bindFloatEvent() {
+ floatSideView.setOnTouchListener(new View.OnTouchListener() {
+ private int lastX, lastY;
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ int rawX = (int) event.getRawX();
+ int rawY = (int) event.getRawY();
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ lastX = rawX;
+ lastY = rawY;
+ return true;
+ case MotionEvent.ACTION_MOVE:
+ int dx = rawX - lastX;
+ int dy = rawY - lastY;
+ layoutParams.x += dx;
+ layoutParams.y += dy;
+ try { windowManager.updateViewLayout(floatSideView, layoutParams); } catch (Exception ignored) {}
+ lastX = rawX; lastY = rawY;
+ return true;
+ }
+ return false;
+ }
+ });
+ }
+
+ public void destroyFloatView() {
+ if (floatSideView != null) {
+ try { windowManager.removeView(floatSideView); } catch (Exception ignored) {}
+ floatSideView = null;
+ }
+ }
+}
diff --git a/android-launcher/app/src/main/java/com/x8/launcher/floatview/FloatSideBarService.java b/android-launcher/app/src/main/java/com/x8/launcher/floatview/FloatSideBarService.java
new file mode 100644
index 00000000..9fdf08ed
--- /dev/null
+++ b/android-launcher/app/src/main/java/com/x8/launcher/floatview/FloatSideBarService.java
@@ -0,0 +1,35 @@
+package com.x8.launcher.floatview;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+public class FloatSideBarService extends Service {
+ private FloatBarManager floatBarManager;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ floatBarManager = new FloatBarManager(this);
+ floatBarManager.createSideFloatView();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ return START_STICKY;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (floatBarManager != null) {
+ floatBarManager.destroyFloatView();
+ }
+ try { startService(new Intent(this, FloatSideBarService.class)); } catch (Exception ignored) {}
+ }
+}
diff --git a/android-launcher/app/src/main/java/com/x8/launcher/manager/AppLoadManager.java b/android-launcher/app/src/main/java/com/x8/launcher/manager/AppLoadManager.java
new file mode 100644
index 00000000..28996820
--- /dev/null
+++ b/android-launcher/app/src/main/java/com/x8/launcher/manager/AppLoadManager.java
@@ -0,0 +1,74 @@
+package com.x8.launcher.manager;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import com.x8.launcher.bean.AppInfo;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+public class AppLoadManager {
+ private Context mContext;
+ private List allAppList = new ArrayList<>();
+ private List desktopShowAppList = new ArrayList<>();
+
+ public AppLoadManager(Context context) {
+ this.mContext = context;
+ }
+
+ public void loadInstallAppList() {
+ allAppList.clear();
+ desktopShowAppList.clear();
+ PackageManager pm = mContext.getPackageManager();
+ Intent intent = new Intent(Intent.ACTION_MAIN, null);
+ intent.addCategory(Intent.CATEGORY_LAUNCHER);
+ List resolveInfoList = pm.queryIntentActivities(intent, 0);
+
+ for (ResolveInfo resolveInfo : resolveInfoList) {
+ AppInfo appInfo = new AppInfo();
+ appInfo.setAppName(resolveInfo.loadLabel(pm).toString());
+ String pkgName = resolveInfo.activityInfo.packageName;
+ appInfo.setPackageName(pkgName);
+ appInfo.setAppIcon(resolveInfo.loadIcon(pm));
+ try {
+ PackageInfo packageInfo = pm.getPackageInfo(pkgName, 0);
+ appInfo.setSystemApp((packageInfo.applicationInfo.flags & android.content.pm.ApplicationInfo.FLAG_SYSTEM) != 0);
+ } catch (Exception e) {
+ appInfo.setSystemApp(false);
+ }
+ allAppList.add(appInfo);
+ }
+ filterHideApp();
+ }
+
+ private void filterHideApp() {
+ desktopShowAppList.addAll(allAppList);
+ }
+
+ public void sortAppByName() {
+ Collections.sort(allAppList, new Comparator() {
+ @Override
+ public int compare(AppInfo a, AppInfo b) {
+ if (a.getAppName() == null) return -1;
+ return a.getAppName().compareToIgnoreCase(b.getAppName());
+ }
+ });
+ filterHideApp();
+ }
+
+ public void sortAppByUseCount() {
+ // Placeholder: usage stats not implemented here. Keep original order.
+ }
+
+ public List getAllAppList() {
+ return allAppList;
+ }
+
+ public List getDesktopShowAppList() {
+ return desktopShowAppList;
+ }
+}
diff --git a/android-launcher/app/src/main/java/com/x8/launcher/permission/PermissionCheckUtil.java b/android-launcher/app/src/main/java/com/x8/launcher/permission/PermissionCheckUtil.java
new file mode 100644
index 00000000..226e4b40
--- /dev/null
+++ b/android-launcher/app/src/main/java/com/x8/launcher/permission/PermissionCheckUtil.java
@@ -0,0 +1,44 @@
+package com.x8.launcher.permission;
+
+import android.app.Activity;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Build;
+import android.provider.Settings;
+
+public class PermissionCheckUtil {
+
+ public static void checkAllNeedPermission(Activity activity) {
+ checkFloatWindowPermission(activity);
+ checkUsageStatsPermission(activity);
+ checkBatteryOptPermission(activity);
+ }
+
+ private static void checkFloatWindowPermission(Activity activity) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(activity)) {
+ Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + activity.getPackageName()));
+ activity.startActivity(intent);
+ }
+ }
+
+ private static void checkUsageStatsPermission(Activity activity) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ AppOpsManager appOps = (AppOpsManager) activity.getSystemService(Context.APP_OPS_SERVICE);
+ int mode = appOps.checkOpNoThrow("android:get_usage_stats", Binder.getCallingUid(), activity.getPackageName());
+ if (mode != AppOpsManager.MODE_ALLOWED) {
+ Intent intent = new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS);
+ activity.startActivity(intent);
+ }
+ }
+ }
+
+ private static void checkBatteryOptPermission(Activity activity) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ Intent intent = new Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS);
+ activity.startActivity(intent);
+ }
+ }
+}
diff --git a/android-launcher/app/src/main/java/com/x8/launcher/receiver/AppInstallReceiver.java b/android-launcher/app/src/main/java/com/x8/launcher/receiver/AppInstallReceiver.java
new file mode 100644
index 00000000..9bce01aa
--- /dev/null
+++ b/android-launcher/app/src/main/java/com/x8/launcher/receiver/AppInstallReceiver.java
@@ -0,0 +1,16 @@
+package com.x8.launcher.receiver;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import com.x8.launcher.manager.AppLoadManager;
+
+public class AppInstallReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // 应用变更后重新加载应用列表,刷新桌面图标
+ AppLoadManager manager = new AppLoadManager(context);
+ manager.loadInstallAppList();
+ // Note: to notify UI, use LocalBroadcast or observer pattern in a full implementation
+ }
+}
diff --git a/android-launcher/app/src/main/java/com/x8/launcher/receiver/BootCompleteReceiver.java b/android-launcher/app/src/main/java/com/x8/launcher/receiver/BootCompleteReceiver.java
new file mode 100644
index 00000000..7a2d2cbb
--- /dev/null
+++ b/android-launcher/app/src/main/java/com/x8/launcher/receiver/BootCompleteReceiver.java
@@ -0,0 +1,20 @@
+package com.x8.launcher.receiver;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import com.x8.launcher.floatview.FloatSideBarService;
+
+public class BootCompleteReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (Intent.ACTION_BOOT_COMPLETED.equals(action) || Intent.ACTION_LOCKED_BOOT_COMPLETED.equals(action)) {
+ Intent service = new Intent(context, FloatSideBarService.class);
+ context.startService(service);
+ Intent launcherIntent = new Intent(context, com.x8.launcher.MainLauncherActivity.class);
+ launcherIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(launcherIntent);
+ }
+ }
+}
diff --git a/android-launcher/app/src/main/java/com/x8/launcher/ui/SettingActivity.java b/android-launcher/app/src/main/java/com/x8/launcher/ui/SettingActivity.java
new file mode 100644
index 00000000..a0d5c136
--- /dev/null
+++ b/android-launcher/app/src/main/java/com/x8/launcher/ui/SettingActivity.java
@@ -0,0 +1,19 @@
+package com.x8.launcher.ui;
+
+import android.os.Bundle;
+import androidx.appcompat.app.AppCompatActivity;
+import com.x8.launcher.R;
+
+public class SettingActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_setting);
+ initSettingView();
+ }
+
+ private void initSettingView() {
+ // 绑定各个设置项点击事件 - 完整实现待补充
+ }
+}
diff --git a/android-launcher/app/src/main/java/com/x8/launcher/util/SPUtil.java b/android-launcher/app/src/main/java/com/x8/launcher/util/SPUtil.java
new file mode 100644
index 00000000..4383a7a0
--- /dev/null
+++ b/android-launcher/app/src/main/java/com/x8/launcher/util/SPUtil.java
@@ -0,0 +1,21 @@
+package com.x8.launcher.util;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+public class SPUtil {
+ private static final String SP_NAME = "xiaoba_launcher_config";
+ private static SharedPreferences sp;
+
+ public static void init(Context context) {
+ if (sp == null) sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
+ }
+
+ public static void putString(String key, String value) { sp.edit().putString(key, value).apply(); }
+ public static String getString(String key, String defVal) { return sp.getString(key, defVal); }
+ public static void putBoolean(String key, boolean value) { sp.edit().putBoolean(key, value).apply(); }
+ public static boolean getBoolean(String key, boolean defVal) { return sp.getBoolean(key, defVal); }
+ public static void putInt(String key, int value) { sp.edit().putInt(key, value).apply(); }
+ public static int getInt(String key, int defVal) { return sp.getInt(key, defVal); }
+ public static void clearAll() { sp.edit().clear().apply(); }
+}
diff --git a/android-launcher/settings.gradle b/android-launcher/settings.gradle
new file mode 100644
index 00000000..c8519657
--- /dev/null
+++ b/android-launcher/settings.gradle
@@ -0,0 +1,2 @@
+rootProject.name = 'android-launcher'
+include ':app'