Android 12.0 第三方应用左右两侧未全屏有黑边问题解决

发布于:2025-06-15 ⋅ 阅读:(17) ⋅ 点赞:(0)

1.前言

在12.0的系统rom定制化中,在某些时候安装第三方应用,突然会发现应用左右两侧为全屏有黑边显示,所以这时候需要进行
优化,看问题出在哪,然后解决这个问题,接下来分析下相关问题

2.第三方应用左右两侧未全屏有黑边问题解决的核心类

 frameworks\base\core\java\android\content\pm\parsing\component\ParsedActivityUtils.java

3.第三方应用左右两侧未全屏有黑边问题解决的核心功能分析和实现

在android7.0以后,在AndroidManifest.xml中的
<application> 标签隐式添加一个属性,android:resizeableActivity=“true”, 该属性的作用后面将详细说明。
在 标签中增加属性:android:resizeableActivity=“false”,同时在节点下增加一个meta-data标签:

原理说明

在Android 7.0(API 级别 24)或更高版本的应用,android:resizeableActivity属性默认为true(对应适配方式1)。这个属性是控制多窗口显示的,决定当前的应用或者Activity是否支持多窗口。
可以在清单的<activity>或 <application>节点中设置该属性,启用或禁用多窗口显示,配置如下:
android:resizeableActivity=[“true” | “false”]

如果该属性设置为 true,Activity 将能以分屏和自由形状模式启动。 如果此属性设置为 false,Activity 将不支持多窗口模式。 如果该值为 false,且用户尝试在多窗口模式下启动 Activity,该 Activity 将全屏显示。

适配方式2即为设置屏幕的最大长宽比,这是官方提供的设置方式。
如果设置了最大长宽比,必须android:resizeableActivity=“false”。 否则最大长宽比没有任何作用

这是android12开始支持了多窗口和分屏等功能,对一些低版本sdk的应用兼容性有了调整,如果不想改变targetsdk到更高,也可以为Activity增加resizeableActivity属性,同样可以达到去除黑边效果。

   <activity
        android:name=".YourActivity"
        android:resizeableActivity="true">
    </activity>

以上两种解决办法适用于自己开发的应用,但对应腾讯视频这种第三方应用就只能从系统层面去解决了。
接下来看下第三方应用如何处理两边黑屏的问题

 ParsedActivityUtils 是Android系统中用于解析 AndroidManifest.xml 文件中的一个类,主要负责解析Activity、Service、Receiver等组件的信息。‌
功能和作用

ParsedActivityUtils类在Android系统中主要用于解析AndroidManifest.xml文件中的Activity、Service和Receiver等组件的信息。它通过读取XML文件中的标签和属性,将这些信息转换为Java对象,以便系统能够正确地管理和调用这些组件。具体功能包括:

    ‌解析Activity信息‌:读取Activity的标签、名称、启动模式等信息。
    ‌解析Service信息‌:读取Service的标签、名称、启动方式等信息。
    ‌解析Receiver信息‌:读取Receiver的标签、名称、接收的Intent等信息。

3.1ParsingPackageUtils.java的相关解析代码分析

    public class ParsingPackageUtils {
    public ParsingPackageUtils(boolean onlyCoreApps, String[] separateProcesses,
                  DisplayMetrics displayMetrics, @NonNull Callback callback) {
              mOnlyCoreApps = onlyCoreApps;
              mSeparateProcesses = separateProcesses;
              mDisplayMetrics = displayMetrics;
              mCallback = callback;
          }

 在ParsingPackageUtils的构造方法中,主要是构造callback参数,供解析的时候回调参数使用的,接下来主要看解析的最主要的方法parseBaseApplication
接下来看parseBaseApplication的具体解析方式方法

   private ParseResult<ParsingPackage> parseBaseApplication(ParseInput input,
                  ParsingPackage pkg, Resources res, XmlResourceParser parser, int flags)
                  throws XmlPullParserException, IOException {
              final String pkgName = pkg.getPackageName();
              int targetSdk = pkg.getTargetSdkVersion();
      
              TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestApplication);
              try {
                  // TODO(b/135203078): Remove this and force unit tests to mock an empty manifest
                  // This case can only happen in unit tests where we sometimes need to create fakes
                  // of various package parser data structures.
                  if (sa == null) {
                      return input.error("<application> does not contain any attributes");
                  }
      
                  String name = sa.getNonConfigurationString(R.styleable.AndroidManifestApplication_name,
                          0);
                  if (name != null) {
                      String packageName = pkg.getPackageName();
                      String outInfoName = ParsingUtils.buildClassName(packageName, name);
                      if (PackageManager.APP_DETAILS_ACTIVITY_CLASS_NAME.equals(outInfoName)) {
                          return input.error("<application> invalid android:name");
                      } else if (outInfoName == null) {
                          return input.error("Empty class name in package " + packageName);
                      }
      
                      pkg.setClassName(outInfoName);
                  }
      
....
      
                      TypedValue v = sa.peekValue(
                              R.styleable.AndroidManifestApplication_fullBackupContent);
                      int fullBackupContent = 0;
      
                      if (v != null) {
                          fullBackupContent = v.resourceId;
      
                          if (v.resourceId == 0) {
                              if (PackageParser.DEBUG_BACKUP) {
                                  Slog.v(TAG, "fullBackupContent specified as boolean=" +
                                          (v.data == 0 ? "false" : "true"));
                              }
                              // "false" => -1, "true" => 0
                              fullBackupContent = v.data == 0 ? -1 : 0;
                          }
      
                          pkg.setFullBackupContent(fullBackupContent);
                      }
                      if (PackageParser.DEBUG_BACKUP) {
                          Slog.v(TAG, "fullBackupContent=" + fullBackupContent + " for " + pkgName);
                      }
                  }
      
                  if (sa.getBoolean(R.styleable.AndroidManifestApplication_persistent, false)) {
                      // Check if persistence is based on a feature being present
                      final String requiredFeature = sa.getNonResourceString(R.styleable
                              .AndroidManifestApplication_persistentWhenFeatureAvailable);
                      pkg.setPersistent(requiredFeature == null || mCallback.hasFeature(requiredFeature));
                  }
     ......
      
                  String classLoaderName = pkg.getClassLoaderName();
                  if (classLoaderName != null
                          && !ClassLoaderFactory.isValidClassLoaderName(classLoaderName)) {
                      return input.error("Invalid class loader name: " + classLoaderName);
                  }
      
                  pkg.setGwpAsanMode(sa.getInt(R.styleable.AndroidManifestApplication_gwpAsanMode, -1));
              } finally {
                  sa.recycle();
              }
      
              boolean hasActivityOrder = false;
              boolean hasReceiverOrder = false;
              boolean hasServiceOrder = false;
              final int depth = parser.getDepth();
              int type;
              while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                      && (type != XmlPullParser.END_TAG
                      || parser.getDepth() > depth)) {
                  if (type != XmlPullParser.START_TAG) {
                      continue;
                  }
      
                  final ParseResult result;
                  String tagName = parser.getName();
                  boolean isActivity = false;
                  switch (tagName) {
                      case "activity":
                          isActivity = true;
                          // fall-through
                      case "receiver":
                          ParseResult<ParsedActivity> activityResult =
                                  ParsedActivityUtils.parseActivityOrReceiver(mSeparateProcesses, pkg,
                                          res, parser, flags, PackageParser.sUseRoundIcon, input);
      
                          if (activityResult.isSuccess()) {
                              ParsedActivity activity = activityResult.getResult();
                              if (isActivity) {
                                  hasActivityOrder |= (activity.getOrder() != 0);
                                  pkg.addActivity(activity);
                              } else {
                                  hasReceiverOrder |= (activity.getOrder() != 0);
                                  pkg.addReceiver(activity);
                              }
                          }
      
                          result = activityResult;
                          break;
                      case "service":
                          ParseResult<ParsedService> serviceResult =
                                  ParsedServiceUtils.parseService(mSeparateProcesses, pkg, res, parser,
                                          flags, PackageParser.sUseRoundIcon, input);
                          if (serviceResult.isSuccess()) {
                              ParsedService service = serviceResult.getResult();
                              hasServiceOrder |= (service.getOrder() != 0);
                              pkg.addService(service);
                          }
      
                          result = serviceResult;
                          break;
                      case "provider":
                          ParseResult<ParsedProvider> providerResult =
                                  ParsedProviderUtils.parseProvider(mSeparateProcesses, pkg, res, parser,
                                          flags, PackageParser.sUseRoundIcon, input);
                          if (providerResult.isSuccess()) {
                              pkg.addProvider(providerResult.getResult());
                          }
      
                          result = providerResult;
                          break;
                      case "activity-alias":
                          activityResult = ParsedActivityUtils.parseActivityAlias(pkg, res,
                                  parser, PackageParser.sUseRoundIcon, input);
                          if (activityResult.isSuccess()) {
                              ParsedActivity activity = activityResult.getResult();
                              hasActivityOrder |= (activity.getOrder() != 0);
                              pkg.addActivity(activity);
                          }
      
                          result = activityResult;
                          break;
                      default:
                          result = parseBaseAppChildTag(input, tagName, pkg, res, parser, flags);
                          break;
                  }
      
                  if (result.isError()) {
                      return input.error(result);
                  }
              }
      
              if (TextUtils.isEmpty(pkg.getStaticSharedLibName())) {
                  // Add a hidden app detail activity to normal apps which forwards user to App Details
                  // page.
                  ParseResult<ParsedActivity> a = generateAppDetailsHiddenActivity(input, pkg);
                  if (a.isError()) {
                      // Error should be impossible here, as the only failure case as of SDK R is a
                      // string validation error on a constant ":app_details" string passed in by the
                      // parsing code itself. For this reason, this is just a hard failure instead of
                      // deferred.
                      return input.error(a);
                  }
      
                  pkg.addActivity(a.getResult());
              }
      
              if (hasActivityOrder) {
                  pkg.sortActivities();
              }
              if (hasReceiverOrder) {
                  pkg.sortReceivers();
              }
              if (hasServiceOrder) {
                  pkg.sortServices();
              }
      
              // Must be run after the entire {@link ApplicationInfo} has been fully processed and after
              // every activity info has had a chance to set it from its attributes.
              setMaxAspectRatio(pkg);
              setMinAspectRatio(pkg);
              setSupportsSizeChanges(pkg);
      
              pkg.setHasDomainUrls(hasDomainURLs(pkg));
      
              return input.success(pkg);
          } 

从上述代码中,可以看出在parseBaseApplication中解析xml时可以对tag的值为activity,receiver,service,provider,activity-alias等几种类型的组件进行解析
在activity-alias这种tag,就是对activity的别类的解析,而这里也是今天实现默认启动Launcher的重点部分在解析activity的实现调用的是ParsedActivityUtils.parseActivityOrReceiver
进行解析的,最后代码还是由parseActivityOrAlias来负责解析的,所以接下来看这部分代码

3.2 ParsedActivityUtils相关解析源码分析

@NonNull
        @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
        public static ParseResult<ParsedActivity> parseActivityOrReceiver(String[] separateProcesses,
                ParsingPackage pkg, Resources res, XmlResourceParser parser, int flags,
                boolean useRoundIcon, ParseInput input)
                throws XmlPullParserException, IOException {
            final String packageName = pkg.getPackageName();
            final ParsedActivity
                    activity = new ParsedActivity();
            ...
             //int resizeMode = getActivityResizeMode(pkg, sa, screenOrientation);
             int resizeMode = ActivityInfo.RESIZE_MODE_RESIZEABLE; //固定开启resizeable属性
            ...
    }

ParseResult()方法中重写resizeMode值即可。这样就可以从系统层面解决所有应用显示黑边问题,包括安装的第三方应用。