深度解析Android Q cutout刘海屏

1.在 Settings/System/Developer options/DRAWING 里面看到有 “Display cutout” 菜单
2.根据这个字符串找到
packages/apps/Settings/res/values/strings.xml

<string name="display_cutout_emulation">Display cutout</string>

3.然后根据 display_cutout_emulation 字符串找到在 packages/apps/Settings/res/xml/development_settings.xml 文件中有使用

<ListPreference
    android:key="display_cutout_emulation"
    android:title="@string/display_cutout_emulation"
    settings:keywords="@string/display_cutout_emulation_keywords" />

4.根据 display_cutout_emulation 这个 key 找到
packages/apps/Settings/src/com/android/settings/development/EmulateDisplayCutoutPreferenceController.java

public class EmulateDisplayCutoutPreferenceController extends OverlayCategoryPreferenceController {

    private static final String KEY = "display_cutout_emulation";

    @VisibleForTesting
    EmulateDisplayCutoutPreferenceController(Context context, PackageManager packageManager,
            IOverlayManager overlayManager) {
        super(context, packageManager, overlayManager, DisplayCutout.EMULATION_OVERLAY_CATEGORY);
    }

    public EmulateDisplayCutoutPreferenceController(Context context) {
        this(context, context.getPackageManager(), IOverlayManager.Stub
                .asInterface(ServiceManager.getService(Context.OVERLAY_SERVICE)));
    }

    @Override
    public String getPreferenceKey() {
        return KEY;
    }
}

其中有个重要的参数
DisplayCutout.EMULATION_OVERLAY_CATEGORY 这个参数是用来过滤和匹配 cutout_emulation 模拟apk的

找到 DisplayCutout 类如下:
frameworks/base/core/java/android/view/DisplayCutout.java

public static final String EMULATION_OVERLAY_CATEGORY = "com.android.internal.display_cutout_emulation";

用 OpenGrok 找到代码中有如下几个应用是有这个 category 的
frameworks/base/packages/overlays/DisplayCutoutEmulationTallOverlay/AndroidManifest.xml frameworks/base/packages/overlays/DisplayCutoutEmulationWideOverlay/AndroidManifest.xml frameworks/base/packages/overlays/DisplayCutoutEmulationCornerOverlay/AndroidManifest.xml frameworks/base/packages/overlays/DisplayCutoutEmulationNarrowOverlay/AndroidManifest.xml frameworks/base/packages/overlays/DisplayCutoutEmulationDoubleOverlay/AndroidManifest.xml

具体使用代码如下

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.android.internal.display.cutout.emulation.tall"
        android:versionCode="1"
        android:versionName="1.0">
    <overlay android:targetPackage="android"
            android:category="com.android.internal.display_cutout_emulation"
            android:priority="1"/>

    <application android:label="@string/display_cutout_emulation_overlay" android:hasCode="false"/>
</manifest>

代码中的过滤方法在 packages/apps/Settings/src/com/android/settings/development/OverlayCategoryPreferenceController.java 类中,具体代码如下:

private List<OverlayInfo> getOverlayInfos() {
    final List<OverlayInfo> filteredInfos = new ArrayList<>();
    try {
        List<OverlayInfo> overlayInfos = mOverlayManager
                .getOverlayInfosForTarget(OVERLAY_TARGET_PACKAGE, USER_SYSTEM);
        for (OverlayInfo overlayInfo : overlayInfos) {
            if (mCategory.equals(overlayInfo.category)) {
                filteredInfos.add(overlayInfo);
            }
        }
    } catch (RemoteException re) {
        throw re.rethrowFromSystemServer();
    }
    filteredInfos.sort(OVERLAY_INFO_COMPARATOR);
    return filteredInfos;
}

找到 DisplayCutoutEmulationTallOverlay 对应代码
首先是 Android.mk 文件,这个文件没什么特别的,只有就 LOCAL_RRO_THEME 这个变量
frameworks/base/packages/overlays/DisplayCutoutEmulationTallOverlay/Android.mk

LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_RRO_THEME := DisplayCutoutEmulationCorner
LOCAL_PRODUCT_MODULE := true
LOCAL_SRC_FILES := $(call all-subdir-java-files)
LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
LOCAL_PACKAGE_NAME := DisplayCutoutEmulationCornerOverlay
LOCAL_SDK_VERSION := current
include $(BUILD_RRO_PACKAGE)

这个变量的定义在 build/make/core/build_rro_package.mk 文件中,代码如下

#############################################################################
## Standard rules for installing runtime resouce overlay APKs.
##
## Set LOCAL_RRO_THEME to the theme name if the package should apply only to
## a particular theme as set by ro.boot.vendor.overlay.theme system property.
##
## If LOCAL_RRO_THEME is not set, the package will apply always, independent
## of themes.
##
#############################################################################

LOCAL_IS_RUNTIME_RESOURCE_OVERLAY := true

ifneq ($(strip $(LOCAL_SRC_FILES)),)
  $(error runtime resource overlay package should not contain sources)
endif

partition :=
ifeq ($(strip $(LOCAL_ODM_MODULE)),true)
  partition := $(TARGET_OUT_ODM)
else ifeq ($(strip $(LOCAL_VENDOR_MODULE)),true)
  partition := $(TARGET_OUT_VENDOR)
else ifeq ($(strip $(LOCAL_PRODUCT_SERVICES_MODULE)),true)
  partition := $(TARGET_OUT_PRODUCT_SERVICES)
else
  partition := $(TARGET_OUT_PRODUCT)
endif

ifeq ($(LOCAL_RRO_THEME),)
  LOCAL_MODULE_PATH := $(partition)/overlay
else
  LOCAL_MODULE_PATH := $(partition)/overlay/$(LOCAL_RRO_THEME)
endif

partition :=

include $(BUILD_SYSTEM)/package.mk

另一个是 config.xml 文件,这个文件是定义刘海屏的参数的
frameworks/base/packages/overlays/DisplayCutoutEmulationTallOverlay/res/values/config.xml

<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
    <string translatable="false" name="config_mainBuiltInDisplayCutout">
        M 0,0
        L -48, 0
        L -44.3940446283, 36.0595537175
        C -43.5582133885, 44.4178661152 -39.6, 48.0 -31.2, 48.0
        L 31.2, 48.0
        C 39.6, 48.0 43.5582133885, 44.4178661152 44.3940446283, 36.0595537175
        L 48, 0
        Z
        @dp
    </string>
    <string translatable="false" name="config_mainBuiltInDisplayCutoutRectApproximation">@*android:string/config_mainBuiltInDisplayCutout</string>
    <bool name="config_fillMainBuiltInDisplayCutout">true</bool>
    <dimen name="status_bar_height_portrait">48dp</dimen>
    <dimen name="status_bar_height_landscape">28dp</dimen>
    <dimen name="quick_qs_offset_height">48dp</dimen>
    <dimen name="quick_qs_total_height">176dp</dimen>
</resources>

其中 config_mainBuiltInDisplayCutout 就是刘海屏的 svg 代码

5.但是 EmulateDisplayCutoutPreferenceController 文件中并没有相关的设置逻辑代码,那我猜想应该是在父类中处理的,于是找到 packages/apps/Settings/src/com/android/settings/development/OverlayCategoryPreferenceController.java

看到 onPreferenceChange 中是调用 setOverlay() 方法去设置水滴屏效果的

private boolean setOverlay(String packageName) {
    final String currentPackageName = getOverlayInfos().stream()
            .filter(info -> info.isEnabled())
            .map(info -> info.packageName)
            .findFirst()
            .orElse(null);

    if (PACKAGE_DEVICE_DEFAULT.equals(packageName) && TextUtils.isEmpty(currentPackageName)
            || TextUtils.equals(packageName, currentPackageName)) {
        // Already set.
        return true;
    }

    new AsyncTask<Void, Void, Boolean>() {
        @Override
        protected Boolean doInBackground(Void... params) {
            try {
                if (PACKAGE_DEVICE_DEFAULT.equals(packageName)) {
                    return mOverlayManager.setEnabled(currentPackageName, false, USER_SYSTEM);
                } else {
                    return mOverlayManager.setEnabledExclusiveInCategory(packageName,
                            USER_SYSTEM);
                }
            } catch (RemoteException re) {
                Log.w(TAG, "Error enabling overlay.", re);
                return false;
            }
        }

        @Override
        protected void onPostExecute(Boolean success) {
            updateState(mPreference);
            if (!success) {
                Toast.makeText(
                        mContext, R.string.overlay_toast_failed_to_apply, Toast.LENGTH_LONG)
                        .show();
            }
        }
    }.execute();

    return true; // Assume success; toast on failure.
}

其中最关键的方法如下

mOverlayManager.setEnabledExclusiveInCategory(packageName, USER_SYSTEM);


mOverlayManager = IOverlayManager.Stub.asInterface(ServiceManager.getService(Context.OVERLAY_SERVICE))

找到对应的 IOverlayManager.aidl 文件如下
frameworks/base/core/java/android/content/om/IOverlayManager.aidl

找到对应的服务
frameworks/base/services/core/java/com/android/server/om/OverlayManagerService.java

找到里面的 setEnabledExclusiveInCategory()方法实现,发现最终是 mImpl.setEnabledExclusive() 方法的

@Override
public boolean setEnabledExclusiveInCategory(@Nullable String packageName, int userId)
        throws RemoteException {
    try {
        traceBegin(TRACE_TAG_RRO, "OMS#setEnabledExclusiveInCategory " + packageName);
        enforceChangeOverlayPackagesPermission("setEnabledExclusiveInCategory");
        userId = handleIncomingUser(userId, "setEnabledExclusiveInCategory");
        if (packageName == null) {
            return false;
        }

        final long ident = Binder.clearCallingIdentity();
        try {
            synchronized (mLock) {
                return mImpl.setEnabledExclusive(packageName, true /* withinCategory */,
                        userId);
            }
        } finally {
            Binder.restoreCallingIdentity(ident);
        }
    } finally {
        traceEnd(TRACE_TAG_RRO);
    }
}

找到最终的实现类 OverlayManagerServiceImpl
frameworks/base/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java

看到 setEnabledExclusive() 方法如下

boolean setEnabledExclusive(@NonNull final String packageName, boolean withinCategory,
        final int userId) {
    if (DEBUG) {
        Slog.d(TAG, String.format("setEnabledExclusive packageName=%s"
                + " withinCategory=%s userId=%d", packageName, withinCategory, userId));
    }

    final PackageInfo overlayPackage = mPackageManager.getPackageInfo(packageName, userId);
    if (overlayPackage == null) {
        return false;
    }

    try {
        final OverlayInfo oi = mSettings.getOverlayInfo(packageName, userId);
        final String targetPackageName = oi.targetPackageName;

        List<OverlayInfo> allOverlays = getOverlayInfosForTarget(targetPackageName, userId);

        boolean modified = false;

        // Disable all other overlays.
        allOverlays.remove(oi);
        for (int i = 0; i < allOverlays.size(); i++) {
            final String disabledOverlayPackageName = allOverlays.get(i).packageName;
            final PackageInfo disabledOverlayPackageInfo = mPackageManager.getPackageInfo(
                    disabledOverlayPackageName, userId);
            if (disabledOverlayPackageInfo == null) {
                modified |= mSettings.remove(disabledOverlayPackageName, userId);
                continue;
            }

            if (disabledOverlayPackageInfo.isStaticOverlayPackage()) {
                // Don't touch static overlays.
                continue;
            }
            if (withinCategory && !Objects.equals(disabledOverlayPackageInfo.overlayCategory,
                    oi.category)) {
                // Don't touch overlays from other categories.
                continue;
            }

            // Disable the overlay.
            modified |= mSettings.setEnabled(disabledOverlayPackageName, userId, false);
            modified |= updateState(targetPackageName, disabledOverlayPackageName, userId, 0);
        }

        // Enable the selected overlay.
        modified |= mSettings.setEnabled(packageName, userId, true);
        modified |= updateState(targetPackageName, packageName, userId, 0);

        if (modified) {
            mListener.onOverlaysChanged(targetPackageName, userId);
        }
        return true;
    } catch (OverlayManagerSettings.BadKeyException e) {
        return false;
    }
}

从注释中可以看到会先去 Disable all other overlays 然后再 Enable the selected overlay,调用的是 mSettings.setEnabled() 方法

关键的方法是 mListener.onOverlaysChanged(targetPackageName, userId);
这个方法的具体实现在 OverlayManagerService.java 文件中,代码如下:

private final class OverlayChangeListener
        implements OverlayManagerServiceImpl.OverlayChangeListener {
    @Override
    public void onOverlaysChanged(@NonNull final String targetPackageName, final int userId) {
        schedulePersistSettings();
        FgThread.getHandler().post(() -> {
            updateAssets(userId, targetPackageName);

            final Intent intent = new Intent(Intent.ACTION_OVERLAY_CHANGED,
                    Uri.fromParts("package", targetPackageName, null));
            intent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);

            if (DEBUG) {
                Slog.d(TAG, "send broadcast " + intent);
            }

            try {
                ActivityManager.getService().broadcastIntent(null, intent, null, null, 0,
                        null, null, null, android.app.AppOpsManager.OP_NONE, null, false, false,
                        userId);
            } catch (RemoteException e) {
                // Intentionally left empty.
            }
        });
    }
}

关键方法

ActivityManager.getService().broadcastIntent(null, intent, null, null, 0, null, null, null, android.app.AppOpsManager.OP_NONE, null, false, false, userId);

frameworks/base/packages/overlays/Android.mk 文件中看到 overlays 包下所有的 Overlay 都是在 frameworks-base-overlays 模块中的,如果要移除这些 overlay 模块只需要把 frameworks-base-overlays 模块移除即可

LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)

LOCAL_MODULE := frameworks-base-overlays
LOCAL_REQUIRED_MODULES := \
	AccentColorBlackOverlay \
	AccentColorCinnamonOverlay \
	AccentColorOceanOverlay \
	AccentColorOrchidOverlay \
	AccentColorSpaceOverlay \
	AccentColorGreenOverlay \
	AccentColorPurpleOverlay \
	DisplayCutoutEmulationCornerOverlay \
	DisplayCutoutEmulationDoubleOverlay \
	DisplayCutoutEmulationTallOverlay \
	FontNotoSerifSourceOverlay \
	IconPackCircularAndroidOverlay \
	IconPackCircularLauncherOverlay \
	IconPackCircularSettingsOverlay \
	IconPackCircularSystemUIOverlay \
	IconPackCircularThemePickerOverlay \
	IconPackFilledAndroidOverlay \
	IconPackFilledLauncherOverlay \
	IconPackFilledSettingsOverlay \
	IconPackFilledSystemUIOverlay \
	IconPackFilledThemePickerOverlay \
	IconPackRoundedAndroidOverlay \
	IconPackRoundedLauncherOverlay \
	IconPackRoundedSettingsOverlay \
	IconPackRoundedSystemUIOverlay \
	IconPackRoundedThemePickerUIOverlay \
	IconShapeRoundedRectOverlay \
	IconShapeSquareOverlay \
	IconShapeSquircleOverlay \
	IconShapeTeardropOverlay \
	NavigationBarMode3ButtonOverlay \
	NavigationBarMode2ButtonOverlay \
	NavigationBarModeGesturalOverlay \
	NavigationBarModeGesturalOverlayNarrowBack \
	NavigationBarModeGesturalOverlayWideBack \
	NavigationBarModeGesturalOverlayExtraWideBack

include $(BUILD_PHONY_PACKAGE)
include $(CLEAR_VARS)

LOCAL_MODULE := frameworks-base-overlays-debug

include $(BUILD_PHONY_PACKAGE)
include $(call first-makefiles-under,$(LOCAL_PATH))

frameworks-base-overlays 这个模块的控制在 build/make/target/product/handheld_product.mk文件中

$(call inherit-product, $(SRC_TARGET_DIR)/product/media_product.mk)

# /product packages
PRODUCT_PACKAGES += \
    Calendar \
    Camera2 \
    Contacts \
    DeskClock \
    Email \
    Gallery2 \
    LatinIME \
    Launcher3QuickStep \
    Music \
    OneTimeInitializer \
    Provision \
    QuickSearchBox \
    Settings \
    SettingsIntelligence \
    StorageManager \
    SystemUI \
    WallpaperCropper \
    frameworks-base-overlays

PRODUCT_PACKAGES_DEBUG += \
    frameworks-base-overlays-debug

以上是 RRO 刘海屏的相关代码流程,如果要用 SRO 只需要参考壁纸等的 overlay 方法即可,在我们的展锐项目上代码如下,关键是 config_mainBuiltInDisplayCutout 这个刘海屏 svg 图
device/sprd/pike2/xxx/yyy/frameworks/base/core/res/res/values/config.xml

<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
    <string translatable="false" name="config_mainBuiltInDisplayCutout">
        M 0,0
        L -33, 0
        L 0, 22
        L 33, 0
        Z
        @dp
    </string>
    <string translatable="false" name="config_mainBuiltInDisplayCutoutRectApproximation">@*android:string/config_mainBuiltInDisplayCutout</string>
    <!-- Whether the display cutout region of the main built-in display should be forced to
         black in software (to avoid aliasing or emulate a cutout that is not physically existent).
     -->
    <bool name="config_fillMainBuiltInDisplayCutout">true</bool>
</resources>

然后把这个 overlay 路径添加到 device/sprd/pike2/xxx/sp7731e_1h10_native.mk 文件

DEVICE_PACKAGE_OVERLAYS += $(BOARDDIR)/overlay_SP652_AH6521_Mobicel

因为我们项目上的刘海屏是一个小水滴,所以我把 config_mainBuiltInDisplayCutout 这个 svg 用了一个近似三角形的路径进行替换

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值