Create User流程

User创建基本流程

以创建manage profile user的代码为例。

frameworks/base/services/core/java/com/android/server/pm/UserManagerService.java

    private void createProfile(String profileName) throws ProvisioningException {

        ProvisionLogger.logd("Creating managed profile with name " + profileName);

        mManagedProfileOrUserInfo = mUserManager.createProfileForUser(profileName,
                UserInfo.FLAG_MANAGED_PROFILE | UserInfo.FLAG_DISABLED,
                Process.myUserHandle().getIdentifier());

        ...
    }

跳转到UserManagerService.java中

    public UserInfo createProfileForUser(String name, int flags, int userId) {
        checkManageOrCreateUsersPermission(flags);
        return createUserInternal(name, flags, userId);
    }
    private UserInfo createUserInternal(String name, int flags, int parentId) {
        if (hasUserRestriction(UserManager.DISALLOW_ADD_USER, UserHandle.getCallingUserId())) {
            Log.w(LOG_TAG, "Cannot add user. DISALLOW_ADD_USER is enabled.");
            return null;
        }
        return createUserInternalUnchecked(name, flags, parentId);
    }
   private UserInfo createUserInternalUnchecked(String name, int flags, int parentId) {
        if (ActivityManager.isLowRamDeviceStatic()) {
            return null;
        }
        final boolean isGuest = (flags & UserInfo.FLAG_GUEST) != 0;
        //依据传入的参数isManagedProfile会为true
        final boolean isManagedProfile = (flags & UserInfo.FLAG_MANAGED_PROFILE) != 0;
        final boolean isRestricted = (flags & UserInfo.FLAG_RESTRICTED) != 0;
        final boolean isDemo = (flags & UserInfo.FLAG_DEMO) != 0;
        final long ident = Binder.clearCallingIdentity();
        UserInfo userInfo;
        UserData userData;
        final int userId;
        try {
            synchronized (mPackagesLock) {
                UserData parent = null;
                if (parentId != UserHandle.USER_NULL) {
                    synchronized (mUsersLock) {
                        parent = getUserDataLU(parentId);
                    }
                    if (parent == null) return null;
                }
                if (isManagedProfile && !canAddMoreManagedProfiles(parentId, false)) {
                    Log.e(LOG_TAG, "Cannot add more managed profiles for user " + parentId);
                    return null;
                }
                ...
                userId = getNextAvailableId(); //获取下一个可用的id,是个递增的数字
                Environment.getUserSystemDirectory(userId).mkdirs(); //创建user目录
                ...

                synchronized (mUsersLock) {
                    ...

                    userInfo = new UserInfo(userId, name, null, flags); //创建新的UserInfo
                    //序列号,user相关目录的inode都会有值为serialNumber的属性
                    userInfo.serialNumber = mNextSerialNumber++; 
                    long now = System.currentTimeMillis();
                    userInfo.creationTime = (now > EPOCH_PLUS_30_YEARS) ? now : 0;
                    userInfo.partial = true; //表明User是创建或者删除中,不完备
                    userInfo.lastLoggedInFingerprint = Build.FINGERPRINT;
                    userData = new UserData(); //创建UserData
                    userData.info = userInfo;  
                    mUsers.put(userId, userData);
                }
                writeUserLP(userData); //用户和用户列表存储数据到xml中
                writeUserListLP();
                if (parent != null) {
                    if (isManagedProfile) {
                        if (parent.info.profileGroupId == UserInfo.NO_PROFILE_GROUP_ID) {
                            parent.info.profileGroupId = parent.info.id;
                            writeUserLP(parent);
                        }
                        标记当前用户和新建用户的profileGroupId都为当前用户的userId,同属一个组
                        userInfo.profileGroupId = parent.info.profileGroupId;
                    }
                    ...
                }
            }
            final StorageManager storage = mContext.getSystemService(StorageManager.class);
            //生成设备加密用的user key。
            storage.createUserKey(userId, userInfo.serialNumber, userInfo.isEphemeral());
            //mPm是PackageManager,确保了用户数据目录的建立
            mPm.prepareUserData(userId, userInfo.serialNumber,
                    StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE);
            //PMS相关工作,比较多,后面具体分析
            mPm.createNewUser(userId);
            userInfo.partial = false; //创建完毕,partial可以设置成false了
            synchronized (mPackagesLock) {
                writeUserLP(userData); //再次保存数据
            }
            updateUserIds(); 更新数组mUserIds
            ...
            mPm.onNewUserCreated(userId);//赋予基本权限,并可能弹出危险权限提示UI
            //发送ACTION_USER_ADDED广播
            Intent addedIntent = new Intent(Intent.ACTION_USER_ADDED);
            addedIntent.putExtra(Intent.EXTRA_USER_HANDLE, userId);
            mContext.sendBroadcastAsUser(addedIntent, UserHandle.ALL,
                    android.Manifest.permission.MANAGE_USERS);
            MetricsLogger.count(mContext, isGuest ? TRON_GUEST_CREATED : TRON_USER_CREATED, 1);
        } finally {
            Binder.restoreCallingIdentity(ident);
        }
        return userInfo;
    }

可以看出和创建普通user没啥大区别,明显的的区别是设置了当前user和新建user的profileGroupId属性,manage profile user是必须有parent user的,不可能单独存在。


PMS在创建User流程中的工作

prepareUserData

frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java

    void prepareUserData(int userId, int userSerial, int flags) {
        synchronized (mInstallLock) {
            final StorageManager storage = mContext.getSystemService(StorageManager.class);
            for (VolumeInfo vol : storage.getWritablePrivateVolumes()) {
                final String volumeUuid = vol.getFsUuid();
                prepareUserDataLI(volumeUuid, userId, userSerial, flags, true);
            }
        }
    }

枚举所有的内部可写存储,然后转到prepareUserDataLI,顺道先看下getWritablePrivateVolumes的代码

frameworks/base/core/java/android/os/storage/StorageManager.java

    public @NonNull List<VolumeInfo> getWritablePrivateVolumes() {
            ...
            final ArrayList<VolumeInfo> res = new ArrayList<>();
            for (VolumeInfo vol : mMountService.getVolumes(0)) {
                if (vol.getType() == VolumeInfo.TYPE_PRIVATE && vol.isMountedWritable()) {
                    res.add(vol);
                }
            }
            return res;
            ...
    }
Volume的实例都在frameworks/base/services/core/java/com/android/server/MountService.java中创建

    private void addInternalVolumeLocked() {
        // Create a stub volume that represents internal storage
        final VolumeInfo internal = new VolumeInfo(VolumeInfo.ID_PRIVATE_INTERNAL,
                VolumeInfo.TYPE_PRIVATE, null, null);
        internal.state = VolumeInfo.STATE_MOUNTED;
        internal.path = Environment.getDataDirectory().getAbsolutePath();
        mVolumes.put(internal.id, internal);
    }
本文不会涉及MountService的具体流程,通过addInternalVolumeLocked可以看出VolumeInfo.TYPE_PRIVATE实际就是指内部存储,不对外开放。

回到PackageManagerService,继续看prepareUserDataLI

 private void prepareUserDataLI(String volumeUuid, int userId, int userSerial, int flags,
            boolean allowRecover) {
        // Prepare storage and verify that serial numbers are consistent; if
        // there's a mismatch we need to destroy to avoid leaking data
        final StorageManager storage = mContext.getSystemService(StorageManager.class);
        try {
            //第一段
            storage.prepareUserStorage(volumeUuid, userId, userSerial, flags);

            //第二段
            if ((flags & StorageManager.FLAG_STORAGE_DE) != 0 && !mOnlyCore) {
                UserManagerService.enforceSerialNumber(
                        Environment.getDataUserDeDirectory(volumeUuid, userId), userSerial);
                if (Objects.equals(volumeUuid, StorageManager.UUID_PRIVATE_INTERNAL)) {
                    UserManagerService.enforceSerialNumber(
                            Environment.getDataSystemDeDirectory(userId), userSerial);
                }
            }
            if ((flags & StorageManager.FLAG_STORAGE_CE) != 0 && !mOnlyCore) {
                UserManagerService.enforceSerialNumber(
                        Environment.getDataUserCeDirectory(volumeUuid, userId), userSerial);
                if (Objects.equals(volumeUuid, StorageManager.UUID_PRIVATE_INTERNAL)) {
                    UserManagerService.enforceSerialNumber(
                            Environment.getDataSystemCeDirectory(userId), userSerial);
                }
            }

            //第三段
            synchronized (mInstallLock) {
                mInstaller.createUserData(volumeUuid, userId, userSerial, flags);
            }
        } catch (Exception e) {
            logCriticalInfo(Log.WARN, "Destroying user " + userId + " on volume " + volumeUuid
                    + " because we failed to prepare: " + e);
            destroyUserDataLI(volumeUuid, userId,
                    StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE);

            if (allowRecover) {
                // Try one last time; if we fail again we're really in trouble
                prepareUserDataLI(volumeUuid, userId, userSerial, flags, false);
            }
        }
    }

 第一段

代码会走到MountService.java

    public void prepareUserStorage(String volumeUuid, int userId, int serialNumber, int flags) {
          ...
            mCryptConnector.execute("cryptfs", "prepare_user_storage", escapeNull(volumeUuid),
                    userId, serialNumber, flags);
          ...
    }
mCryptConnector是连接通过socket连接cryptd daemon, 然后运行cryptfs中的prepare_user_storage方法。中间流程不再叙述,最后跳转到system/vold/Ext4Crypt.cpp的e4crypt_prepare_user_storage
 bool e4crypt_prepare_user_storage(const char* volume_uuid, userid_t user_id, int serial,
        int flags) {
    LOG(DEBUG) << "e4crypt_prepare_user_storage for volume " << escape_null(volume_uuid)
               << ", user " << user_id << ", serial " << serial << ", flags " << flags;

    if (flags & FLAG_STORAGE_DE) {
        // DE_sys key
        ...
        //路径地址,实际上就是/data/user_de/
        auto user_de_path = android::vold::BuildDataUserDePath(volume_uuid, user_id);

        ...
        #if MANAGE_MISC_DIRS
            if (!prepare_dir(misc_legacy_path, 0750, multiuser_get_uid(user_id, AID_SYSTEM),
                    multiuser_get_uid(user_id, AID_EVERYBODY))) return false;
        #endif  
        ...
        //创建目录并设置目录权限
        if (!prepare_dir(user_de_path, 0771, AID_SYSTEM, AID_SYSTEM)) return false;

        // For now, FBE is only supported on internal storage        
        if (e4crypt_is_native() && volume_uuid == nullptr) {
            //使用FBE会走这里
            std::string de_raw_ref;
            if (!lookup_key_ref(s_de_key_raw_refs, user_id, &de_raw_ref)) return false;
            if (!ensure_policy(de_raw_ref, system_de_path)) return false;
            if (!ensure_policy(de_raw_ref, misc_de_path)) return false;
            if (!ensure_policy(de_raw_ref, user_de_path)) return false;
        }
    }

    if (flags & FLAG_STORAGE_CE) {
        ...
    }

    return true;
}
prepare_dir最终会调用到系统C库system/core/libcutils/fs.c中的fs_prepare_path_impl,代码中省略了很多其它路径,因为原理是相同的
e4crypt_is_native方法在system/extras/ext4_utils/ext4_crypt.cpp

bool e4crypt_is_native() {
    char value[PROPERTY_VALUE_MAX];
    property_get("ro.crypto.type", value, "none");
    return !strcmp(value, "file");
}
查看手机e4crypt_is_native是返回false的,系统属性是block。
FBE是file-based encryption,加密方式的一种,文件加密详细见http://blog.csdn.net/myfriend0/article/details/77094890,文中也解释了CE和DE是什么
    凭据加密 (CE) 存储空间:这是默认存储位置,只有在用户解锁设备后才可用。
    设备加密 (DE) 存储空间:在直接启动模式期间以及用户解锁设备后均可用。
CE和DE流程类似,不贴代码了。
第一段代码的作用就是创建user加密所需相应目录和设置权限。注意最后并没有地方使用serial参数

第二段

先看两个if语句中的判断条件,flag是满足的,那么就要看mOnlyCore,mOnlyCore是在PackageManagerService构造方法中初始化的,而构造方法唯一使用的地方是main方法。

 public static PackageManagerService main(Context context, Installer installer,
            boolean factoryTest, boolean onlyCore) {
        // Self-check for initial settings.
        PackageManagerServiceCompilerMapping.checkProperties();

        PackageManagerService m = new PackageManagerService(context, installer,
                factoryTest, onlyCore);
        m.enableSystemUserPackages();
        ServiceManager.addService("package", m);
        return m;
    }
main方法被frameworks/base/services/java/com/android/server/SystemServer.java调用
private void startBootstrapServices() {
        ...
        mIsAlarmBoot = SystemProperties.getBoolean("ro.alarm_boot", false);
        if (ENCRYPTING_STATE.equals(cryptState)) {
            Slog.w(TAG, "Detected encryption in progress - only parsing core apps");
            mOnlyCore = true;
        } else if (ENCRYPTED_STATE.equals(cryptState)) {
            Slog.w(TAG, "Device encrypted - only parsing core apps");
            mOnlyCore = true;
        } else if (mIsAlarmBoot) {
            // power off alarm mode is similar to encryption mode. Only power off alarm
            // applications will be parsed by packageParser. Some services or settings are
            // not necessary to power off alarm mode. So reuse mOnlyCore for power off alarm
            // mode.
            mOnlyCore = true;
        }
        ...
  }
看出在设备加密或关机闹钟特殊情况下mOnlyCore为true,在一般情况下mOnlyCore是false的,所以prepareUserDataLI的头两个if语句都是满足条件的。
然后第二段代码中剩下的唯一核心方法就是enforceSerialNumber
 public static void enforceSerialNumber(File file, int serialNumber) throws IOException {
        ...

        final int foundSerial = getSerialNumber(file);
        Slog.v(LOG_TAG, "Found " + file + " with serial number " + foundSerial);

        if (foundSerial == -1) {
            Slog.d(LOG_TAG, "Serial number missing on " + file + "; assuming current is valid");
            try {
                setSerialNumber(file, serialNumber);
            } catch (IOException e) {
                Slog.w(LOG_TAG, "Failed to set serial number on " + file, e);
            }

        } else if (foundSerial != serialNumber) {
            throw new IOException("Found serial number " + foundSerial
                    + " doesn't match expected " + serialNumber);
        }
    }


    private static int getSerialNumber(File file) throws IOException {
        try {
            final byte[] buf = new byte[256];
            final int len = Os.getxattr(file.getAbsolutePath(), XATTR_SERIAL, buf);
            final String serial = new String(buf, 0, len);
            try {
                return Integer.parseInt(serial);
            } catch (NumberFormatException e) {
                throw new IOException("Bad serial number: " + serial);
            }
        } catch (ErrnoException e) {
            if (e.errno == OsConstants.ENODATA) {
                return -1;
            } else {
                throw e.rethrowAsIOException();
            }
        }
    }
getxattr参见https://baike.baidu.com/item/getxattr/1370228?fr=aladdin,是从inode中获取属性。
第一段代码并没有使用userinfo中的serialNumber,所以第一次走到getSerialNumber返回的肯定是-1,enforceSerialNumber肯定会走setSerialNumber。
  private static void setSerialNumber(File file, int serialNumber)
            throws IOException {
        try {
            final byte[] buf = Integer.toString(serialNumber).getBytes(StandardCharsets.UTF_8);
            Os.setxattr(file.getAbsolutePath(), XATTR_SERIAL, buf, OsConstants.XATTR_CREATE);
        } catch (ErrnoException e) {
            throw e.rethrowAsIOException();
        }
    }
设置serialNumber到inode的属性列表中。
第二段代码是往user加密所需相应目录inode的属性中加入了key为user.serial,value为userinfo的serialNumber。

第三段

mInstaller类似与MountService中的mCryptConnector,通过socket连接installd daemon完成工作。
installd代码位于frameworks/native/cmds/installd
执行了installd中的create_user_data方法,代码在commands.cpp
int create_user_data(const char *uuid, userid_t userid, int user_serial ATTRIBUTE_UNUSED,
        int flags) {
    if (flags & FLAG_STORAGE_DE) {
        if (uuid == nullptr) {
            return ensure_config_user_dirs(userid);
        }
    }
    return 0;
}
两个if的条件都是满足的
utils.cpp
int ensure_config_user_dirs(userid_t userid) {
    // writable by system, readable by any app within the same user
    const int uid = multiuser_get_uid(userid, AID_SYSTEM);
    const int gid = multiuser_get_uid(userid, AID_EVERYBODY);

    // Ensure /data/misc/user/<userid> exists
    auto path = create_data_misc_legacy_path(userid);
    return fs_prepare_dir(path.c_str(), 0750, uid, gid);
}
创建了/data/misc/user/<userid>目录并设置了新的权限,注意这段代码e4crypt_prepare_user_storage中已经覆盖。
不过MANAGE_MISC_DIRS为0,所以e4crypt_prepare_user_storage中的相同代码其实没有走。
另一个注意点就是uid和gid是一句userid生成的,所以该目录的linux层面权限检查会更严格。

createNewUser

    void createNewUser(int userId) {
        synchronized (mInstallLock) {
            mSettings.createNewUserLI(this, mInstaller, userId); //第一段
        }
        synchronized (mPackages) {
            scheduleWritePackageRestrictionsLocked(userId); //第二段
            scheduleWritePackageListLocked(userId);//第三段
            applyFactoryDefaultBrowserLPw(userId);//第四段
            primeDomainVerificationsLPw(userId);//第五段
        }
    }

第一段

 void createNewUserLI(@NonNull PackageManagerService service, @NonNull Installer installer,
            int userHandle) {
        ...
        for (int i = 0; i < packagesCount; i++) {
            if (names[i] == null) {
                continue;
            }
            // TODO: triage flags!
            final int flags = StorageManager.FLAG_STORAGE_CE | StorageManager.FLAG_STORAGE_DE;
            try {
                installer.createAppData(volumeUuids[i], names[i], userHandle, flags, appIds[i],
                        seinfos[i], targetSdkVersions[i]);
            } catch (InstallerException e) {
                Slog.w(TAG, "Failed to prepare app data", e);
            }
        }
        synchronized (mPackages) {
            applyDefaultPreferredAppsLPw(service, userHandle);
        }
    }
 create_app_data位于frameworks/native/cmds/installd/commands.cpp
int create_app_data(const char *uuid, const char *pkgname, userid_t userid, int flags,
        appid_t appid, const char* seinfo, int target_sdk_version) {
    uid_t uid = multiuser_get_uid(userid, appid);
    mode_t target_mode = target_sdk_version >= MIN_RESTRICTED_HOME_SDK_VERSION ? 0700 : 0751;
    if (flags & FLAG_STORAGE_CE) {
        auto path = create_data_user_ce_package_path(uuid, userid, pkgname);
        //创建cache和code_cache目录
        if (prepare_app_dir(path, target_mode, uid) ||
                prepare_app_dir(path, "cache", 0771, uid) ||
                prepare_app_dir(path, "code_cache", 0771, uid)) {
            return -1;
        }

        // Consider restorecon over contents if label changed
        //restorecon命令用来恢复SELinux文件属性即恢复文件的安全上下文。参见http://man.linuxde.net/restorecon
        if (restorecon_app_data_lazy(path, seinfo, uid) ||
                restorecon_app_data_lazy(path, "cache", seinfo, uid) ||
                restorecon_app_data_lazy(path, "code_cache", seinfo, uid)) {
            return -1;
        }

        // Remember inode numbers of cache directories so that we can clear
        // contents while CE storage is locked
        //标准C库方法,往inode中写属性
        if (write_path_inode(path, "cache", kXattrInodeCache) ||
                write_path_inode(path, "code_cache", kXattrInodeCodeCache)) {
            return -1;
        }
    }
    if (flags & FLAG_STORAGE_DE) {
        auto path = create_data_user_de_package_path(uuid, userid, pkgname);
        if (prepare_app_dir(path, target_mode, uid)) {
            // TODO: include result once 25796509 is fixed
            return 0;
        }

        // Consider restorecon over contents if label changed
        if (restorecon_app_data_lazy(path, seinfo, uid)) {
            return -1;
        }

        //对JIT Profile的支持,参见https://www.zhihu.com/question/37389356
        //运行app获取profile数据,然后profile会提高jit效率,profile当然需要地方存放
        if (property_get_bool("dalvik.vm.usejitprofiles")) {
            const std::string profile_path = create_data_user_profile_package_path(userid, pkgname);
            // read-write-execute only for the app user.
            if (fs_prepare_dir_strict(profile_path.c_str(), 0700, uid, uid) != 0) {
                PLOG(ERROR) << "Failed to prepare " << profile_path;
                return -1;
            }
            std::string profile_file = create_primary_profile(profile_path);
            // read-write only for the app user.
            if (fs_prepare_file_strict(profile_file.c_str(), 0600, uid, uid) != 0) {
                PLOG(ERROR) << "Failed to prepare " << profile_path;
                return -1;
            }
            const std::string ref_profile_path = create_data_ref_profile_package_path(pkgname);
            // dex2oat/profman runs under the shared app gid and it needs to read/write reference
            // profiles.
            appid_t shared_app_gid = multiuser_get_shared_app_gid(uid);
            if (fs_prepare_dir_strict(
                    ref_profile_path.c_str(), 0700, shared_app_gid, shared_app_gid) != 0) {
                PLOG(ERROR) << "Failed to prepare " << ref_profile_path;
                return -1;
            }
        }
    }
    return 0;
}
创建cache和code_cache目录,设置selinux,设置JIT profile相关目录。
最后看看applyDefaultPreferredAppsLPw
  void applyDefaultPreferredAppsLPw(PackageManagerService service, int userId) {
        // First pull data from any pre-installed apps.
        //应用程序默认值
        for (PackageSetting ps : mPackages.values()) {
            if ((ps.pkgFlags&ApplicationInfo.FLAG_SYSTEM) != 0 && ps.pkg != null
                    && ps.pkg.preferredActivityFilters != null) {
                ArrayList<PackageParser.ActivityIntentInfo> intents
                        = ps.pkg.preferredActivityFilters;
                for (int i=0; i<intents.size(); i++) {
                    PackageParser.ActivityIntentInfo aii = intents.get(i);
                    applyDefaultPreferredActivityLPw(service, aii, new ComponentName(
                            ps.name, aii.activity.className), userId);
                }
            }
        }

        //后续从etc/preferred-apps读取xml来设置默认应用,省略
        ...
    }

第二段

    void scheduleWritePackageRestrictionsLocked(int userId) {
        final int[] userIds = (userId == UserHandle.USER_ALL)
                ? sUserManager.getUserIds() : new int[]{userId};
        for (int nextUserId : userIds) {
            if (!sUserManager.exists(nextUserId)) return;
            mDirtyUsers.add(nextUserId);
            if (!mHandler.hasMessages(WRITE_PACKAGE_RESTRICTIONS)) {
                mHandler.sendEmptyMessageDelayed(WRITE_PACKAGE_RESTRICTIONS, WRITE_SETTINGS_DELAY);
            }
        }
    }
WRITE_PACKAGE_RESTRICTIONS消息处理
                        for (int userId : mDirtyUsers) {
                            mSettings.writePackageRestrictionsLPr(userId);
                        }
                        mDirtyUsers.clear();
跳转到writePackageRestrictionsLPr,依据当前的PackageSetting写新user的package-restrictions.xml文件
其中所有的属性都定义与frameworks/base/services/core/java/com/android/server/pm/Settings.java。例如
    private static final String ATTR_HIDDEN = "hidden";
    private static final String ATTR_SUSPENDED = "suspended";
    private static final String ATTR_BLOCK_UNINSTALL = "blockUninstall";

第三段

void scheduleWritePackageListLocked(int userId) {
        if (!mHandler.hasMessages(WRITE_PACKAGE_LIST)) {
            Message msg = mHandler.obtainMessage(WRITE_PACKAGE_LIST);
            msg.arg1 = userId;
            mHandler.sendMessageDelayed(msg, WRITE_SETTINGS_DELAY);
        }
    }
写packages.list文件,该文件内容可参见代码注释
                // pkgName    - package name
                // userId     - application-specific user id
                // debugFlag  - 0 or 1 if the package is debuggable.
                // dataPath   - path to package's data path
                // seinfo     - seinfo label for the app (assigned at install time)
                // gids       - supplementary gids this app launches with

第四段

applyFactoryDefaultBrowserLPw设置系统资源com.android.internal.R.string.default_browser中定义的浏览器为默认浏览器

第五段

primeDomainVerificationsLPw处理linked app,参见http://www.cnblogs.com/android-zcq/p/5882012.html。
linked app指能通过浏览器url调用起的app,例如
<intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data
       android:scheme="http"
       android:mimeType="application/pdf"/>
</intent-filter>
你在浏览器中输入  http://www.devdiv.com/1.pdf ,那么这个activity自动被浏览器给调起来。
最终该设置也会写入相关文件



猜你喜欢

转载自blog.csdn.net/firedancer0089/article/details/78327265
今日推荐