Android10.0 Launcher3 启动和加载流程

一、分析文件介绍

  • ActivityManagerService.java
  • ActivityTaskManagerInternal.java
  • ActivityTaskManagerService.java
  • RootActivityContainer.java
  • ActivityStartController.java
  • LauncherProvider.java
  • Launcher.java
  • InvariantDeviceProfile.java
  • MultiModeController.java
  • LauncherProvider.java
  • MultiModeUtilities.java
  • LauncherSettingsExtension.java
  • FeatureOption.java
  • config_ext.xml
  • device_profiles.xml

二、相关文件介绍

ActivityManagerService.java

Activity会调用startHomeActivityLocked方法,此方法会创建一个Intent,mTopAction和mTopData传给Intent,其中mTopAction为Intent.ACTION_MAIN,Intent的category为android.intent.category.Home。而Launcher的AndroidMainfest.xml文件里面给Launcher定义的category也是Home,根据匹配原则,这样就会启动这个Launcher。
源码位置:frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java

public class ActivityManagerService extends IActivityManager.Stub
        implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback {
   
    public ActivityTaskManagerInternal mAtmInternal;

    public void systemReady(final Runnable goingCallback, TimingsTraceLog traceLog) {
          ....
          //启动Launcher
          mAtmInternal.startHomeOnAllDisplays(currentUserId, "systemReady");
          .....
    }
}

ActivityTaskManagerInternal.java和ActivityTaskManagerService.java

ActivityTaskManagerInternal.java是一个抽象类,里面定义了启动Luancher的方法,具体实现是由LocalService类,它定义在了ActivityTaskManagerService.java文件中
frameworks/base/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java

    /** @return The intent used to launch the home activity. */
    public abstract Intent getHomeIntent();
    public abstract boolean startHomeActivity(int userId, String reason);
    public abstract boolean startHomeOnDisplay(int userId, String reason, int displayId,
            boolean allowInstrumenting, boolean fromHomeKey);
    /** Start home activities on all displays that support system decorations. */
    public abstract boolean startHomeOnAllDisplays(int userId, String reason);

    

frameworks/base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
内部类LocalService

public class ActivityTaskManagerService extends IActivityTaskManager.Stub {
       private ActivityStartController mActivityStartController;   //Activity启动控制器

    ActivityStartController getActivityStartController() {
        return mActivityStartController;
    }
    //注意看intent.addCategory(Intent.CATEGORY_HOME),这个代表的就是要启动Activity的意图,通常来说,整个系统的只会有一个应用会在清单文件中配置CATEGORY_HOME,如果配置了多个,系统在启动的时候就会要求用户手动去选择哪个作为启动应用,如果在系统设置应用中进行配置了,就会选择配置的那个应用启动。
    Intent getHomeIntent() {
        Intent intent = new Intent(mTopAction, mTopData != null ? Uri.parse(mTopData) : null);
        intent.setComponent(mTopComponent);
        intent.addFlags(Intent.FLAG_DEBUG_TRIAGED_MISSING);
        if (mFactoryTest != FactoryTest.FACTORY_TEST_LOW_LEVEL) {
            intent.addCategory(Intent.CATEGORY_HOME);
        }
        return intent;
    }

  final class LocalService extends ActivityTaskManagerInternal {
    RootActivityContainer mRootActivityContainer; //实际上调用了RootActivityContainer 中的方法
       @Override
       public Intent getHomeIntent() {
           synchronized (mGlobalLock) {
               return ActivityTaskManagerService.this.getHomeIntent();
           }
       }

       @Override
       public boolean startHomeActivity(int userId, String reason) {
           synchronized (mGlobalLock) {
               return mRootActivityContainer.startHomeOnDisplay(userId, reason, DEFAULT_DISPLAY);
           }
       }

       @Override
       public boolean startHomeOnDisplay(int userId, String reason, int displayId,
               boolean allowInstrumenting, boolean fromHomeKey) {
           synchronized (mGlobalLock) {
               return mRootActivityContainer.startHomeOnDisplay(userId, reason, displayId,
                       allowInstrumenting, fromHomeKey);
           }
       }
      @Override
      public boolean startHomeOnAllDisplays(int userId, String reason) {
          synchronized (mGlobalLock) {
              return mRootActivityContainer.startHomeOnAllDisplays(userId, reason);
          }
      }
  }

  ...
}

RootActivityContainer.java

frameworks/base/services/core/java/com/android/server/wm/RootActivityContainer.java

class RootActivityContainer extends ConfigurationContainer
        implements DisplayManager.DisplayListener {
   ActivityTaskManagerService mService;

    //在所有显示设备上启动Home
    boolean startHomeOnAllDisplays(int userId, String reason) {
        boolean homeStarted = false;
        for (int i = mActivityDisplays.size() - 1; i >= 0; i--) {
            final int displayId = mActivityDisplays.get(i).mDisplayId;
            homeStarted |= startHomeOnDisplay(userId, reason, displayId);
        }
        return homeStarted;
    }
   boolean startHomeOnDisplay(int userId, String reason, int displayId) {
       return startHomeOnDisplay(userId, reason, displayId, false /* allowInstrumenting */,
               false /* fromHomeKey */);
   }

  //这将在可以基于 display 的系统装饰的显示器上启动主页活动Id 默认显示器始终使用主要主页组件
//对于辅助显示,主页活动必须具有类别SECONDARY_HOME然后解析根据下面列出的优先级。
  // -如果未设置默认主页,请始终使用配置中定义的辅助主页
  //-使用当前选定的主要活动
  //-使用与当前选定的主要家庭活动相同的套餐中的活动,如果有多个匹配的活动,请使用第一个活动。
    //-使用配置中定义的辅助主页
    boolean startHomeOnDisplay(int userId, String reason, int displayId, boolean allowInstrumenting,
            boolean fromHomeKey) {
        // Fallback to top focused display if the displayId is invalid.
        if (displayId == INVALID_DISPLAY) {
            displayId = getTopDisplayFocusedStack().mDisplayId;
        }

        Intent homeIntent = null;
        ActivityInfo aInfo = null;
        if (displayId == DEFAULT_DISPLAY) {
            homeIntent = mService.getHomeIntent(); //获取到需要启动的intent
            aInfo = resolveHomeActivity(userId, homeIntent); //解析出需要启动Activity的信息
        } else if (shouldPlaceSecondaryHomeOnDisplay(displayId)) {
            Pair<ActivityInfo, Intent> info = resolveSecondaryHomeActivity(userId, displayId);
            aInfo = info.first;
            homeIntent = info.second;
        }
        if (aInfo == null || homeIntent == null) {
            return false;
        }

        if (!canStartHomeOnDisplay(aInfo, displayId, allowInstrumenting)) {
            return false;
        }

        // 更新home Intent
        homeIntent.setComponent(new ComponentName(aInfo.applicationInfo.packageName, aInfo.name));
        homeIntent.setFlags(homeIntent.getFlags() | FLAG_ACTIVITY_NEW_TASK);
        // Updates the extra information of the intent.
        if (fromHomeKey) {
            homeIntent.putExtra(WindowManagerPolicy.EXTRA_FROM_HOME_KEY, true);
        }
        // Update the reason for ANR debugging to verify if the user activity is the one that
        //开始启动Launcher
        final String myReason = reason + ":" + userId + ":" + UserHandle.getUserId(
                aInfo.applicationInfo.uid) + ":" + displayId;
        mService.getActivityStartController().startHomeActivity(homeIntent, aInfo, myReason,
                displayId);
        return true;
    }

}

ActivityStartController.java

frameworks/base/services/core/java/com/android/server/wm/ActivityStartController.java
用于控制委派启动的Activity

public class ActivityStartController {

    void startHomeActivity(Intent intent, ActivityInfo aInfo, String reason, int displayId) {
        final ActivityOptions options = ActivityOptions.makeBasic(); 
        options.setLaunchWindowingMode(WINDOWING_MODE_FULLSCREEN);
        if (!ActivityRecord.isResolverActivity(aInfo.name)) {
            // The resolver activity shouldn't be put in home stack because when the foreground is
            // standard type activity, the resolver activity should be put on the top of current
            // foreground instead of bring home stack to front.
            options.setLaunchActivityType(ACTIVITY_TYPE_HOME);
        }   
        options.setLaunchDisplayId(displayId);
        //此处执行启动Launcher,intent中包含了ACTION:"android.intent.action.MAIN"和category:"android.intent.category.HOME",这样就直接启动了拥有这两个属性的Activity
        mLastHomeActivityStartResult = obtainStarter(intent, "startHomeActivity: " + reason)
                .setOutActivity(tmpOutRecord)
                .setCallingUid(0)
                .setActivityInfo(aInfo)
                .setActivityOptions(options.toBundle())
                .execute();
        mLastHomeActivityStartRecord = tmpOutRecord[0];
        final ActivityDisplay display =
                mService.mRootActivityContainer.getActivityDisplay(displayId);
        final ActivityStack homeStack = display != null ? display.getHomeStack() : null;
        if (homeStack != null && homeStack.mInResumeTopActivity) {
            // If we are in resume section already, home activity will be initialized, but not
            // resumed (to avoid recursive resume) and will stay that way until something pokes it
            // again. We need to schedule another resume.
            mSupervisor.scheduleResumeTopActivities();
        }   
    }   

}

Laucnher3的AndroidManifest.xml
可以看到com.android.launcher3.Launcher这个Activity定义了作为Launcher的ACTION和CATEGORY

     <activity
         android:name="com.android.launcher3.Launcher"
         android:launchMode="singleTask"
         android:clearTaskOnLaunch="true"
         android:stateNotNeeded="true"
         android:windowSoftInputMode="adjustPan"
         android:screenOrientation="unspecified"
         android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
         android:resizeableActivity="true"
         android:resumeWhilePausing="true"
         android:taskAffinity=""
         android:enabled="true">
         <intent-filter>
             <action android:name="android.intent.action.MAIN" />
             <category android:name="android.intent.category.HOME" />
             <category android:name="android.intent.category.DEFAULT" />
             <category android:name="android.intent.category.MONKEY"/>
             <category android:name="android.intent.category.LAUNCHER_APP" />
         </intent-filter>
         <meta-data
             android:name="com.android.launcher3.grid.control"
             android:value="${packageName}.grid_control" />
     </activity>

LauncherProvider.java

packages/apps/Launcher3/src/com/android/launcher3/LauncherProvider.java

public class LauncherProvider extends ContentProvider {
    public static final int SCHEMA_VERSION = 28; //数据库版本号

     @Override
     public boolean onCreate() {
         if (LOGD) LogUtils.d(TAG, "Launcher process started");
         mListenerHandler = new Handler(mListenerWrapper);
 
         // 内容提供程序在启动器主进程的整个持续时间内都存在,并且是要创建的第一个组件。
         MainProcessInitializer.initialize(getContext().getApplicationContext());
         return true;
     }

}

Launcher.java

public class Launcher extends BaseDraggingActivity implements LauncherExterns,
        LauncherModel.Callbacks, LauncherProviderChangeListener, UserEventDelegate,

    protected void onCreate(Bundle savedInstanceState) {
        Log.d(TAG, "bianjb onCreate");
        RaceConditionTracker.onEvent(ON_CREATE_EVT, ENTER);
        //debug时启用严格模式
        if (DEBUG_STRICT_MODE) {
            StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                    .detectDiskReads()
                    .detectDiskWrites()
                    .detectNetwork()   // or .detectAll() for all detectable problems
                    .penaltyLog()
                    .build());
            StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                    .detectLeakedSqlLiteObjects()
                    .detectLeakedClosableObjects()
                    .penaltyLog()
                    .penaltyDeath()
                    .build());
        }
        TraceHelper.beginSection("Launcher-onCreate");
        //Launcher监听器,展锐新加,用于监听并重新控制Launcher的行为
        mAppMonitor = LauncherAppMonitor.getInstance(this);
        mAppMonitor.onLauncherPreCreate(this);

}

LauncherAppMonitor.java(展锐)

packages/apps/Launcher3/src/com/sprd/ext/LauncherAppMonitor.java

InvariantDeviceProfile.java

packages/apps/Launcher3/src/com/android/launcher3/InvariantDeviceProfile.java
顾名思义,InvariantDeviceProfile是把显示相关的常量在这里初始化

调用构造方法

它有多个构造方法,调用的是下面这个

   //程序加载时就创建了一个静态MainThreadInitializedObject对象,并创建InvariantDeviceProfile作为构造参数传给MainThreadInitializedObject。双冒号为java8 lambda表达式
  public static final MainThreadInitializedObject<InvariantDeviceProfile> INSTANCE =
          new MainThreadInitializedObject<>(InvariantDeviceProfile::new);
   //gridname的key,通过它获取gridname值
   public static final String KEY_IDP_GRID_NAME = "idp_grid_name";

  private InvariantDeviceProfile(Context context) {
      Log.d(TAG, "bianjb Constructor1");
      try{
          throw new RuntimeException("打印栈调用测试~!");
      }catch(Exception e){
          e.printStackTrace();
      }
      //初始化LauncherAppMonitor,下面有介绍
      mMonitor = LauncherAppMonitor.getInstance(context);
      mIdpGridKey = MultiModeController.getKeyByMode(context, KEY_IDP_GRID_NAME);
      //调用初始化方法,getDefaultGridName获取是否有默认的GridName配置,即几x几
      //Utilities.getPrefs(context).getString(mIdpGridKey, getDefaultGridName(context))最终是在sharepref中获取的值,key="idp_grid_name",value=3_by_3,对应的sharepref.xml见下面
      initGrid(context, Utilities.getPrefs(context).getString(mIdpGridKey, getDefaultGridName(context)));
      //屏幕配置监听器
      mConfigMonitor = new ConfigMonitor(context,  APPLY_CONFIG_AT_RUNTIME.get() ? this::onConfigChanged : this::killProcess);
      //
      mOverlayMonitor = new OverlayMonitor(context);
  }

initGrid初始化显示选项参数常量

    //参数gridName,获取指定gridName的显示选项,该值在device_profile.xml中定义
    private String initGrid(Context context, String gridName) {
        //获取系统窗口管理器
        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        Display display = wm.getDefaultDisplay(); //获取默认的Display,里面有屏幕相关的信息
      
        //描述有关显示器的常规信息(如其大小、密度和字体缩放)的结构体
        DisplayMetrics dm = new DisplayMetrics();
        display.getMetrics(dm);

        Point smallestSize = new Point();
        Point largestSize = new Point();
        //返回应用程序在正常操作下可能遇到的显示大小范围,只要屏幕大小没有物理变化。这基本上是随着方向的变化而看到的大小,考虑到每次旋转中的任何屏幕装饰。例如,状态栏始终位于屏幕顶部,因此它将降低横向和纵向的高度,此处返回的最小高度将是两者中较小的高度。这旨在让应用程序了解它们在经历设备旋转时将遇到的大小范围,以便通过旋转提供稳定的 UI。此处的尺寸考虑了所有标准系统装饰,这些装饰减小了应用程序实际可用的尺寸:状态栏,导航栏,系统栏等。它没有考虑更多瞬态元素,如 IME 软键盘。
      //例如在240*320的屏幕机器上,打印如下smallestSize=Point(240, 219), largestSize=Point(320, 299),也就是高度被状态栏占了一部分21像素,所以为219和299
        display.getCurrentSizeRange(smallestSize, largestSize);

        //加载并解析device_profiles.xml文件,见下面"二"
        ArrayList<DisplayOption> allOptions = getPredefinedDeviceProfiles(context, gridName);
        // This guarantees that width < height
        float minWidthDps = Utilities.dpiFromPx(Math.min(smallestSize.x, smallestSize.y), dm);
        float minHeightDps = Utilities.dpiFromPx(Math.min(largestSize.x, largestSize.y), dm);
        // 对DiaplayOption进行排序,依据从窗口管理器中获取的最小宽高和每个DisplayOption中的最小宽高进行Math.hypot计算,选出最合适的显示选项
        Collections.sort(allOptions, (a, b) ->
                Float.compare(dist(minWidthDps, minHeightDps, a.minWidthDps, a.minHeightDps),
                        dist(minWidthDps, minHeightDps, b.minWidthDps, b.minHeightDps)));
        //依据算法纠正DiaplayOption数据,返回最合适的DiaplayOption
        DisplayOption interpolatedDisplayOption =
                invDistWeightedInterpolate(minWidthDps,  minHeightDps, allOptions);
        //上面已经做过排序,那么最接近的显示选项位于第0个位置
        GridOption closestProfile = allOptions.get(0).grid; //获取行列,hotseat设置
        numRows = closestProfile.numRows; //行数
        numColumns = closestProfile.numColumns; //列数
        numHotseatIcons = closestProfile.numHotseatIcons; //hotseat图标个数
       defaultLayoutId = closestProfile.defaultLayoutId; //hotseat图标定义文件
       demoModeLayoutId = closestProfile.demoModeLayoutId;
       numFolderRows = closestProfile.numFolderRows;   //图标文件夹行数
       numFolderColumns = closestProfile.numFolderColumns;  //图标文件夹列数
       mExtraAttrs = closestProfile.extraAttrs;
        
      //再次判断closestProfile是否与gridName对应
       if (!closestProfile.name.equals(gridName)) {
            //如果gridname发生变化,则重新保存到shareprefs
            Utilities.getPrefs(context).edit()
                   .putString(mIdpGridKey, closestProfile.name).apply();
        }
       if (mMonitor.getSRController() != null) {
           mMonitor.getSRController().saveGridNameIntoStorage(context, closestProfile.name);
       }

       iconSize = interpolatedDisplayOption.iconSize; //图标尺寸
       iconShapePath = getIconShapePath(context);  //
       landscapeIconSize = interpolatedDisplayOption.landscapeIconSize; //横屏图标尺寸
       iconBitmapSize = ResourceUtils.pxFromDp(iconSize, dm);
       iconTextSize = interpolatedDisplayOption.iconTextSize; //字体大小
       fillResIconDpi = getLauncherIconDensity(iconBitmapSize);
       // 如果有APK包含grid参数设置,在这里可以应用,支持覆盖的参数有: numRows, numColumns, iconSize
       applyPartnerDeviceProfileOverrides(context, dm);

       Point realSize = new Point();
       display.getRealSize(realSize); //获取屏幕实际尺寸,例如还是240*320的屏幕,获取到的是240*320
       // 实际大小永远不会改变。小边和大边在任何方向上都将保持不变。
       int smallSide = Math.min(realSize.x, realSize.y);
       int largeSide = Math.max(realSize.x, realSize.y);

      //横屏参数
       landscapeProfile = new DeviceProfile(context, this, smallestSize, largestSize,
               largeSide, smallSide, true /* isLandscape */, false /* isMultiWindowMode */);
        //竖屏参数      
        portraitProfile = new DeviceProfile(context, this, smallestSize, largestSize,
               smallSide, largeSide, false /* isLandscape */, false /* isMultiWindowMode */);

       FolderIconController fic = mMonitor.getFolderIconController();
       if (fic != null) {
           fic.backupOriginalFolderRowAndColumns(numFolderRows, numFolderColumns);
           fic.updateFolderRowAndColumns(this);
       }

       //我们需要确保壁纸中有足够的额外空间用于预期的视差效果
       if (context.getResources().getConfiguration().smallestScreenWidthDp >= 720) {
           defaultWallpaperSize = new Point(
                   (int) (largeSide * wallpaperTravelToScreenWidthRatio(largeSide, smallSide)),
                   largeSide);
       } else {
           defaultWallpaperSize = new Point(Math.max(smallSide * 2, largeSide), largeSide);
       }

       ComponentName cn = new ComponentName(context.getPackageName(), getClass().getName());
       defaultWidgetPadding = AppWidgetHostView.getDefaultPaddingForWidget(context, cn, null);

       mDisplayOptionName = interpolatedDisplayOption.name;
       mGridName = closestProfile.name;
       return closestProfile.name;
   }

getPredefinedDeviceProfiles

    //参数gridName,显示是要获取指定gridName的显示参数
    static ArrayList<DisplayOption> getPredefinedDeviceProfiles(Context context, String gridName) {
        ArrayList<DisplayOption> profiles = new ArrayList<>();
        try (XmlResourceParser parser = context.getResources().getXml(R.xml.device_profiles)) {
            final int depth = parser.getDepth();
            int type;
            while (((type = parser.next()) != XmlPullParser.END_TAG ||
                    parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
                if ((type == XmlPullParser.START_TAG)
                        && GridOption.TAG_NAME.equals(parser.getName())) {

                    GridOption gridOption = new GridOption(context, Xml.asAttributeSet(parser));
                    final int displayDepth = parser.getDepth();
                    while (((type = parser.next()) != XmlPullParser.END_TAG ||
                            parser.getDepth() > displayDepth)
                            && type != XmlPullParser.END_DOCUMENT) {
                        if ((type == XmlPullParser.START_TAG) && "display-option".equals(
                                parser.getName())) {
                            profiles.add(new DisplayOption(
                                    gridOption, context, Xml.asAttributeSet(parser)));
                        }
                    }
                }
            }
        } catch (IOException|XmlPullParserException e) {
            throw new RuntimeException(e);
        }

        //根据gridName进行过滤
        ArrayList<DisplayOption> filteredProfiles = new ArrayList<>();
        if (!TextUtils.isEmpty(gridName)) {
            for (DisplayOption option : profiles) {
                if (gridName.equals(option.grid.name)) {
                    filteredProfiles.add(option);
                }
            }
        }
        if (filteredProfiles.isEmpty()) {
            // No grid found, use the default options
            for (DisplayOption option : profiles) {
                if (option.canBeDefault) {
                    filteredProfiles.add(option);
                }
            }
        }
        if (filteredProfiles.isEmpty()) {
            throw new RuntimeException("No display option with canBeDefault=true");
        }
        return filteredProfiles;
    }                                                                                                     

invDistWeightedInterpolate

调用这个方法时传进去的参数是当前手机真实的宽和高,以及经过排序后得到的与目标匹配度由高到低的profiles集合。具体的操作在代码中进行了注解,其实,很多手机型号一致的,计算的时候不算多,有些许差别,计算出来的偏差值也不多,所以这个偏差值纠正就分析到这里。

  static DisplayOption invDistWeightedInterpolate(float width, float height,
              ArrayList<DisplayOption> points) {
      float weights = 0;

      DisplayOption p = points.get(0);
      if (dist(width, height, p.minWidthDps, p.minHeightDps) == 0) {
          return p;
      }

      DisplayOption out = new DisplayOption();
      out.name = "";
      for (int i = 0; i < points.size() && i < KNEARESTNEIGHBOR; ++i) {
          p = points.get(i);
          float w = weight(width, height, p.minWidthDps, p.minHeightDps, WEIGHT_POWER);
          weights += w;
          out.add(new DisplayOption().add(p).multiply(w));
          if (!TextUtils.isEmpty(out.name)) {
              out.name += "&";
          }
          out.name += "<" + (p.grid != null ? p.grid.name : "not found") + "," + p.name + ">";
      }
      return out.multiply(1.0f / weights);
  }

//获取默认的gridname,在该版本中返回的是空值
 private String getDefaultGridName(Context context) {
   String defaultGridName = "";

   DesktopGridController gridController = mMonitor.getDesktopGridController();
   if (gridController != null) {
       defaultGridName = gridController.getDefaultGridName();

       if (mMonitor.getSRController() != null
               && !gridController.isGridNameValid(defaultGridName)) {
           if (LogUtils.DEBUG) {
               LogUtils.d(TAG, "The default grid name configured is invalid:" + defaultGridName);
           }
           defaultGridName = mMonitor.getSRController().getGridNameFromStorage(context);
       }

       if (LogUtils.DEBUG) {
           LogUtils.d(TAG, "getDefaultGridName: defaultGridName = " + defaultGridName);
       }
   }

   return defaultGridName;

}

GridOption和DiaplayOption

  private static final class DisplayOption {
      private final GridOption grid;

      private String name;
      private final float minWidthDps;
      private final float minHeightDps;
      private final boolean canBeDefault;

      private float iconSize;
      private float landscapeIconSize;
      private float iconTextSize;
    
      ...
}

   public static final class GridOption {

       public static final String TAG_NAME = "grid-option";

       public final String name;
       public final int numRows;
       public final int numColumns;

       private final int numFolderRows;
       private final int numFolderColumns;

       private final int numHotseatIcons;

       private final int defaultLayoutId;
       private final int demoModeLayoutId;

       private final SparseArray<TypedValue> extraAttrs;

##com.android.launcher3.prefs.xml
这是Launcher3应用主要的sharedprefs文件,作为分析时的参考
`/data/data/com.android.launcher3/shared_prefs/com.android.launcher3.prefs.xml`
```                                                                             <
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <int name="launcher.home_bounce_count" value="4" />
    <string name="migration_src_workspace_size">2,2</string>
    <boolean name="pref_add_icon_to_home" value="true" />
    <int name="migration_src_hotseat_count" value="0" />
    <boolean name="pref_add_icon_to_home_initialized" value="true" />
    <boolean name="sl_EMPTY_DATABASE_CREATED" value="true" />
    <int name="migration_src_density_dpi" value="140" />
    <string name="idp_grid_name">3_by_3</string>
</map>

MultiModeController.java

packages/apps/Launcher3/src/com/sprd/ext/multimode/MultiModeController.java

import static com.android.launcher3.LauncherProvider.SCHEMA_VERSION; //28

   public static String getKeyByMode(Context context, String originalKey) {
       return getKeyByMode(context, originalKey, SCHEMA_VERSION, isSingleLayerMode(context));
   }

    //参数:key, 数据库版本,是单层还是双层显示
   public static String getKeyByMode(
           Context context, String originalKey, int dbVersion, boolean isSingleLayerMode) {
       initControllerIfNeeded(context);
       return MultiModeUtilities.getKeyByMode(originalKey, isSingleLayerMode, dbVersion);
   }

   public static String getKeyByPreMode(Context context, String originalKey) {
       return getKeyByMode(context, originalKey, SCHEMA_VERSION, !isSingleLayerMode(context));
   }

    public static boolean isSingleLayerMode() {
        throwIfControllerNotInited();
        return sIsSingleLayerMode; //返回变量sIsSingleLayerMode
    }
    public static void setSingleLayerMode(Context context, boolean isSingleMode) {
        MultiModeUtilities.syncSaveNewModel(context,
                isSingleMode ? MultiModeUtilities.SINGLE : MultiModeUtilities.DUAL);
        //sIsSingleLayerMode在构造方法中赋值
        sIsSingleLayerMode = MultiModeUtilities.isSingleLayerMode(context);
        sIsDefaultMode = MultiModeUtilities.isDefaultMode(context);
    }
   public MultiModeController(Context context, LauncherAppMonitor monitor) {
       super(context);
        //是否支持动态显示
       sIsSupportDynamicChange = MultiModeUtilities.isSupportDynamicHomeStyle(context);
        //是否是单层显示
       sIsSingleLayerMode = MultiModeUtilities.isSingleLayerMode(context);
       sIsDefaultMode = MultiModeUtilities.isDefaultMode(context);
       
       monitor.registerCallback(mAppMonitorCallback);
       LogUtils.d(TAG, this);
   }

    //是否支持动态变化
   public static boolean isSupportDynamicChange() {
       throwIfControllerNotInited();
       return sIsSupportDynamicChange;
   }
   public static void setSupportDynamicChange(boolean isSupport) {
       sIsSupportDynamicChange = isSupport;
   }

MultiModeUtilities

packages/apps/Launcher3/src/com/sprd/ext/multimode/MultiModeUtilities.java

   static final String SINGLE = "single";
   static final String DUAL = "dual";
   static final String SL_PREFIX = "sl_";
    //判断是否是单层显示
   static boolean isSingleLayerMode(Context context) {
       return FeatureOption.SPRD_MULTI_MODE_SUPPORT.get()
               && SINGLE.equals(getHomeScreenStylePrefValue(context));
   }
    //在sharepref中获取显示风格的值
   static String getHomeScreenStylePrefValue(Context context) {
       if (context == null) {
           return DUAL;
       }
       Resources res = context.getResources();
       return Utilities.getPrefs(context)
               .getString(LauncherSettingsExtension.PREF_HOME_SCREEN_STYLE_KEY,
                       res.getString(R.string.default_home_screen_style)); 
              //默认值为R.string.default_home_screen_style
   }


    // We add SL_PREFIX only single layer mode & support dynamic change.
    static String getKeyByMode(String originalKey, boolean isSingleMode, int dbVersion) {
        if (TextUtils.isEmpty(originalKey)) {
            throw new RuntimeException("getKeyByMode() the original key is empty!");
        }

        String outKey = originalKey;
        //如果支持多模式,再进行判断,如果不支持,则使用双层模式
        //我修改了show_home_screen_style_settings的值为false,outKey=idp_grid_name
        if (MultiModeController.isSupportDynamicChange()) {
            switch (dbVersion) {
                case 27:
                    // Android P db version is 27.
                    outKey = isSingleMode ? SINGLE + "_" + originalKey : DUAL + "_" + originalKey;
                    break;
                default:
                    //我的设备是单层显示,所以返回的是sl_idp_grid_name
                    outKey = isSingleMode ? SL_PREFIX + originalKey : originalKey;
                    break;

            }
        }
        return outKey;
    }

  //是否支持动态显示
   static boolean isSupportDynamicHomeStyle(Context context) {
       return FeatureOption.SPRD_MULTI_MODE_SUPPORT.get()
               && context.getResources().getBoolean(R.bool.show_home_screen_style_settings);
   }

LauncherSettingsExtension

packages/apps/Launcher3/src/com/sprd/ext/LauncherSettingsExtension.java

 public static final String PREF_HOME_SCREEN_STYLE_KEY = "pref_home_screen_style";

FeatureOption

packages/apps/Launcher3/src/com/sprd/ext/FeatureOption.java

  public static final TogglableFlag SPRD_MULTI_MODE_SUPPORT = new TogglableFlag(
          "SPRD_MULTI_MODE_SUPPORT", getProp("ro.launcher.multimode"),
          "enable user can select user aosp mode or singlelayer mode");
  public static final TogglableFlag SPRD_DESKTOP_GRID_SUPPORT = new TogglableFlag(
          "SPRD_DESKTOP_GRID_SUPPORT", getProp("ro.launcher.desktopgrid"),
          "enable allows customization of the columns and rows on the desktop");

config_ext.xml

packages/apps/Launcher3/res/values/config_ext.xml

    //从上面分析可以得出结论
    <!--default value of launcher style allapps or singlelayer 是否显示单层/双层设置显示-->
    <bool name="show_home_screen_style_settings">false</bool>
    <!--The value must be dual or single 默认为单层还是双层-->
    <string name="default_home_screen_style" translatable="false">dual</string>


device_profiles.xml

位置:packages/apps/Launcher3/res/xml/device_profiles.xml

<profiles xmlns:launcher="http://schemas.android.com/apk/res-auto" >

    <grid-option
        launcher:name="2_by_2"
        launcher:numRows="2"
        launcher:numColumns="2"
        launcher:numFolderRows="2"
        launcher:numFolderColumns="2"
        launcher:numHotseatIcons="3" 
        launcher:defaultLayoutId="@xml/default_workspace_3x3" >

        <display-option
            launcher:name="Super Short Stubby"
            launcher:minWidthDps="255"
            launcher:minHeightDps="300"
            launcher:iconImageSize="48"
            launcher:iconTextSize="13.0"
            launcher:canBeDefault="true" />

        <display-option
            launcher:name="Shorter Stubby"
            launcher:minWidthDps="255"
            launcher:minHeightDps="400"
            launcher:iconImageSize="48"
            launcher:iconTextSize="13.0"
            launcher:canBeDefault="true" />

    </grid-option>
    <grid-option
        launcher:name="3_by_3"
        launcher:numRows="3"
        launcher:numColumns="3"
        launcher:numFolderRows="2"
        launcher:numFolderColumns="3"
        launcher:numHotseatIcons="3"
        launcher:defaultLayoutId="@xml/default_workspace_3x3" >

        <display-option
            launcher:name="Super Short Stubby"
            launcher:minWidthDps="255"
            launcher:minHeightDps="300"
            launcher:iconImageSize="48"
            launcher:iconTextSize="13.0"
            launcher:canBeDefault="true" />

        <display-option
            launcher:name="Shorter Stubby"
            launcher:minWidthDps="255"
            launcher:minHeightDps="400"
            launcher:iconImageSize="48"
            launcher:iconTextSize="13.0"
            launcher:canBeDefault="true" />

    </grid-option>

    <grid-option
        launcher:name="4_by_4"
        launcher:numRows="4"
        launcher:numColumns="4"
        launcher:numFolderRows="3"
        launcher:numFolderColumns="4"
        launcher:numHotseatIcons="4"
        launcher:defaultLayoutId="@xml/default_workspace_4x4" >

        <display-option
            launcher:name="Short Stubby"
            launcher:minWidthDps="275"
            launcher:minHeightDps="420"
            launcher:iconImageSize="48"
            launcher:iconTextSize="13.0"
            launcher:canBeDefault="true" />

        <display-option
            launcher:name="Stubby"
            launcher:minWidthDps="255"
            launcher:minHeightDps="450"
            launcher:iconImageSize="48"
            launcher:iconTextSize="13.0"
            launcher:canBeDefault="true" />

        <display-option
            launcher:name="Nexus S"
            launcher:minWidthDps="296"
            launcher:minHeightDps="491.33"
            launcher:iconImageSize="48"
            launcher:iconTextSize="13.0"
            launcher:canBeDefault="true" />

        <display-option
            launcher:name="Nexus 4"
            launcher:minWidthDps="359"
            launcher:minHeightDps="567"
            launcher:iconImageSize="54"
            launcher:iconTextSize="13.0"
            launcher:canBeDefault="true" />

        <display-option
            launcher:name="Nexus 5"
            launcher:minWidthDps="335"
            launcher:minHeightDps="567"
            launcher:iconImageSize="54"
            launcher:iconTextSize="13.0"
            launcher:canBeDefault="true" />

    </grid-option>
</profiles>

参考

Launcher 8.0源码
Android10 launcher启动流程
Android10.0系统启动之Launcher(桌面)启动流程

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容