Android 车载应用开发与分析(5) - CarLauncher

简介:

在之前的Android车载应用开发与分析(1) - Android Automotive概述与编译中了解了如何下载以及编译面向车载IVI的Android系统,一切顺利的话,运行模拟器,等待启动动画播放完毕后,我们所能看到的第一个APP就是车载android的桌面,而这就是本篇文章的重点 - CarLauncher。

本篇文章以解析Android 11 源码中CarLauncher为主。为了便于阅读源码,现将CarLauncher的源码整理成可以导入Android Studio的结构,源码地址:https://github.com/linux-link/CarLauncher。由于`CarLauncher`对于源码存在依赖,该项目不能直接运行,引入jar依赖的方式也不完全正确,仅供阅读使用。

本篇文章中的功能以及源码分析基于android-11.0.0_r43,CarLauncher源码位于 packages/apps/Car/Launcher

1.Launcher 与 CarLauncher

Launcher是安卓系统中的桌面启动器,安卓系统的桌面UI统称为Launcher。Launcher是安卓系统中的主要程序组件之一,安卓系统中如果没有Launcher就无法启动安卓桌面,Launcher出错的时候,安卓系统会出现“进程 com.android.launcher 意外停止”的提示窗口。这时需要重新启动Launcher。
《百度百科 - launcher》

Launcher是android系统的桌面,是用户接触到的第一个带有界面的APP。它本质上就是一个系统级APP,和普通的APP一样,它界面也是在Activity上绘制出来的。

虽然Launcher也是一个APP,但是它涉及到的技术点却比一般的APP要多。CarLauncher作为IVI系统的桌面,需要显示系统中所有用户可用app的入口,显示最近用户使用的APP,同时还需要支持在桌面上动态显示如地图、音乐在内各个APP内部的信息,在桌面显示地图并与之进行简单的交互。地图开发的工作量极大,Launcher显然不可能引入地图的SDK再开发一个地图应用,那么如何在不扩大工作量的前提下动态的显示地图就成了CarLauncher的一个技术难点。

2.CarLauncher功能分析

原生的Carlaunher代码并不复杂,主要是协同SystemUI完成以下两个功能。

  • 显示 可以快捷操作的 『首页』
    image.png
  • 显示 所有APP入口的 『桌面』
    image.png
    需要注意的是,只有红框中的内容才属于CarLauncher的内容,红框之外的属于SystemUI的内容。虽然SystemUI在下方的NaviBar有6个按钮,但是只有点击首页App桌面才会进入CarLauncher,点击其它按钮都会进入其它APP,所以都不在本篇文章的分析范围。

    3.CarLauncher 源码分析

CarLauncher的源码结构如下:
image.png

3.1 Android.dp

CarLauncherandroid.bp相对比较简单,定义了CarLauncher的源码结构,和依赖的类库。如果你对android.bp完全不了解,可以先看一下 Android.bp入门教程 学习一下基础的语法,再来回过头来看CarLauncherandroid.bp相信会容易理解很多。

android_app {
    name: "CarLauncher",
    srcs: ["src/**/*.java"],
    resource_dirs: ["res"],
    // 允许使用系统的hide api
    platform_apis: true,
    required: ["privapp_whitelist_com.android.car.carlauncher"],
    // 签名类型 : platform
    certificate: "platform",
    // 设定apk安装路径为priv-app
    privileged: true,
    // 覆盖其它类型的Launcher
    overrides: [
        "Launcher2",
        "Launcher3",
        "Launcher3QuickStep",
    ],
    optimize: {
        enabled: false,
    },
    dex_preopt: {
        enabled: false,
    },
    // 引入静态库
    static_libs: [
        "androidx-constraintlayout_constraintlayout-solver",
        "androidx-constraintlayout_constraintlayout",
        "androidx.lifecycle_lifecycle-extensions",
        "car-media-common",
        "car-ui-lib",
    ],
    libs: ["android.car"],
    product_variables: {
        pdk: {
            enabled: false,
        },
    },
}

上述Android.bp中我们需要注意一个属性overrides,它表示覆盖的意思。在系统编译时Launcher2Launcher3Launcher3QuickStep都会被CarLauncher取代,前面三个Launcher并不是车机系统的桌面,车载系统中会用CarLauncher这个定制新的桌面取代掉其它系统的桌面。同样的,如果我们不想使用系统中自带的CarLauncher,那么也需要在overrides中覆盖掉CarLauncher。在自主开发的车载Android系统中这个属性我们会经常用到,用我们自己定制的各种APP来取代系统中默认的APP,比如系统设置等等。

3.2 AndroidManifest.xml

Manifest文件中我们可以看到CarLauncher所需要的权限,以及入口Activity。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.android.car.carlauncher">

    <uses-permission android:name="android.car.permission.ACCESS_CAR_PROJECTION_STATUS" />
    <!--  System permission to host maps activity  -->
    <uses-permission android:name="android.permission.ACTIVITY_EMBEDDING" />
    <!--  System permission to send events to hosted maps activity  -->
    <uses-permission android:name="android.permission.INJECT_EVENTS" />
    <!--  System permission to use internal system windows  -->
    <uses-permission android:name="android.permission.INTERNAL_SYSTEM_WINDOW" />
    <!--  System permissions to bring hosted maps activity to front on main display  -->
    <uses-permission android:name="android.permission.MANAGE_ACTIVITY_STACKS" />
    <!--  System permission to query users on device  -->
    <uses-permission android:name="android.permission.MANAGE_USERS" />
    <!--  System permission to control media playback of the active session  -->
    <uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL" />
    <!--  System permission to get app usage data  -->
    <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
    <!--  System permission to query all installed packages  -->
    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
    <uses-permission android:name="android.permission.REORDER_TASKS" />
    <!--  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>
        <intent>
            <action android:name="android.media.browse.MediaBrowserService" />
        </intent>
    </queries>
    <application
        android:icon="@drawable/ic_launcher_home"
        android:label="@string/app_title"
        android:supportsRtl="true"
        android:theme="@style/Theme.Launcher">
        <activity
            android:name=".CarLauncher"
            android:clearTaskOnLaunch="true"
            android:configChanges="uiMode|mcc|mnc"
            android:launchMode="singleTask"
            android:resumeWhilePausing="true"
            android:stateNotNeeded="true"
            android:windowSoftInputMode="adjustPan">
            <meta-data
                android:name="distractionOptimized"
                android:value="true" />
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.HOME" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>
        <activity
            android:name=".AppGridActivity"
            android:exported="true"
            android:launchMode="singleInstance"
            android:theme="@style/Theme.Launcher.AppGridActivity">
            <meta-data
                android:name="distractionOptimized"
                android:value="true" />
        </activity>
    </application>
</manifest>

关于Manifest我们重点来了解其中一些不常用的标签即可。

queries

<queries/>是在Android 11 上为了收紧应用权限而引入的。用于,指定当前应用程序要与之交互的其他应用程序集,这些其他应用程序可以通过 package、intent、provider。示例:

<queries>
    <package android:name="string" />
    <intent>
        ...
    </intent>
    <provider android:authorities="list" />
    ...
</queries>

更多内容可以参考:Android Developers |

android:clearTaskOnLaunch = "true"

每次启动都启动根Activity,并清理其他的Activity。

android:configChanges="uiMode|mcc|mnc"

关于android:configChanges就不废话了,直接上一张相对完整的表格供参考。
| VALUE | DESCRIPTION|
| ----|-----|
|mcc| 国际移动用户识别码所属国家代号是改变了,sim被侦测到了,去更新mcc MCC是移动用户所属国家代号|
|mnc|国际移动用户识别码的移动网号码是改变了, sim被侦测到了,去更新mnc MNC是移动网号码,最多由两位数字组成,用于识别移动用户所归属的移动通信网|
|locale|用户所在区域发生变化。例如:用户切换了语言时,切换后的语言会显示出来|
|touchscreen| 触摸屏发生改变|
|keyboard|键盘发生了改变。例如:用户介入了外部的键盘|
|keyboardHidden|键盘的可用性发生了改变|
|navigation|导航发生了变化|
|screenLayout|屏幕的显示发生了变化。例如:不同的显示被激活|
|fontScale|字体比例发生了变化。例如:选择了不同的全局字体|
|uiMode|用户的模式发生了变化|
|orientation|屏幕方向改变了。例如:横竖屏切换|
|smallestScreenSize|屏幕的物理大小改变了。例如:连接到一个外部的屏幕上|

android:resumeWhilePausing = "true"

当前一个Activity还在执行onPause()方法时(即在暂停过程中,还没有完全暂停),允许该Activity显示(此时Activity不能申请任何其他额外的资源,比如相机)

android:stateNotNeeded="true"

这个属性默认情况为false,若设为true,则当Activity重新启动时不会调用onSaveInstanceState方法,onCreate()方法中的Bundle参数将永远为null。在一些特殊场合下,由于用户按了Home键,该属性设置为true时,可以保证不用保存原先的状态引用,一定程度上节省空间资源。

android:name="distractionOptimized"

设定当前Activity处于活动状态,是否导致驾驶员分心,在国外车载Android应用程序需要遵守Android官方制定《驾驶员分心指南》,这个规则在国内使用的很少,具体请参考**Driver Distraction Guidelines | Android Open Source Project**

3.3 AppGridActivity

**AppGridActivity**用来显示系统中所有的APP,为用户的使用提供入口。

作为应用开发者,我们需要关注以下两个功能是如何实现的:

  • 显示系统中所有的APP,并过滤掉一些不需要显示在桌面的APP(例如:后台的Service)
  • 显示最近使用的APP

    显示系统中所有的APP(All App)

    CarLauncher中用于筛选所有APP的方法都集中在AppLauncherUtils
    ```
    /**

    • 获取我们希望在启动器中以未排序的顺序看到的所有组件,包括启动器活动和媒体服务。
      *
    • @param blackList 要隐藏的应用程序(包名称)列表(可能为空)
    • @param customMediaComponents 不应在Launcher中显示的媒体组件(组件名称)列表(可能为空),因为将显示其应用程序的Launcher活动
    • @param appTypes 要显示的应用程序类型(例如:全部或仅媒体源)
    • @param openMediaCenter 当用户选择媒体源时,启动器是否应导航到media center。
    • @param launcherApps {@link LauncherApps}系统服务
    • @param carPackageManager {@link CarPackageManager}系统服务
    • @param packageManager {@link PackageManager}系统服务
    • @return 一个新的 {@link LauncherAppsInfo}
      */
      @NonNull
      static LauncherAppsInfo getLauncherApps(
      @NonNull Set blackList,
      @NonNull Set customMediaComponents,
      @AppTypes int appTypes,
      boolean openMediaCenter,
      LauncherApps launcherApps,
      CarPackageManager carPackageManager,
      PackageManager packageManager,
      CarMediaManager carMediaManager) {

    if (launcherApps == null || carPackageManager == null || packageManager == null

        || carMediaManager == null) {
    return EMPTY_APPS_INFO;
    

    }
    // 检索所有符合给定intent的服务
    List mediaServices = packageManager.queryIntentServices(

        new Intent(MediaBrowserService.SERVICE_INTERFACE),
        PackageManager.GET_RESOLVED_FILTER);
    

    // 检索指定packageName的Activity的列表
    List availableActivities =

        launcherApps.getActivityList(null, Process.myUserHandle());
    

    Map launchablesMap = new HashMap<>(

        mediaServices.size() + availableActivities.size());
    

    Map mediaServicesMap = new HashMap<>(mediaServices.size());

    // Process media services
    if ((appTypes & APP_TYPE_MEDIA_SERVICES) != 0) {

    for (ResolveInfo info : mediaServices) {
        String packageName = info.serviceInfo.packageName;
        String className = info.serviceInfo.name;
        ComponentName componentName = new ComponentName(packageName, className);
        mediaServicesMap.put(componentName, info);
        if (shouldAddToLaunchables(componentName, blackList, customMediaComponents,
                appTypes, APP_TYPE_MEDIA_SERVICES)) {
            final boolean isDistractionOptimized = true;
    
            Intent intent = new Intent(Car.CAR_INTENT_ACTION_MEDIA_TEMPLATE);
            intent.putExtra(Car.CAR_EXTRA_MEDIA_COMPONENT, componentName.flattenToString());
    
            AppMetaData appMetaData = new AppMetaData(
                info.serviceInfo.loadLabel(packageManager),
                componentName,
                info.serviceInfo.loadIcon(packageManager),
                isDistractionOptimized,
                context -> {
                    if (openMediaCenter) {
                        AppLauncherUtils.launchApp(context, intent);
                    } else {
                        selectMediaSourceAndFinish(context, componentName, carMediaManager);
                    }
                },
                context -> {
                    // 返回系统中所有MainActivity带有Intent.CATEGORY_INFO 和 Intent.CATEGORY_LAUNCHER的intent
                    Intent packageLaunchIntent =
                            packageManager.getLaunchIntentForPackage(packageName);
                    AppLauncherUtils.launchApp(context,
                            packageLaunchIntent != null ? packageLaunchIntent : intent);
                });
            launchablesMap.put(componentName, appMetaData);
        }
    }
    

    }

    // Process activities
    if ((appTypes & APP_TYPE_LAUNCHABLES) != 0) {

    for (LauncherActivityInfo info : availableActivities) {
        ComponentName componentName = info.getComponentName();
        String packageName = componentName.getPackageName();
        if (shouldAddToLaunchables(componentName, blackList, customMediaComponents,
                appTypes, APP_TYPE_LAUNCHABLES)) {
            boolean isDistractionOptimized =
                isActivityDistractionOptimized(carPackageManager, packageName,
                    info.getName());
    
            Intent intent = new Intent(Intent.ACTION_MAIN)
                .setComponent(componentName)
                .addCategory(Intent.CATEGORY_LAUNCHER)
                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            // 获取app的name,和 app的图标
            AppMetaData appMetaData = new AppMetaData(
                info.getLabel(),
                componentName,
                info.getBadgedIcon(0),
                isDistractionOptimized,
                context -> AppLauncherUtils.launchApp(context, intent),
                null);
            launchablesMap.put(componentName, appMetaData);
        }
    }
    

    }

    return new LauncherAppsInfo(launchablesMap, mediaServicesMap);
    }

通过LauncherApps.getActivityList(),获得的List<LauncherActivityInfo>包含了系统中所有配置了`Intent#ACTION_MAIN` 和`Intent#CATEGORY_LAUNCHER`的Activity信息。
**String LauncherActivityInfogetLabel() : 获取app的name**
**String LauncherActivityInfo.getComponentName() : 获取app的Mainactivity信息**
**Drawable LauncherActivityInfo.getBadgedIcon(0) : 获取App的图标**
最后,当用户点击图标时,虽然也是通过startActivity启动App,但是ActivityOptions可以让我们决定目标APP在哪个屏幕上启动,这对当前车载多屏系统而言非常重要。

static void launchApp(Context context, Intent intent) {
ActivityOptions options = ActivityOptions.makeBasic();
// 在当前的屏幕上启动目标App的Activity
options.setLaunchDisplayId(context.getDisplayId());
context.startActivity(intent, options.toBundle());
}


#### **显示最近使用的APP (Recent APP)**
Android系统中提供了`UsageStatusManager`来提供对设备使用情况历史记录和统计信息的访问,`UsageStatusManager`
使用`android.provider.Settings#ACTION_USAGE_ACCESS_SETTINGS`,`getAppStandbyBucket()`,`queryEventsForSelf(long,long)`,方法时不需要添加额外的权限。但是除此以外的方法都需要`android.permission.PACKAGE_USAGE_STATS`权限。

/**

  • 请注意,为了从上一次boot中获得使用情况统计数据,设备必须经过干净的关闭过程。
    */
    private List getMostRecentApps(LauncherAppsInfo appsInfo) {
    ArrayList apps = new ArrayList<>();
    if (appsInfo.isEmpty()) {

     return apps;
    

    }

    // 获取从1年前开始的使用情况统计数据,返回如下条目:
    // "During 2017 App A is last used at 2017/12/15 18:03"
    // "During 2017 App B is last used at 2017/6/15 10:00"
    // "During 2018 App A is last used at 2018/1/1 15:12"
    List stats =

         mUsageStatsManager.queryUsageStats(
                 UsageStatsManager.INTERVAL_YEARLY,
                 System.currentTimeMillis() - DateUtils.YEAR_IN_MILLIS,
                 System.currentTimeMillis());
    

    if (stats == null || stats.size() == 0) {

     return apps; // empty list
    

    }

    stats.sort(new LastTimeUsedComparator());

    int currentIndex = 0;
    int itemsAdded = 0;
    int statsSize = stats.size();
    int itemCount = Math.min(mColumnNumber, statsSize);
    while (itemsAdded < itemCount && currentIndex < statsSize) {

     UsageStats usageStats = stats.get(currentIndex);
     String packageName = usageStats.mPackageName;
     currentIndex++;
    
     // 不包括自己
     if (packageName.equals(getPackageName())) {
         continue;
     }
    
     // TODO(b/136222320): 每个包都可以获得UsageStats,但一个包可能包含多个媒体服务。我们需要找到一种方法来获取每个服务的使用率统计数据。
     ComponentName componentName = AppLauncherUtils.getMediaSource(mPackageManager,
             packageName);
     // 免除媒体服务的后台和启动器检查
     if (!appsInfo.isMediaService(componentName)) {
         // 不要包括仅在后台运行的应用程序
         if (usageStats.getTotalTimeInForeground() == 0) {
             continue;
         }
         // 不要包含不支持从启动器启动的应用程序
         Intent intent = getPackageManager().getLaunchIntentForPackage(packageName);
         if (intent == null || !intent.hasCategory(Intent.CATEGORY_LAUNCHER)) {
             continue;
         }
     }
    
     AppMetaData app = appsInfo.getAppMetaData(componentName);
     // 防止重复条目
     // e.g. app is used at 2017/12/31 23:59, and 2018/01/01 00:00
     if (app != null && !apps.contains(app)) {
         apps.add(app);
         itemsAdded++;
     }
    

    }
    return apps;
    }


### 3.4 CarLauncher

`CarLauncher`和普通的Android应用一样,都会有一个主布局文件,并且在Activity的onCreate中进行一系列初始化工作,所以我们先从`CarLauncher`的布局文件和onCreate方法开始分析。

#### CarLauncher的主布局文件

*这里我们只分析横屏的布局文件,源码中其实还包含了竖屏和多窗口的布局。*

![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8e66324f31aa4f0f96ce70289d72ba6e~tplv-k3u1fbpfcp-watermark.image?)
<!-- 省略 -->

<View
    android:id="@+id/top_line"
    style="@style/HorizontalLineDivider"
    app:layout_constraintTop_toTopOf="parent" />

<androidx.cardview.widget.CardView
    style="@style/CardViewStyle"
    // ... 
    >
    <!-- 用来显示地图的 ActivityView -->
    <android.car.app.CarActivityView
        android:id="@+id/maps"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.cardview.widget.CardView>

<!-- 显示天气 -->
<FrameLayout
    android:id="@+id/contextual"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:layout_margin="@dimen/main_screen_widget_margin"
    android:layoutDirection="locale"
    app:layout_constraintBottom_toTopOf="@+id/divider_horizontal"
    app:layout_constraintLeft_toRightOf="@+id/divider_vertical"
    app:layout_constraintRight_toLeftOf="@+id/end_edge"
    app:layout_constraintTop_toBottomOf="@+id/top_edge" />

<!-- 显示本地音乐 -->
<FrameLayout
    android:id="@+id/playback"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:layout_margin="@dimen/main_screen_widget_margin"
    android:layoutDirection="locale"
    app:layout_constraintBottom_toTopOf="@+id/bottom_edge"
    app:layout_constraintLeft_toRightOf="@+id/divider_vertical"
    app:layout_constraintRight_toLeftOf="@+id/end_edge"
    app:layout_constraintTop_toBottomOf="@+id/divider_horizontal" />

<View
    android:id="@+id/bottom_line"
    style="@style/HorizontalLineDivider"
    app:layout_constraintBottom_toBottomOf="parent" />

`CarLauncher`的布局文件很简单,几乎没什么值得解释的地方。

#### 初始化Android桌面
初始化Android桌面的工作大多都是在CarLuancher.onCreate方法中完成,该方法代码如下:

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 在多窗口模式下『car_launcher_multiwindow』不显示“地图”面板。
// 注意:拆分屏幕的CTS测试与启动器默认活动的活动视图不兼容
if (isInMultiWindowMode() || isInPictureInPictureMode()) {
setContentView(R.layout.car_launcher_multiwindow);
} else {
setContentView(R.layout.car_launcher);
}
// 初始化『天气』和『音乐』fragment
initializeFragments();
// 监听ActivityView状态,并启动地图面板
mActivityView = findViewById(R.id.maps);
if (mActivityView != null) {
mActivityView.setCallback(mActivityViewCallback);
}
// 监听屏幕状态,并启动地图面板
mDisplayManager = getSystemService(DisplayManager.class);
mDisplayManager.registerDisplayListener(mDisplayListener, mMainHandler);
}

上述代码中的`ActivityView`是用来显示地图的一个ViewGroup。`ActivityView`的状态回调如下:

private final ActivityView.StateCallback mActivityViewCallback = new ActivityView.StateCallback() {
@Override
public void onActivityViewReady(ActivityView view) {
if (DEBUG) Log.d(TAG, "onActivityViewReady(" + getUserId() + ")");
mActivityViewReady = true;
startMapsInActivityView();
maybeLogReady();
}

@Override
public void onActivityViewDestroyed(ActivityView view) {
    if (DEBUG) Log.d(TAG, "onActivityViewDestroyed(" + getUserId() + ")");
    mActivityViewReady = false;
}

@Override
public void onTaskMovedToFront(int taskId) {
    if (DEBUG) {
        Log.d(TAG, "onTaskMovedToFront(" + getUserId() + "): started=" + mIsStarted);
    }
    try {
        if (mIsStarted) {
            ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
            am.moveTaskToFront(CarLauncher.this.getTaskId(), /* flags= */ 0);
        }
    } catch (RuntimeException e) {
        Log.w(TAG, "Failed to move CarLauncher to front.");
    }
}

};

设定回调的作用就是在`ActivityView`初始化完毕后,启动地图。

private void startMapsInActivityView() {
if (mActivityView == null || !mActivityViewReady) {
return;
}
// 如果我们碰巧被重新呈现为多显示模式,我们将跳过在“Activity”视图中启动内容,因为我们无论如何都会被重新创建。
if (isInMultiWindowMode() || isInPictureInPictureMode()) {
return;
}
// 在“活动可见性测试(ActivityVisibilityTests)”的显示关闭时不要启动地图。
if (getDisplay().getState() != Display.STATE_ON) {
return;
}
try {
mActivityView.startActivity(getMapsIntent());
} catch (ActivityNotFoundException e) {
Log.w(TAG, "Maps activity not found", e);
}
}

private Intent getMapsIntent() {
// 为应用程序的主Activity创建一个意图,不指定要运行的特定Activity,而是提供一个选择器来查找该Activity。
return Intent.makeMainSelectorActivity(Intent.ACTION_MAIN, Intent.CATEGORY_APP_MAPS);
}

在onCreate中还会监听当前的屏幕状态,每当屏幕的逻辑显示属性(如大小和密度)发生更改时,都会在`ActivityView`中重新显示地图。

private final DisplayListener mDisplayListener = new DisplayListener() {
@Override
public void onDisplayAdded(int displayId) {}
@Override
public void onDisplayRemoved(int displayId) {}

@Override
public void onDisplayChanged(int displayId) {
    if (displayId != getDisplay().getDisplayId()) {
        return;
    }
    // startMapsInActivityView()将检查显示器的状态。
    startMapsInActivityView();
}

};

```

ActivityView

允许将Activity启动到自身的任务容器。本质上是一个虚拟屏幕,所以在此ActivityView中启动的活动受适用于在VirtualDisplay上启动的相同规则的限制。
如果想在我们自己项目中使用ActivityView,需要从Android源码移植,Android SDK中并没有提供ActivityView相关的API。

注意:由于ActivityView中的hosted maps活动当前处于虚拟屏幕,因此系统认为该活动始终位于前面。以直接intent启动“地图”Activity将不起作用。要在real display上启动“地图”活动,需要使用{ *@link *Intent#CATEGORY_APP_MAPS}类别将intent发送给launcher,launcher将在real display上启动Activity。

有关VirtualDisplay的内容可以参考这篇老哥的博客:Android P 图形显示系统(四) Android VirtualDisplay解析

参考资料

Android Developers |

目录
相关文章
|
11天前
|
Android开发 Swift iOS开发
深入探索iOS与Android操作系统的架构差异及其对应用开发的影响
在当今数字化时代,移动设备已经成为我们日常生活和工作不可或缺的一部分。其中,iOS和Android作为全球最流行的两大移动操作系统,各自拥有独特的系统架构和设计理念。本文将深入探讨iOS与Android的系统架构差异,并分析这些差异如何影响应用开发者的开发策略和用户体验设计。通过对两者的比较,我们可以更好地理解它们各自的优势和局限性,从而为开发者提供有价值的见解,帮助他们在这两个平台上开发出更高效、更符合用户需求的应用。
|
20天前
|
安全 Android开发 数据安全/隐私保护
深入探讨iOS与Android系统安全性对比分析
在移动操作系统领域,iOS和Android无疑是两大巨头。本文从技术角度出发,对这两个系统的架构、安全机制以及用户隐私保护等方面进行了详细的比较分析。通过深入探讨,我们旨在揭示两个系统在安全性方面的差异,并为用户提供一些实用的安全建议。
|
28天前
|
缓存 Java Shell
Android 系统缓存扫描与清理方法分析
Android 系统缓存从原理探索到实现。
53 15
Android 系统缓存扫描与清理方法分析
|
20天前
|
搜索推荐 前端开发 Android开发
安卓应用开发中的自定义视图实现
【10月更文挑战第30天】在安卓开发的海洋中,自定义视图是那抹不可或缺的亮色,它为应用界面的个性化和交互体验的提升提供了无限可能。本文将深入探讨如何在安卓平台创建自定义视图,并展示如何通过代码实现这一过程。我们将从基础出发,逐步引导你理解自定义视图的核心概念,然后通过一个实际的代码示例,详细讲解如何将理论应用于实践,最终实现一个美观且具有良好用户体验的自定义控件。无论你是想提高自己的开发技能,还是仅仅出于对安卓开发的兴趣,这篇文章都将为你提供价值。
|
20天前
|
Android开发 Swift iOS开发
探索iOS与安卓应用开发的差异性
探索iOS与安卓应用开发的差异性
41 2
|
26天前
|
传感器 XML IDE
探索安卓应用开发:从基础到进阶
【10月更文挑战第23天】在数字化时代的浪潮中,移动应用已成为人们日常生活的延伸。本文以安卓平台为例,深入浅出地介绍了如何从零开始构建一个安卓应用,涵盖了开发环境搭建、基本组件使用、界面设计原则以及进阶技巧等关键步骤。通过实例演示和代码片段,引导读者逐步掌握安卓应用开发的核心技能,旨在激发更多开发者对安卓平台的探索热情,并为初学者提供一条清晰的学习路径。
|
15天前
|
前端开发 Android开发 UED
安卓应用开发中的自定义控件实践
【10月更文挑战第35天】在移动应用开发中,自定义控件是提升用户体验、增强界面表现力的重要手段。本文将通过一个安卓自定义控件的创建过程,展示如何从零开始构建一个具有交互功能的自定义视图。我们将探索关键概念和步骤,包括继承View类、处理测量与布局、绘制以及事件处理。最终,我们将实现一个简单的圆形进度条,并分析其性能优化。
|
26天前
|
开发工具 Android开发 Swift
探索iOS与安卓应用开发的异同点
【10月更文挑战第24天】本文通过比较iOS和安卓开发环境,旨在揭示两大移动平台在开发过程中的相似性与差异性。我们将探讨开发工具、编程语言、用户界面设计、性能优化及市场分布等方面,以期为开发者提供全面的视角。通过深入浅出的分析,文章将帮助读者更好地理解每个平台的独特之处及其对应用开发的影响。
|
27天前
|
XML IDE Java
安卓应用开发入门:从零开始的旅程
【10月更文挑战第23天】本文将带领读者开启一段安卓应用开发的奇妙之旅。我们将从最基础的概念讲起,逐步深入到开发实践,最后通过一个简易的代码示例,展示如何将理论知识转化为实际的应用。无论你是编程新手,还是希望扩展技能的软件工程师,这篇文章都将为你提供有价值的指导和启发。
29 0
|
6天前
|
开发框架 前端开发 Android开发
安卓与iOS开发中的跨平台策略
在移动应用开发的战场上,安卓和iOS两大阵营各据一方。随着技术的演进,跨平台开发框架成为开发者的新宠,旨在实现一次编码、多平台部署的梦想。本文将探讨跨平台开发的优势与挑战,并分享实用的开发技巧,帮助开发者在安卓和iOS的世界中游刃有余。