Android15私密空間源碼分析

<meta charset="utf-8">

一了嚎、概述

私密空間是Android15新特性萎津,涉及了多個模塊。本文從設置递宅,框架娘香,Launcher3個模塊對私密空間主流程進行分析

二、開始

  • 設置 ->安全和隱私->私密空間

私密空間數(shù)據(jù)初始化

設置監(jiān)聽開機廣播初始化安全數(shù)據(jù)

public class SafetySourceBroadcastReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
     ...
        if (ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
            refreshAllSafetySources(context, EVENT_DEVICE_REBOOTED);
        }
    }

接著PrivateSpaceSafetySource中設置鎖屏安全數(shù)據(jù)

    public static void setSafetySourceData(Context context,
            SafetyEvent safetyEvent) {
      ...
       //判斷是否滿足了私密空間開啟的前置條件
         1:safetyCenter page is enabled
         2:PrivateSpaceFeatures is enabled
         3:userManager.isMainUser()
         // 獲取 PrivateSpaceAuthenticationActivity的PendingIntent
         PendingIntent pendingIntent = getPendingIntentForPsDashboard(context);
        //設置隱私安全的title办龄,sunnary烘绽,以及 PendingIntent
        SafetySourceStatus status = new SafetySourceStatus.Builder(
                context.getString(R.string.private_space_title),
                context.getString(R.string.private_space_summary),
                SafetySourceData.SEVERITY_LEVEL_UNSPECIFIED)
                .setPendingIntent(pendingIntent).build();
        SafetySourceData safetySourceData =
                new SafetySourceData.Builder().setStatus(status).build();
        Log.d(TAG, "Setting safety source data:"+ Log.getStackTraceString(new Throwable("setSafetySourceData")));
        SafetyCenterManagerWrapper.get().setSafetySourceData(
                context,
                SAFETY_SOURCE_ID,
                safetySourceData,
                safetyEvent
        );

點擊私密空間后跳轉(zhuǎn)到PrivateSpaceAuthenticationActivity

PrivateSpaceAuthenticationActivity:
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
     ...
     //判斷鎖屏密碼是否設置
        if (getKeyguardManager().isDeviceSecure()) {
            if (savedInstanceState == null) {
            //判斷私密空間是否已存在,存在則進行解鎖并進入私密空間
                if (mPrivateSpaceMaintainer.doesPrivateSpaceExist()) {
                    unlockAndLaunchPrivateSpaceSettings(this);
                } else {
                //不存在則驗證密碼并創(chuàng)建私密空間
                    authenticatePrivateSpaceEntry();
                }
            }
        } else {
            //彈窗提示用戶設置鎖屏密碼
            promptToSetDeviceLock();
        }
    }

創(chuàng)建私密空間

PrivateSpaceAuthenticationActivity#authenticatePrivateSpaceEntry中

    private void authenticatePrivateSpaceEntry() {
      //獲取私密空間的鎖屏憑證意圖(Intent)的方法俐填,主要用于管理和訪問 Android 中的私密空間安接。
        Intent credentialIntent = mPrivateSpaceMaintainer.getPrivateProfileLockCredentialIntent();
        if (credentialIntent != null) {
            if (android.multiuser.Flags.usePrivateSpaceIconInBiometricPrompt()) {
                credentialIntent.putExtra(CUSTOM_BIOMETRIC_PROMPT_LOGO_RES_ID_KEY,
                        com.android.internal.R.drawable.stat_sys_private_profile_status);
                credentialIntent.putExtra(CUSTOM_BIOMETRIC_PROMPT_LOGO_DESCRIPTION_KEY,
                        getApplicationContext().getString(
                                com.android.internal.R.string.private_space_biometric_prompt_title
                        ));
            }
            //驗證鎖屏密碼
            mVerifyDeviceLock.launch(credentialIntent);
        } else {
            Log.e(TAG, "verifyCredentialIntent is null even though device lock is set");
            finish();
        }
    }

驗證完鎖屏密碼后執(zhí)行PrivateSpaceAuthenticationActivity#onLockAuthentication方法

    @VisibleForTesting
    public void onLockAuthentication(Context context) {
        if (mPrivateSpaceMaintainer.doesPrivateSpaceExist()) {
            unlockAndLaunchPrivateSpaceSettings(context);
        } else {
          //私密空間不存在則進入私密空間引導頁
            startActivity(new Intent(context, PrivateSpaceSetupActivity.class));
            finish();
        }
    }

[圖片上傳失敗...(image-a1d04-1732513869780)]

PrivateSpaceEducation

    private View.OnClickListener onSetup() {
        return v -> {
            mMetricsFeatureProvider.action(
                    getContext(), SettingsEnums.ACTION_PRIVATE_SPACE_SETUP_START);
            Log.i(TAG, "Starting private space setup");

            //這里會進入PrivateSpaceCreationFragment進行私密空間的創(chuàng)建
            NavHostFragment.findNavController(PrivateSpaceEducation.this)
                    .navigate(R.id.action_education_to_create);
        };
    }

    private View.OnClickListener onCancel() {
        return v -> {
            Activity activity = getActivity();
            if (activity != null) {
                mMetricsFeatureProvider.action(
                        getContext(), SettingsEnums.ACTION_PRIVATE_SPACE_SETUP_CANCEL);
                Log.i(TAG, "private space setup cancelled");
                activity.finish();
            }
        };
    }

PrivateSpaceCreationFragment

 //首先會再次判斷是否支持私密空間
  @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        if (android.os.Flags.allowPrivateProfile()
                && android.multiuser.Flags.enablePrivateSpaceFeatures()) {
            super.onCreate(savedInstanceState);
        }
    }

    //延遲1s防止阻塞UI,再創(chuàng)建私密空間
    @Override
    public void onResume() {
        super.onResume();
        // Ensures screen visibility to user by introducing a 1-second delay before creating private
        // space.
        sHandler.removeCallbacks(mRunnable);
        sHandler.postDelayed(mRunnable, PRIVATE_SPACE_CREATE_POST_DELAY_MS);
    }

      private Runnable mRunnable =
            () -> {
                createPrivateSpace();
            };

    private void createPrivateSpace() {

        if (PrivateSpaceMaintainer.getInstance(getActivity()).createPrivateSpace()) {
            Log.i(TAG, "Private Space created");
            mMetricsFeatureProvider.action(
                    getContext(), SettingsEnums.ACTION_PRIVATE_SPACE_SETUP_SPACE_CREATED, true);
             //這里判斷是否連接網(wǎng)絡,有網(wǎng)絡就需要登錄英融,無網(wǎng)絡就跳轉(zhuǎn)到私密空間密碼鎖定設置頁面
            if (isConnectedToInternet()) {
                registerReceiver();
                sHandler.postDelayed(
                        mAccountLoginRunnable, PRIVATE_SPACE_ACCOUNT_LOGIN_POST_DELAY_MS);
            } else {
                NavHostFragment.findNavController(PrivateSpaceCreationFragment.this)
                        .navigate(R.id.action_set_lock_fragment);
            }
        }
    }

createPrivateSpace源碼

  @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
    public final synchronized boolean createPrivateSpace() {
        if (!Flags.allowPrivateProfile()
                || !android.multiuser.Flags.enablePrivateSpaceFeatures()) {
            return false;
        }
        // Check if Private space already exists
        if (doesPrivateSpaceExist()) {
            return true;
        }
        // a name indicating that the profile was created from the PS Settings page
        final String userName = "Private space";

        if (mUserHandle == null) {
            try {
                mUserHandle = mUserManager.createProfile(
                        userName, USER_TYPE_PROFILE_PRIVATE, new ArraySet<>());
            } catch (Exception e) {
                Log.e(TAG, "Error creating private space", e);
                return false;
            }

            if (mUserHandle == null) {
                Log.e(TAG, "Failed to create private space");
                return false;
            }
            //注冊私密空間Intent.ACTION_PROFILE_REMOVED 廣播
            registerBroadcastReceiver();
            //啟動私密空間
            if (!startProfile()) {
                // TODO(b/333884792): Add test to mock when startProfile fails.
                Log.e(TAG, "profile not started, created profile is deleted");
                deletePrivateSpace();
                return false;
            }

            Log.i(TAG, "Private space created with id: " + mUserHandle.getIdentifier());
            //設置顯示設置的入口盏檐,設定自動鎖定策略,設置隱藏通知
            resetPrivateSpaceSettings();
            //設置完畢
            setUserSetupComplete();
            //私密空間app跳過用戶提示
            setSkipFirstUseHints();
            //Disable settings app launcher icon驶悟,胡野,,Disable Shortcut picker
            disableComponentsToHidePrivateSpaceSettings();
        }
        return true;
    }

這里梳理一下流程:

1:createPrivateSpace跨進程創(chuàng)建虛擬私密空間痕鳍,得到UserHandle

2:注冊Intent.ACTION_PROFILE_REMOVED廣播硫豆,當收到廣播時移除私密空間配置信息

3:啟動后臺私密空間,啟動失敗則刪除私密空間

4:初始化私密空間配置

這里可以看到私密空間就是基于多用戶笼呆,userName為"Private space",userType為USER_TYPE_PROFILE_PRIVATE熊响,如下圖所示

[圖片上傳失敗...(image-a2def3-1732513869779)]

初始化私密空間應用

桌面應用的LauncherAppState注冊了私密空間的廣播監(jiān)聽,前面說到創(chuàng)建多用戶完成后會發(fā)送Intent.ACTION_PROFILE_ADDED/廣播,刪除私密空間會發(fā)送 Intent.ACTION_PROFILE_REMOVED廣播诗赌,桌面監(jiān)聽到隱私空間用戶創(chuàng)建和銷毀的廣播后會觸發(fā)forceReload()汗茄;

     SafeCloseable userChangeListener = UserCache.INSTANCE.get(mContext)
                .addUserEventListener(mModel::onUserEvent);

     public void onUserEvent(UserHandle user, String action) {
      ...
       else if (UserCache.ACTION_PROFILE_ADDED.equals(action)
                || UserCache.ACTION_PROFILE_REMOVED.equals(action)) {
            forceReload();
        } 
        ...
     }

接著調(diào)用到LauncherModel#startLoader方法,最終mLoaderTask的run方法

    private boolean startLoader(@NonNull final Callbacks[] newCallbacks) {
      ...
         stopLoader();
                    mLoaderTask = new LoaderTask(
                            mApp, mBgAllAppsList, mBgDataModel, mModelDelegate, launcherBinder);

                    // Always post the loader task, instead of running directly
                    // (even on same thread) so that we exit any nested synchronized blocks
                    MODEL_EXECUTOR.post(mLoaderTask);
    }

接著調(diào)用loadAllApps()加載appInfo

   public void run() {
    ...
        allActivityList = loadAllApps();
        ...
     }

遍歷私密空間和主空間境肾,mLauncherApps.getActivityList(null, user)獲取指定用戶的LauncherActivityInfo

    private List<LauncherActivityInfo> loadAllApps() {
        final List<UserHandle> profiles = mUserCache.getUserProfiles();
        List<LauncherActivityInfo> allActivityList = new ArrayList<>();
        // Clear the list of apps
        mBgAllAppsList.clear();

        List<IconRequestInfo<AppInfo>> iconRequestInfos = new ArrayList<>();
        boolean isWorkProfileQuiet = false;
        boolean isPrivateProfileQuiet = false;
        for (UserHandle user : profiles) {
            // Query for the set of apps
            final List<LauncherActivityInfo> apps = mLauncherApps.getActivityList(null, user);
            // Fail if we don't have any apps
            // TODO: Fix this. Only fail for the current user.
            if (apps == null || apps.isEmpty()) {
                return allActivityList;
            }
            boolean quietMode = mUserManagerState.isUserQuiet(user);

            if (Flags.enablePrivateSpace()) {
                if (mUserCache.getUserInfo(user).isWork()) {
                    isWorkProfileQuiet = quietMode;
                } else if (mUserCache.getUserInfo(user).isPrivate()) {
                    isPrivateProfileQuiet = quietMode;
                }
            }
         ...

mLauncherApps.getActivityList(null, user) 實現(xiàn)是通過ILauncherApps.getLauncherActivities() Binder調(diào)用

    @SuppressLint("RequiresPermission")
    @RequiresPermission(conditional = true,
            anyOf = {ACCESS_HIDDEN_PROFILES_FULL, ACCESS_HIDDEN_PROFILES})
    public List<LauncherActivityInfo> getActivityList(String packageName, UserHandle user) {
        logErrorForInvalidProfileAccess(user);
        try {
            return convertToActivityList(mService.getLauncherActivities(mContext.getPackageName(),
                    packageName, user), user);
        } catch (RemoteException re) {
            throw re.rethrowFromSystemServer();
        }
    }

接著看system_process實現(xiàn)端LauncherAppsService邏輯剔难,最終generateLauncherActivitiesForArchivedApp通過PackageManagerService獲取指定user的launcherActivities胆屿,并返回給Launcher

 LauncherAppsService:

  private ParceledListSlice<LauncherActivityInfoInternal> getActivitiesForArchivedApp(
                @Nullable String packageName,
                UserHandle user,
                ParceledListSlice<LauncherActivityInfoInternal> launcherActivities) {
            final List<LauncherActivityInfoInternal> archivedActivities =
                    generateLauncherActivitiesForArchivedApp(packageName, user);
            if (archivedActivities.isEmpty()) {
                return launcherActivities;
            }
            if (launcherActivities == null) {
                return new ParceledListSlice(archivedActivities);
            }
            List<LauncherActivityInfoInternal> result = launcherActivities.getList();
            result.addAll(archivedActivities);
            return new ParceledListSlice(result);
        }

最終Launcher得到私密空間和主空間的最新的appInfo,最后刷新Launcher UI

總結(jié)一下:

可以簡單理解桌面是個容器偶宫,可以加載不同用戶空間的apk launcher數(shù)據(jù)(如主用戶非迹,分身用戶,Android15新增的私密空間用戶)桌面點擊不同用戶apk纯趋,那么這個apk所處的運行環(huán)境就是自身用戶的環(huán)境憎兽,得到的私有路徑也是私密空間的環(huán)境,任務欄中不同應用切換不涉及多用戶的切換吵冒。

私密空間的應用安裝和卸載

點擊安裝會跳轉(zhuǎn)到Google Play才能安裝纯命,這里我們通過adb安裝 adb install -t --user USER_ID xx.apk。

本質(zhì)上跟應用安裝流程一樣這里不多累贅痹栖。主要看看桌面私密空間的刷新邏輯

[圖片上傳失敗...(image-94a445-1732513869779)]

LauncherApps向system_process進程的LauncherAppsService服務注冊了一個Binder接口亿汞,當應用卸載或者安裝時會收到LauncherAppsService的回調(diào)

    public void registerCallback(Callback callback, Handler handler) {
        synchronized (this) {
            if (callback != null && findCallbackLocked(callback) < 0) {
                boolean addedFirstCallback = mCallbacks.size() == 0;
                addCallbackLocked(callback, handler);
                if (addedFirstCallback) {
                    try {
                        mService.addOnAppsChangedListener(mContext.getPackageName(),
                                mAppsChangedListener);
                    } catch (RemoteException re) {
                        throw re.rethrowFromSystemServer();
                    }
                }
            }
        }
    }

    private final IOnAppsChangedListener.Stub mAppsChangedListener =
            new IOnAppsChangedListener.Stub() {

        @Override
        public void onPackageRemoved(UserHandle user, String packageName)
                throws RemoteException {
            if (DEBUG) {
                Log.d(TAG, "onPackageRemoved " + user.getIdentifier() + "," + packageName);
            }
            synchronized (LauncherApps.this) {
                for (CallbackMessageHandler callback : mCallbacks) {
                    callback.postOnPackageRemoved(packageName, user);
                }
            }
        }

        @Override
        public void onPackageAdded(UserHandle user, String packageName) throws RemoteException {
            if (DEBUG) {
                Log.d(TAG, "onPackageAdded " + user.getIdentifier() + "," + packageName);
            }
            synchronized (LauncherApps.this) {
                for (CallbackMessageHandler callback : mCallbacks) {
                    callback.postOnPackageAdded(packageName, user);
                }
            }
        }

最終調(diào)用PackageUpdatedTask#execute,安裝應用執(zhí)行appsList.addPackage揪阿,卸載應用執(zhí)行appsList.removePackage疗我,最后刷新Launcher UI

 public void execute(@NonNull ModelTaskController taskController, @NonNull BgDataModel dataModel,
            @NonNull AllAppsList appsList) {
        final LauncherAppState app = taskController.getApp();
        final Context context = app.getContext();
      ...
        switch (mOp) {
            case OP_ADD: {
                for (int i = 0; i < N; i++) {
                    iconCache.updateIconsForPkg(packages[i], mUser);
                    if (FeatureFlags.PROMISE_APPS_IN_ALL_APPS.get()) {
                        if (DEBUG) {
                            Log.d(TAG, "OP_ADD: PROMISE_APPS_IN_ALL_APPS enabled:"
                                    + " removing promise icon apps from package=" + packages[i]);
                        }
                        appsList.removePackage(packages[i], mUser);
                    }
                    activitiesLists.put(packages[i],
                            appsList.addPackage(context, packages[i], mUser));
                }
                flagOp = FlagOp.NO_OP.removeFlag(WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE);
                break;
            }

              case OP_UNAVAILABLE:
                for (int i = 0; i < N; i++) {
                    if (DEBUG) {
                        Log.d(TAG, getOpString() + ": removing package=" + packages[i]);
                    }
                    appsList.removePackage(packages[i], mUser);
                }
                flagOp = FlagOp.NO_OP.addFlag(WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE);
                break;
                ...

刪除私密空間

該界面為setting的PrivateSpaceDeleteFragment,點擊刪除需要驗證鎖屏密碼南捂,然后進入PrivateSpaceDeletionProgressFragment執(zhí)行真正的刪除工作

[圖片上傳失敗...(image-b4aa77-1732513869779)]

  private Runnable mDeletePrivateSpace =
            new Runnable() {
                @Override
                public void run() {
                    deletePrivateSpace();
                    getActivity().finish();
                }
            };

    public void deletePrivateSpace() {
        PrivateSpaceMaintainer.ErrorDeletingPrivateSpace error =
                mPrivateSpaceMaintainer.deletePrivateSpace();
        if (error == DELETE_PS_ERROR_NONE) {
            showSuccessfulDeletionToast();
        } else if (error == DELETE_PS_ERROR_INTERNAL) {
            showDeletionInternalErrorToast();
        }
    }

     public synchronized ErrorDeletingPrivateSpace deletePrivateSpace() {
        if (!doesPrivateSpaceExist()) {
            return ErrorDeletingPrivateSpace.DELETE_PS_ERROR_NO_PRIVATE_SPACE;
        }

        try {
            Log.i(TAG, "Deleting Private space with id: " + mUserHandle.getIdentifier());
            if (mUserManager.removeUser(mUserHandle)) {
                Log.i(TAG, "Private space deleted");
                mUserHandle = null;

                return ErrorDeletingPrivateSpace.DELETE_PS_ERROR_NONE;
            } else {
                Log.e(TAG, "Failed to delete private space");
            }
        } catch (Exception e) {
            Log.e(TAG, "Error deleting private space", e);
        }
        return ErrorDeletingPrivateSpace.DELETE_PS_ERROR_INTERNAL;
    }   

可以看到是通過UserManager.removeUser() 刪除私密空間吴裤,桌面收到Intent.ACTION_PROFILE_REMOVED,流程同 Launcher****初始化私密空間應用

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市溺健,隨后出現(xiàn)的幾起案子麦牺,更是在濱河造成了極大的恐慌,老刑警劉巖鞭缭,帶你破解...
    沈念sama閱讀 212,816評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件剖膳,死亡現(xiàn)場離奇詭異,居然都是意外死亡缚去,警方通過查閱死者的電腦和手機潮秘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來易结,“玉大人枕荞,你說我怎么就攤上這事「愣” “怎么了躏精?”我有些...
    開封第一講書人閱讀 158,300評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長鹦肿。 經(jīng)常有香客問我矗烛,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,780評論 1 285
  • 正文 為了忘掉前任瞭吃,我火速辦了婚禮碌嘀,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘歪架。我一直安慰自己股冗,他們只是感情好,可當我...
    茶點故事閱讀 65,890評論 6 385
  • 文/花漫 我一把揭開白布和蚪。 她就那樣靜靜地躺著止状,像睡著了一般。 火紅的嫁衣襯著肌膚如雪攒霹。 梳的紋絲不亂的頭發(fā)上怯疤,一...
    開封第一講書人閱讀 50,084評論 1 291
  • 那天,我揣著相機與錄音催束,去河邊找鬼集峦。 笑死,一個胖子當著我的面吹牛抠刺,可吹牛的內(nèi)容都是我干的少梁。 我是一名探鬼主播,決...
    沈念sama閱讀 39,151評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼矫付,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了第焰?” 一聲冷哼從身側(cè)響起买优,我...
    開封第一講書人閱讀 37,912評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎挺举,沒想到半個月后杀赢,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,355評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡湘纵,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,666評論 2 327
  • 正文 我和宋清朗相戀三年脂崔,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片梧喷。...
    茶點故事閱讀 38,809評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡砌左,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出铺敌,到底是詐尸還是另有隱情汇歹,我是刑警寧澤,帶...
    沈念sama閱讀 34,504評論 4 334
  • 正文 年R本政府宣布偿凭,位于F島的核電站产弹,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏弯囊。R本人自食惡果不足惜痰哨,卻給世界環(huán)境...
    茶點故事閱讀 40,150評論 3 317
  • 文/蒙蒙 一胶果、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧斤斧,春花似錦早抠、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至锐秦,卻和暖如春咪奖,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背酱床。 一陣腳步聲響...
    開封第一講書人閱讀 32,121評論 1 267
  • 我被黑心中介騙來泰國打工羊赵, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人扇谣。 一個月前我還...
    沈念sama閱讀 46,628評論 2 362
  • 正文 我出身青樓昧捷,卻偏偏與公主長得像,于是被迫代替她去往敵國和親罐寨。 傳聞我的和親對象是個殘疾皇子靡挥,可洞房花燭夜當晚...
    茶點故事閱讀 43,724評論 2 351

推薦閱讀更多精彩內(nèi)容