精华内容
下载资源
问答
  • Android 8.0 系统自带 Settings Android 8.0 系统自带 Settings
  • 国内连接maven官方的仓库更新依赖,收集一些国内快速的maven仓库镜像以备用。 settings.xml配置好的国内私服,直接可以下载使用!
  • maven的settings文件,阿里云镜像,快到飞起。给maven初体验者作为参考
  • maven settings.xml配置文件,亲试无问题,可以使用,eclipse和Myeclipse都可以
  • 之前找了很多个镜像配置,一直无法更新,好不容易找了一个可以用的,希望可以帮到大家
  • Android Settings

    热门讨论 2012-12-24 14:51:05
    已经增加了所需要的库,并且修改了包名,可以直接安装。
  • Maven Settings.xml国内资源配置文件 官方绿色版

    千次下载 热门讨论 2016-07-21 22:59:41
    maven国内资源资源配置以及nexus私服的配置fangs
  • settings.apk

    2013-05-15 15:14:59
    Android settings.apk,修复了移动热点界面旋转屏报错异常.
  • SettingsProvider简单分析

    千次阅读 2020-01-11 10:52:46
    SettingsProvider顾名思义是一个提供数据共享的Provider,SettingsProvider和Android系统其它Provider有很多不一样的地方: 1.SettingsProvider只接受int float String等基本类型的数据; 2.SettingsProvider由...

    SettingsProvider顾名思义是一个提供数据共享的Provider,SettingsProvider和Android系统其它Provider有很多不一样的地方:

    1.SettingsProvider只接受int float String等基本类型的数据;

    2.SettingsProvider由Android系统frameowrk进行了封装

    3.SettingsProvider的数据由键值对组成

    SettingsProvider有点类似Android的properties系统(Android属性系统):SystemProperites。SystemProperites除具有SettingsProvider以上的三个特性,SettingsProvider和SystemProperties的不同点在于:

    1、数据保存方式不同:SystemProperies的数据保存属性文件中(/system/build.prop等),开机后会被加载到system properites store;SettingsProvider的数据保存在文件/data/system/users/0/settings_***.xml和数据库settings.db中;

    2、作用范围不同:SettingsProvider可以实现跨进程、跨层次调用,即底层的c/c++可调用,java层也可以调用;SettingsProvider只能在java层调用

    3、公开程度不同:SettingsProvider有部分功能上层第三方APP可以使用,SystemProvider上层第三方APP不可以使用。

    在Android 6.0版本时,SettingsProvider被重构,Android从性能、安全等方面考虑,把SettingsProvider中原本保存在settings.db中的数据,目前全部保存在XML文件中。

    数据分类

    SettingsProvider主要有三种类型数据:Global、System、Secure三种类型

    Global:所有的偏好设置对系统的所有用户公开,第三方有读没有写的权限

    System:包含各种各样的用户偏好系统设置

    Secure:安全性的用户偏好系统设置,第三方APP有读没有写的权限

     Android6.0版本之后SettingsProvider管理的用户偏好设置数据从原来的settings.db数据库文件中转移到下面的3个xml文件中:data/system/users/0/settings_global.xml
    data/system/users/userid/settings_system.xml
    data/system/users/userid/settings_secure.xml

    主要源码

    SettingsProvider的代码数量不多,主要包含如下的java文件:

    frameworks/base/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java

    frameworks/base/packages/SettingsProvider/src/com/android/providers/settings/GenerationRegistry.java

    frameworks/base/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java

    frameworks/base/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java

    frameworks/base/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java

    frameworks/base/core/java/android/provider/Settings.java

    AndroidManifest.xml配置

    SettingsProvider的AndroidManifest.xml文件对应和ContentProvider的配置如下:

    <manifest ......
            android:sharedUserId="android.uid.system">
    
        <application android:allowClearUserData="false"
                     android:label="@string/app_label"
                     android:process="system"
                     ......
                     android:directBootAware="true">
    
            <provider android:name="SettingsProvider"
                      android:authorities="settings"
                      android:multiprocess="false"
                      android:exported="true"
                      android:singleUser="true"
                      android:initOrder="100" />
        </application>
    </manifest>

    上面的Manifest配置由sharedUserId可知,SettingsProvider运行在系统进程中,定义的ContentProvider实现类是SettingsProvider,Uri凭证是settings。

    SettingsProvider的启动过程

    系统启动SettingsProvider是在frameworks/base/services/java/com/android/server/SystemServer.java

    private void startOtherServices() {
    	//省略一部分代码
    	//...
    	
    	traceBeginAndSlog("InstallSystemProviders");
    	mActivityManagerService.installSystemProviders();
    	// Now that SettingsProvider is ready, reactivate SQLiteCompatibilityWalFlags
    	SQLiteCompatibilityWalFlags.reset();
    	traceEnd();
    	
    	//省略一部分代码
    	//...
    }
    

    frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java

    public final void installSystemProviders() {
    	// 1、获取系统中所有的Provider,最终通过调用包管理PackageManagerService中的
    //queryContentProviders()方法来查询所有的Provider
        List<ProviderInfo> providers;
        synchronized (this) {
            ProcessRecord app = mProcessNames.get("system", SYSTEM_UID);
            providers = generateApplicationProvidersLocked(app);
            if (providers != null) {
                for (int i=providers.size()-1; i>=0; i--) {
                    ProviderInfo pi = (ProviderInfo)providers.get(i);
                    if ((pi.applicationInfo.flags&ApplicationInfo.FLAG_SYSTEM) == 0) {
                        Slog.w(TAG, "Not installing system proc provider " + pi.name
                                + ": not system .apk");
                        providers.remove(i);
                    }
                }
            }
        }
    	// 2、调用ActivityThread中的installSystemProviders来完成Provider的安装,包括
    //SettingsProvider
        if (providers != null) {
            mSystemThread.installSystemProviders(providers);
        }
    
        synchronized (this) {
            mSystemProvidersInstalled = true;
        }
    
        mConstants.start(mContext.getContentResolver());
        mCoreSettingsObserver = new CoreSettingsObserver(this);
        mFontScaleSettingObserver = new FontScaleSettingObserver();
        mDevelopmentSettingsObserver = new DevelopmentSettingsObserver();
        GlobalSettingsToPropertiesMapper.start(mContext.getContentResolver());
    
    	// 3、Provider启动完成
        // Now that the settings provider is published we can consider sending
        // in a rescue party.
        RescueParty.onSettingsProviderPublished(mContext);
    
        //mUsageStatsService.monitorPackages();
    }
    

    其中第二步中,installSystemProviders()会启动SettingsProvider。启动SettingsProvider即运行SettingsProvider,首先调用OnCreate()方法。

    frameworks/base/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java

    @Override
    public boolean onCreate() {
        Settings.setInSystemServer();
    
        // fail to boot if there're any backed up settings that don't have a non-null validator
        ensureAllBackedUpSystemSettingsHaveValidators();
        ensureAllBackedUpGlobalSettingsHaveValidators();
        ensureAllBackedUpSecureSettingsHaveValidators();
    
        synchronized (mLock) {
            mUserManager = UserManager.get(getContext());
            mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
            mPackageManager = AppGlobals.getPackageManager();
            mHandlerThread = new HandlerThread(LOG_TAG,
                    Process.THREAD_PRIORITY_BACKGROUND);
            mHandlerThread.start();
            mHandler = new Handler(mHandlerThread.getLooper());
            mSettingsRegistry = new SettingsRegistry();
        }
        mHandler.post(() -> {
            //注册广播接收,关心设备用户变化以及APP卸载的广播
            registerBroadcastReceivers();
            startWatchingUserRestrictionChanges();
        });
        ServiceManager.addService("settings", new SettingsService(this));
        return true;
    }
    

    重点代码分析,上述代码中首先实例化一个HandlerThread的实例mHandlerThread,优先级是Process.THREAD_PRIORITY_BACKGROUND。关键的部分是:mSettingsRegistry = new SettingsRegistry()。

    SettingsRegistry是SettingsProvider.java的内部类,先分析构造方法

        public SettingsRegistry() {
            mHandler = new MyHandler(getContext().getMainLooper());
            // 1、对xml的修改做版本管理
            mGenerationRegistry = new GenerationRegistry(mLock);
            // 2、创建Backup,做系统备份
            mBackupManager = new BackupManager(getContext());
            // 3、迁移所有的系统设置数据
            migrateAllLegacySettingsIfNeeded();
            syncSsaidTableOnStart();
        }
    

    migrateAllLegacySettingsIfNeeded()方法,从命名上是迁移settings数据。

    private void migrateAllLegacySettingsIfNeeded() {
                synchronized (mLock) {
                    final int key = makeKey(SETTINGS_TYPE_GLOBAL, UserHandle.USER_SYSTEM);
                    File globalFile = getSettingsFile(key);
                    if (SettingsState.stateFileExists(globalFile)) {
                        return;
                    }
    
                    mSettingsCreationBuildId = Build.ID;
    
                    final long identity = Binder.clearCallingIdentity();
                    try {
                        //获取系统中所有的用户(多用户,一般user0)
                        List<UserInfo> users = mUserManager.getUsers(true);
    
                        final int userCount = users.size();
                        for (int i = 0; i < userCount; i++) {
                            final int userId = users.get(i).id;
                            //关键代码,通过DatabaseHelper类创建数据库settings.db,并使用默认设置 
                            //对数据库表数据初始化
                            DatabaseHelper dbHelper = new DatabaseHelper(getContext(), userId);
                            SQLiteDatabase database = dbHelper.getWritableDatabase();
                            migrateLegacySettingsForUserLocked(dbHelper, database, userId);
                            //关键代码,生成和初始化xml文件
                            // Upgrade to the latest version.
                            UpgradeController upgrader = new UpgradeController(userId);
                            upgrader.upgradeIfNeededLocked();
    
                            // Drop from memory if not a running user.
                            if (!mUserManager.isUserRunning(new UserHandle(userId))) {
                                removeUserStateLocked(userId, false);
                            }
                        }
                    } finally {
                        Binder.restoreCallingIdentity(identity);
                    }
                }
            }

    上面的代码首先是调用了makeKey()方法,所谓makeKey()就是和上文中的数据分类小章节中提到的System、Global和Secure三种key。然后调用getSettingsFile()方法获取到一个File对象的实例,如下:

            private File getSettingsFile(int key) {
                if (isGlobalSettingsKey(key)) {
                    final int userId = getUserIdFromKey(key);
                    return new File(Environment.getUserSystemDirectory(userId),
                            SETTINGS_FILE_GLOBAL);
                } else if (isSystemSettingsKey(key)) {
                    final int userId = getUserIdFromKey(key);
                    return new File(Environment.getUserSystemDirectory(userId),
                            SETTINGS_FILE_SYSTEM);
                } else if (isSecureSettingsKey(key)) {
                    final int userId = getUserIdFromKey(key);
                    return new File(Environment.getUserSystemDirectory(userId),
                            SETTINGS_FILE_SECURE);
                } else if (isSsaidSettingsKey(key)) {
                    final int userId = getUserIdFromKey(key);
                    return new File(Environment.getUserSystemDirectory(userId),
                            SETTINGS_FILE_SSAID);
                } else {
                    throw new IllegalArgumentException("Invalid settings key:" + key);
                }
            }

    上面的代码中对Global、System、Secure分别生成一个File对象实例,它们的File对象分别对应的文件是:

    • /data/system/users/0/settings_global.xml
    • /data/system/users/0/settings_system.xml
    • /data/system/users/0/settings_secure.xml
    • 在8.0以后,每位使用者所安装的每个 APP (package) 都会产生一组独立的 ID (SSAID),并将描述档案放置 “/data/system/users/0/settings_ssaid.xml"。

    在migrateAllLegacySettingsIfNeeded()方法,实例化一股DatabaseHelper,DatabaseHelper是SQLiteOpenHelper的子类。

    frameworks/base/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java

    class DatabaseHelper extends SQLiteOpenHelper {
    
    	private static final String DATABASE_NAME = "settings.db";
    
    	public DatabaseHelper(Context context, int userHandle) {
          super(context, dbNameForUser(userHandle), null, DATABASE_VERSION);
          mContext = context;
          mUserHandle = userHandle;
      	}
    
    	static String dbNameForUser(final int userHandle) {
         // The owner gets the unadorned db name;
         if (userHandle == UserHandle.USER_SYSTEM) {
             return DATABASE_NAME;
         } else {
             // Place the database in the user-specific data tree so that it's
             // cleaned up automatically when the user is deleted.
             File databaseFile = new File(
                     Environment.getUserSystemDirectory(userHandle), DATABASE_NAME);
             // If databaseFile doesn't exist, database can be kept in memory. It's safe because the
             // database will be migrated and disposed of immediately after onCreate finishes
             if (!databaseFile.exists()) {
                 Log.i(TAG, "No previous database file exists - running in in-memory mode");
                 return null;
             }
             return databaseFile.getPath();
         }
    	}
    
    	@Override
    	public void onCreate(SQLiteDatabase db) {
    	   //创建System数据表
    	   db.execSQL("CREATE TABLE system (" +
    	               "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
    	               "name TEXT UNIQUE ON CONFLICT REPLACE," +
    	               "value TEXT" +
    	               ");");
    	   db.execSQL("CREATE INDEX systemIndex1 ON system (name);");
    	
    		// 1、创建表,createSecureTable,Secure数据表
    	   createSecureTable(db);
    	
    	   // 2、创建表,createGlobalTable,Global数据表
    	   // Only create the global table for the singleton 'owner/system' user
    	   if (mUserHandle == UserHandle.USER_SYSTEM) {
    	       createGlobalTable(db);
    	   }
    	
    	   db.execSQL("CREATE TABLE bluetooth_devices (" +
    	               "_id INTEGER PRIMARY KEY," +
    	               "name TEXT," +
    	               "addr TEXT," +
    	               "channel INTEGER," +
    	               "type INTEGER" +
    	               ");");
    	
    	   db.execSQL("CREATE TABLE bookmarks (" +
    	               "_id INTEGER PRIMARY KEY," +
    	               "title TEXT," +
    	               "folder TEXT," +
    	               "intent TEXT," +
    	               "shortcut INTEGER," +
    	               "ordering INTEGER" +
    	               ");");
    	
    	   db.execSQL("CREATE INDEX bookmarksIndex1 ON bookmarks (folder);");
    	   db.execSQL("CREATE INDEX bookmarksIndex2 ON bookmarks (shortcut);");
    	
    	   // Populate bookmarks table with initial bookmarks
    	   boolean onlyCore = false;
    	   try {
    	       onlyCore = IPackageManager.Stub.asInterface(ServiceManager.getService(
    	               "package")).isOnlyCoreApps();
    	   } catch (RemoteException e) {
    	   }
    	   if (!onlyCore) {
    	       loadBookmarks(db);
    	   }
    	
    		// 3、默认音量数据填充数据库
    	   // Load initial volume levels into DB
    	   loadVolumeLevels(db);
    	
    		// 4、默认数据填充数据库
    	   // Load inital settings values
    	   loadSettings(db);
    	}
    }
    

    loadSettings()这个方法和loadVolumeLevels()方法类似,都是加载很多默认值写入到数据库中,这些默认值很大一部分被定义在文件frameworks/base/packages/SettingsProvider/res/values/defaults.xml中,也有一些来自其它地方。总之,loadVolumeLevels()和loadSettings()的作用就是在手机第一次启动时,把手机编好设置的默认值写入到数据库settings.db中。

    Settings.db数据迁移到xml中

    继续回到SettingsRegistry中的migrateAllLegacySettingsIfNeeded方法,在migrateAllLegacySettingsIfNeeded中接下来会执行migrateLegacySettingsForUserLocked方法

    private void migrateLegacySettingsForUserLocked(DatabaseHelper dbHelper, SQLiteDatabase database, int userId) {
         //1、处理System数据
         // Move over the system settings.
         final int systemKey = makeKey(SETTINGS_TYPE_SYSTEM, userId); 
         //生成对应的SettingsState对象保存在mSettingsStates中,生成setting_system.xml
         ensureSettingsStateLocked(systemKey);
         SettingsState systemSettings = mSettingsStates.get(systemKey);
         //数据迁移,将settings.db的数据迁移到settingsStates.get(systemKey);
         migrateLegacySettingsLocked(systemSettings, database, TABLE_SYSTEM);
         systemSettings.persistSyncLocked();
         
         //2、处理Secure数据
         // Move over the secure settings.
         // Do this after System settings, since this is the first thing we check when deciding
         // to skip over migration from db to xml for a secondary user.
         final int secureKey = makeKey(SETTINGS_TYPE_SECURE, userId);
         //生成对应的SettingsState对象保存在mSettingsStates中,生成settings_secure.xml
         ensureSettingsStateLocked(secureKey);
         SettingsState secureSettings = mSettingsStates.get(secureKey);
         migrateLegacySettingsLocked(secureSettings, database, TABLE_SECURE);
         //数据迁移,将settings.db的数据迁移到settings_secure.xml中
         ensureSecureSettingAndroidIdSetLocked(secureSettings);
         secureSettings.persistSyncLocked();  
    
         //3、处理Global数据   
         // Move over the global settings if owner.
         // Do this last, since this is the first thing we check when deciding
         // to skip over migration from db to xml for owner user.
         if (userId == UserHandle.USER_SYSTEM) {
             final int globalKey = makeKey(SETTINGS_TYPE_GLOBAL, userId);
             //生成对应的SettingsState对象保存在mSettingsStates中,生成settings_global.xml
             ensureSettingsStateLocked(globalKey);
             SettingsState globalSettings = mSettingsStates.get(globalKey);
             //数据迁移,将settings.db的数据迁移到settings_global.xml中
             migrateLegacySettingsLocked(globalSettings, database, TABLE_GLOBAL);
             // If this was just created
             if (mSettingsCreationBuildId != null) {
                 globalSettings.insertSettingLocked(Settings.Global.DATABASE_CREATION_BUILDID,
                         mSettingsCreationBuildId, null, true,
                         SettingsState.SYSTEM_PACKAGE_NAME);
             }
             globalSettings.persistSyncLocked();
         }
         //数据迁移完成后,是否删除settings.db,或者做一个备份
         // Drop the database as now all is moved and persisted.
         if (DROP_DATABASE_ON_MIGRATION) {
             dbHelper.dropDatabase();
         } else {
             dbHelper.backupDatabase();
         }
      }
    
    
    ensureSettingsStateLocked是一个非常重要的方法,实例化一个SettingsState对象,
    这个对象指向文件data/system/users/0/xx.xml文件,然后将settingsState放置在对象mSettingsStates中。如果是工程版本的系统,吧数据库settings.db重命名为settings.db-backup,如果是非工程版本的系统,把数据库文件删除,也会删除日志settings.db-journal.
    

    SettnigsProvider启动时会创建settings.db数据库,然后把所有的默认设置项写入到数据库,接着会把数据库中所有的设置项从数据库转移到xml文件中,随后便会对数据库执行删除操作。为什么会有这么一个过程,这些过程是否可以移除创建数据库这一步?其实这个过程是为了兼容之前的版本而设计,在SettingsProvider被Android重构后,SettingsProvider中数据库相关的代码Android已经停止更新。

      //ensureSettingsStateLocked是一个重要的方法,给每一个数据类型key,
      //创建一个对应的SettingsState对象并保存在mSettingsStates
      private void ensureSettingsStateLocked(int key) {
          if (mSettingsStates.get(key) == null) {
              final int maxBytesPerPackage = getMaxBytesPerPackageForType(getTypeFromKey(key));
              SettingsState settingsState = new SettingsState(getContext(), mLock,
                      getSettingsFile(key), key, maxBytesPerPackage, mHandlerThread.getLooper());
              mSettingsStates.put(key, settingsState);
          }
      }
    

    之后会执行migrateLegacySettingsLocked方法

    private void migrateLegacySettingsLocked(SettingsState settingsState,
            SQLiteDatabase database, String table) {
        SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
        queryBuilder.setTables(table);
    
        Cursor cursor = queryBuilder.query(database, ALL_COLUMNS,
                null, null, null, null, null);
    
        try {
            ......
    
            while (!cursor.isAfterLast()) {
                String name = cursor.getString(nameColumnIdx);
                String value = cursor.getString(valueColumnIdx);
                settingsState.insertSettingLocked(name, value,
                        SettingsState.SYSTEM_PACKAGE_NAME);
                cursor.moveToNext();
            }
        } finally {
            cursor.close();
        }
    }

    上面这个方法,查询数据库中System所有的设置,然后在while循环中把每个值的信息作为insertSettingLocked()的参数。

    frameworks/base/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java

    public boolean insertSettingLocked(String name, String value, String packageName) {
    
        Setting oldState = mSettings.get(name);
        String oldValue = (oldState != null) ? oldState.value : null;
    
        if (oldState != null) {
            ......
        } else {
            Setting state = new Setting(name, value, packageName);
            mSettings.put(name, state);
        }
        ......
    }

    上面的方法把每一个设置项封装到Setting中,接着把state放置到ArrayMap<String,Setting>的实例mSettings中。

    那么,从方法ensureSettingsStateLocked()到insertSettingsLocked()方法,这个过程表明,有一个对象SettingsState,指向文件/data/system/users/0/settings_system.xml,持有变量mSettings,而mSettings持有封装了设置项的name,value,packageName的对象Setting,也就是settings_system.xml文件中的所有设置项间接被SettingsState持有。

    在migrateLegacySettingsForUserLocked中,migrateLegacySettingsLocked方法执行完毕后,调用systemSettings.persistSyncLocked(),systemSettings是SettingsState的实例,代表的是settings_system.xml的所有设置项,persistSyncLocked()方法就是把systemSettings持有的所有的设置项从内存中固化到文件settings_system.xml中。

    封装SettingsProvider接口

    SettingsProvider是向整个Android系统提供用户偏好设置的程序,framework有一个类Settings.java对使用SettingsProvider进行了封装。

    frameworks/base/core/java/android/provider/Settings.java

    public final class Settings {
        public static final String AUTHORITY = "settings";
        public static final class Global extends NameValueTable {
            public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/global");
            ......
        }
        
        public static final class Secure extends NameValueTable {
            public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/secure");
            ......
        }
        
        public static final class System extends NameValueTable {
            public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/system");
            ......
        }
        
        private static class NameValueCache {
            private final Uri mUri;
            private final HashMap<String, String> mValues = new HashMap<String, String>();
            public String getStringForUser(ContentResolver cr, String name, final int userHandle) {
                ......
            }
            public boolean putStringForUser(ContentResolver cr, String name, String value,
                    final int userHandle) {
                ......
            }
            private IContentProvider lazyGetProvider(ContentResolver cr) {
                IContentProvider cp = null;
                synchronized (NameValueCache.this) {
                    cp = mContentProvider;
                    if (cp == null) {
                        cp = mContentProvider = cr.acquireProvider(mUri.getAuthority());
                    }
                }
                return cp;
            }
    }

    上面的代码中,分别声明了Global、Secure、System三个静态内部类,分别对应SettingsProvider中的Global、Secure、System三种数据类型。Global、Secure、System三个静态内部类会分别持有自己NameValueCache的实例变量,每个NameValueCache持有指向SettingsProvider中的SettingsProvider.java的AIDL远程调用IContentProvider,读者可以阅读《Android System Server大纲之ContentService和ContentProvider原理剖析》了解ConatentProvider的这个过程。因此,查询数据需要经过NameValueCache的getStringForUser()方法,插入数据需要经过putStringForUser()方法。同时,NameValueCache还持有一个变量mValues,用于保存查询过的设置项,以便下下次再次发起查询时,能够快速返回。

    操作SettingsProvider

    由于Settings.java对使用SettingsProvider进行了封装,所以,使用起来相当简单简洁。由于Global、Secure、System三种数据类型的使用是几乎相同,所以本文就只以Global为例对查询插入数据的过程进行分析。

    查询数据

    从SettingsProvider的Global中查询数据,查询是否是飞行模式使用方法如下:

    String globalValue = Settings.Global.getString(getContentResolver(), Settings.Global.AIRPLANE_MODE_ON);

    分析一下getString()方法

    frameworks/base/core/java/android/provider/Settings.java

    public static String getString(ContentResolver resolver, String name) {
        return getStringForUser(resolver, name, UserHandle.myUserId());
    }
    
    /** @hide */
    public static String getStringForUser(ContentResolver resolver, String name,
            int userHandle) {
        //因为在Android系统的更新中,保存在Global、Secure、System三种类型的数据存放位置有变化
        //需要添加判断兼容老版本的使用方法。
        if (MOVED_TO_SECURE.contains(name)) {
            Log.w(TAG, "Setting " + name + " has moved from android.provider.Settings.Global"
                    + " to android.provider.Settings.Secure, returning read-only value.");
            return Secure.getStringForUser(resolver, name, userHandle);
        }
        return sNameValueCache.getStringForUser(resolver, name, userHandle);
    }

    NameValueCache.getStringForUser()方法,如下

    public String getStringForUser(ContentResolver cr, String name, final int userHandle) {
        final boolean isSelf = (userHandle == UserHandle.myUserId());
        if (isSelf) {
            ......
                } else if (mValues.containsKey(name)) {
                    return mValues.get(name);
            ......
        IContentProvider cp = lazyGetProvider(cr);
    
        // Try the fast path first, not using query().  If this
        // fails (alternate Settings provider that doesn't support
        // this interface?) then we fall back to the query/table
        // interface.
        if (mCallGetCommand != null) {
            try {
                ......
                Bundle b = cp.call(cr.getPackageName(), mCallGetCommand, name, args);
                if (b != null) {
                    String value = b.getString(Settings.NameValueTable.VALUE);
                    ......
                            mValues.put(name, value);
                        }
                    return value;
                }
            } catch (RemoteException e) {
                // Not supported by the remote side?  Fall through
                // to query().
            }
        }
        Cursor c = null;
        try {
            c = cp.query(cr.getPackageName(), mUri, SELECT_VALUE, NAME_EQ_PLACEHOLDER,
                         new String[]{name}, null, null);
            String value = c.moveToNext() ? c.getString(0) : null;
            synchronized (NameValueCache.this) {
                mValues.put(name, value);
            }
            return value;
            ......
         if (c != null) c.close();
        }
    }
    }

    NameValueCache是Settings.java中的内部类,

    首先从缓存mValues变量中去找,如果没有查询到,就调用SettingsProvider的call()接口,如果call()接口也没有查询到,再调用query()接口。这里用的是call()接口,下文就以call()接口往下分析。cp.call()会调用到SettingsProvider的call()方法,读者可以阅读《Android System Server大纲之ContentService和ContentProvider原理剖析》了解ConatentProvider的这个过程。注意参数mCallGetCommand。

    frameworks/base/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java

    public Bundle call(String method, String name, Bundle args) {
        final int requestingUserId = getRequestingUserId(args);
        switch (method) {
            case Settings.CALL_METHOD_GET_GLOBAL: {
                Setting setting = getGlobalSetting(name);
                return packageValueForCallResult(setting, isTrackingGeneration(args));
            }
    
            case Settings.CALL_METHOD_GET_SECURE: {
                Setting setting = getSecureSetting(name, requestingUserId);
                return packageValueForCallResult(setting, isTrackingGeneration(args));
            }
            ......
        }
    
        return null;
    }

    上层传过来的参数的command是mCallGetCommand,即Settings.CALL_METHOD_GET_GLOBAL,所以调用getGlobalSettings(),getGlobalSetting()返回的Setting setting是封装了设置项name、value等信息的,查看getGlobalSetting()方法:

    private Setting getGlobalSetting(String name) {
        // Get the value.
        synchronized (mLock) {
            return mSettingsRegistry.getSettingLocked(SETTINGS_TYPE_GLOBAL,
                    UserHandle.USER_SYSTEM, name);
        }
    }

    通过mSettingsRegistry.getSettingLocked()继续寻找:

    public Setting getSettingLocked(int type, int userId, String name) {
        final int key = makeKey(type, userId);
    
        SettingsState settingsState = peekSettingsStateLocked(key);
        return settingsState.getSettingLocked(name);
    }

    通过peekSettingsStateLocked(key)寻找SettingsState:

    private SettingsState peekSettingsStateLocked(int key) {
        SettingsState settingsState = mSettingsStates.get(key);
        if (settingsState != null) {
            return settingsState;
        }
    
        ensureSettingsForUserLocked(getUserIdFromKey(key));
        return mSettingsStates.get(key);
    }

    获取到SettingsState settingsState,回到getSettingLocked(),调用settingsState.getSettingLocked(name):

    public Setting getSettingLocked(String name) {
        if (TextUtils.isEmpty(name)) {
            return mNullSetting;
        }
        Setting setting = mSettings.get(name);
        if (setting != null) {
            return new Setting(setting);
        }
        return mNullSetting;
    }

    到getSettingsLocked()最终获取到Setting setting。

    插入数据

    从SettingsProvider的Global中插入数据,插入飞行模式的使用方法如下:

    boolean isSuccess = Settings.System.putInt(getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);

    和查询代码一样,代码非常简洁,整个查询过程几乎类似

    frameworks/base/core/java/android/provider/Settings.java

    public static boolean putInt(ContentResolver cr, String name, int value) {
        return putString(cr, name, Integer.toString(value));
    }
    public static boolean putString(ContentResolver resolver,
            String name, String value) {
        return putStringForUser(resolver, name, value, UserHandle.myUserId());
    }
    public static boolean putStringForUser(ContentResolver resolver,
            String name, String value, int userHandle) {
        ......
        return sNameValueCache.putStringForUser(resolver, name, value, userHandle);
    }

    和查询一样,继续看putStringForUser():

    public boolean putStringForUser(ContentResolver cr, String name, String value,
            final int userHandle) {
        try {
            Bundle arg = new Bundle();
            arg.putString(Settings.NameValueTable.VALUE, value);
            arg.putInt(CALL_METHOD_USER_KEY, userHandle);
            IContentProvider cp = lazyGetProvider(cr);
            cp.call(cr.getPackageName(), mCallSetCommand, name, arg);
        } catch (RemoteException e) {
            Log.w(TAG, "Can't set key " + name + " in " + mUri, e);
            return false;
        }
        return true;
    }

    frameworks/base/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java

    public Bundle call(String method, String name, Bundle args) {
        final int requestingUserId = getRequestingUserId(args);
        switch (method) {
            ......
    
            case Settings.CALL_METHOD_PUT_GLOBAL: {
                String value = getSettingValue(args);
                insertGlobalSetting(name, value, requestingUserId, false);
                break;
            }
    
            case Settings.CALL_METHOD_PUT_SECURE: {
                String value = getSettingValue(args);
                insertSecureSetting(name, value, requestingUserId, false);
                break;
            }
    
            ......
        }
    
        return null;
    }

    首先调用getSettingValue(args)获取对应的设置项,接着insertGlobalSetting()方法:

    private boolean insertGlobalSetting(String name, String value, int requestingUserId,
            boolean forceNotify) {
        return mutateGlobalSetting(name, value, requestingUserId, MUTATION_OPERATION_INSERT,
                forceNotify);
    }

    直接调用了mutateGlobalSetting()方法:

    private boolean mutateGlobalSetting(String name, String value, int requestingUserId,
            int operation, boolean forceNotify) {
        // Make sure the caller can change the settings - treated as secure.
        enforceWritePermission(Manifest.permission.WRITE_SECURE_SETTINGS);
    
        // If this is a setting that is currently restricted for this user, do not allow
        // unrestricting changes.
        if (isGlobalOrSecureSettingRestrictedForUser(name, callingUserId, value)) {
            return false;
        }
    
        // Perform the mutation.
        synchronized (mLock) {
            switch (operation) {
                case MUTATION_OPERATION_INSERT: {
                    return mSettingsRegistry
                            .insertSettingLocked(SETTINGS_TYPE_GLOBAL, UserHandle.USER_SYSTEM,
                                    name, value, getCallingPackage(), forceNotify);
                }
    
                ......
            }
        }
        return false;
    }

    首先进行权限检查,然后调用mSettingsRegistry.insertSettingLocked()方法:

    public boolean insertSettingLocked(int type, int userId, String name, String value,
            String packageName, boolean forceNotify) {
        final int key = makeKey(type, userId);
    
        SettingsState settingsState = peekSettingsStateLocked(key);
        final boolean success = settingsState.insertSettingLocked(name, value, packageName);
    
        if (forceNotify || success) {
            notifyForSettingsChange(key, name);
        }
        return success;
    }

    新增数据保存到数据库

    以新增一个"intercept_back"来获取back键是否被禁用,数据类型是 System integer类型。

    定义变量:

    frameworks/base/core/java/android/provider/Settings.java

    public static final class System extends NameValueTable {
    ..
    ...
    		/**
    		*Enable / disable back key interface
    		*0 = enable
    		*1 = disable
    		*@hide
    		*/
    		public static final String INTERCEPT_BACK = "intercept_back";
            public static final String[] SETTINGS_TO_BACKUP = {
                STAY_ON_WHILE_PLUGGED_IN,   // moved to global
                WIFI_USE_STATIC_IP,
                WIFI_STATIC_IP,
                WIFI_STATIC_GATEWAY,
                WIFI_STATIC_NETMASK,
                ......
                SHOW_BATTERY_PERCENT,
    			INTERCEPT_BACK  //add by wangjin in 2019/01/09
            };
    
    }

    注意:

    在这一步一定要加/** @hide */(一定是/** */格式),不然编译会报以下错误:

    Checking API: checkpublicapi-current
    out/target/common/obj/PACKAGING/public_api.txt:20: error 5: Added public field android.Manifest.permission.BACKUP
    out/target/common/obj/PACKAGING/public_api.txt:82: error 5: Added public field android.Manifest.permission.INVOKE_CARRIER_SETUP
    out/target/common/obj/PACKAGING/public_api.txt:106: error 5: Added public field android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE
    out/target/common/obj/PACKAGING/public_api.txt:116: error 5: Added public field android.Manifest.permission.RECEIVE_EMERGENCY_BROADCAST
     
    ******************************
    You have tried to change the API from what has been previously approved.
     
    To make these errors go away, you have two choices:
       1) You can add "@hide" javadoc comments to the methods, etc. listed in the
          errors above.
     
       2) You can update current.txt by executing the following command:
             make update-api
     
          To submit the revised current.txt to the main Android repository,
          you will need approval.

    设置默认值

    vendor/mediatek/proprietary/packages/apps/SettingsProvider/res/values/defaults.xml  (mtk)

    <integer name="intercept_back">0</integer>

    加载该值

    private void loadSystemSettings(SQLiteDatabase db) {
       loadIntegerSetting(stmt,Settings.System.INTERCEPT_BACK, R.integer.intercept_back);
    }

     

    展开全文
  • android10.0(Q) Settings 添加设置项——动态方式

    千次阅读 多人点赞 2020-06-08 17:24:15
    为什么要这样做? 上一篇通过静态方式添加配置项,应用场景太局限。 所以继续研究加载原理,终于发现了动态加载的...vendor\mediatek\proprietary\packages\apps\MtkSettings\src\com\android\settings\dashboard\Dashbo

    为什么要这样做?

    上一篇通过静态方式添加配置项,应用场景太局限。

    所以继续研究加载原理,终于发现了动态加载的奥秘。

    效果图

    tsKNlj.gif trfk1U.png

    文件清单

    frameworks\base\packages\SettingsLib\Tile\src\com\android\settingslib\drawer\TileUtils.java
    vendor\mediatek\proprietary\packages\apps\MtkSettings\src\com\android\settings\dashboard\DashboardFragment.java
    
    

    实现过程

    去除 TileUtils 中是否系统App判断逻辑,注释 getTilesForAction() 中 resolved.system 判断

    frameworks\base\packages\SettingsLib\Tile\src\com\android\settingslib\drawer\TileUtils.java

    static void getTilesForAction(Context context,
                UserHandle user, String action, Map<Pair<String, String>, Tile> addedCache,
                String defaultCategory, List<Tile> outTiles, boolean requireSettings) {
            final Intent intent = new Intent(action);
            if (requireSettings) {
                intent.setPackage(SETTING_PKG);
            }
            final PackageManager pm = context.getPackageManager();
            List<ResolveInfo> results = pm.queryIntentActivitiesAsUser(intent,
                    PackageManager.GET_META_DATA, user.getIdentifier());
            for (ResolveInfo resolved : results) {
                if (!resolved.system) {
                    // Do not allow any app to add to settings, only system ones.
                    Log.w(LOG_TAG, "not allow  app " + resolved.activityInfo.name);
    				//cczheng annotaion for 3rd app can add setting preference
                    //continue;
                }
                Log.w(LOG_TAG, "resolved.targetUserId="+resolved.targetUserId);
                ActivityInfo activityInfo = resolved.activityInfo;
                Bundle metaData = activityInfo.metaData;
                String categoryKey = defaultCategory;
    
    

    系统修改就已经搞定了,我去,这也太简单了吧,不是,你骗我的吧。大兄弟真的搞定了,只要注释 continue; 就行了

    接下来就可以为所欲为的添加配置项了。客户只需要在自己 app 的 AndroidManifest.xml 中配置属性给要跳转的Activity即可

    <activity android:name=".activity.SettingPreferenceActivity">
    			 <intent-filter >
                    <action android:name="com.android.settings.action.EXTRA_SETTINGS" />
                </intent-filter>
                <meta-data
                    android:name="com.android.settings.category"
                    android:value="com.android.settings.category.ia.homepage" />
                <meta-data
                    android:name="com.android.settings.order"
                    android:value="-150" />
                <meta-data
                    android:name="com.android.settings.icon"
                    android:resource="@mipmap/ic_icon" />
                <meta-data
                    android:name="com.android.settings.summary"
                    android:resource="@string/title_activity_settings" />
            </activity>
    

    解释下各个属性意义

    com.android.settings.action.EXTRA_SETTINGS 设置遍历所有应用解析标记

    com.android.settings.category.ia.homepage 在设置主界面显示

    com.android.settings.order 设置主界面排序,网络和互联网默认-120,只要大于即可排到第一

    com.android.settings.icon 显示图标

    com.android.settings.summary 显示子标题文字

    遇到的问题解决

    当动态添加设置项对应app卸载后,再次进入设置页面,会看到如下bug

    trfMh6.png

    问题日志如下,app卸载后由于设置应用没有重新初始化,缓存了刚刚的状态,加载设置项对应icon找不到资源,就出现上述bug,

    当你把设置强行停止再进入发现bug消失了,但总不能要求客户也这么操作吧。

    2020-06-05 16:39:56.964 6059-6059/com.android.settings D/AdaptiveHomepageIcon: Setting background color -15043608
    2020-06-05 16:39:56.965 6059-6059/com.android.settings I/TopLevelSettings: key dashboard_tile_pref_com.cczheng.androiddemo.activity.SettingPreferenceActivity
    2020-06-05 16:39:56.965 6059-6059/com.android.settings D/TopLevelSettings: tile null
    2020-06-05 16:39:56.966 6059-6059/com.android.settings D/Tile: Can't find package, probably uninstalled.
    2020-06-05 16:39:56.966 6059-6059/com.android.settings W/ziparchive: Unable to open '/data/app/com.cczheng.androiddemo-tcKDlXiPvEgQLoVFL4Pd3g==/base.apk': No such file or directory
    2020-06-05 16:39:56.967 6059-6059/com.android.settings E/ndroid.setting: Failed to open APK '/data/app/com.cczheng.androiddemo-tcKDlXiPvEgQLoVFL4Pd3g==/base.apk' I/O error
    2020-06-05 16:39:56.967 6059-6059/com.android.settings E/ResourcesManager: failed to add asset path /data/app/com.cczheng.androiddemo-tcKDlXiPvEgQLoVFL4Pd3g==/base.apk
    2020-06-05 16:39:56.967 6059-6059/com.android.settings W/PackageManager: Failure retrieving resources for com.cczheng.androiddemo
    2020-06-05 16:39:56.968 6059-6059/com.android.settings D/Tile: Can't find package, probably uninstalled.
    2020-06-05 16:39:56.970 6059-6059/com.android.settings D/Tile: Couldn't find info
        android.content.pm.PackageManager$NameNotFoundException: com.cczheng.androiddemo
            at android.app.ApplicationPackageManager.getApplicationInfoAsUser(ApplicationPackageManager.java:414)
            at android.app.ApplicationPackageManager.getApplicationInfo(ApplicationPackageManager.java:395)
            at android.app.ApplicationPackageManager.getResourcesForApplication(ApplicationPackageManager.java:1545)
            at com.android.settingslib.drawer.Tile.getSummary(Tile.java:220)
            at com.android.settings.dashboard.DashboardFeatureProviderImpl.bindSummary(DashboardFeatureProviderImpl.java:172)
            at com.android.settings.dashboard.DashboardFeatureProviderImpl.bindPreferenceToTile(DashboardFeatureProviderImpl.java:117)
            at com.android.settings.dashboard.DashboardFragment.refreshDashboardTiles(DashboardFragment.java:511)
            at com.android.settings.dashboard.DashboardFragment.refreshAllPreferences(DashboardFragment.java:394)
            at com.android.settings.dashboard.DashboardFragment.onCreatePreferences(DashboardFragment.java:170)
            at androidx.preference.PreferenceFragmentCompat.onCreate(PreferenceFragmentCompat.java:160)
            at com.android.settingslib.core.lifecycle.ObservablePreferenceFragment.onCreate(ObservablePreferenceFragment.java:61)
    

    解决办法

    依旧是通过检测APP是否已经卸载来决定是否加载对应配置项,依旧是在 DashboardFragment 中

    vendor\mediatek\proprietary\packages\apps\MtkSettings\src\com\android\settings\dashboard\DashboardFragment.java

    void refreshDashboardTiles(final String TAG) {
            ......
            // Install dashboard tiles.
            final boolean forceRoundedIcons = shouldForceRoundedIcon();
            for (Tile tile : tiles) {
                final String key = mDashboardFeatureProvider.getDashboardKeyForTile(tile);
                if (TextUtils.isEmpty(key)) {
                    Log.d(TAG, "tile does not contain a key, skipping " + tile);
                    continue;
                }
    
                Log.i(TAG, "key " +  key);
                Log.d(TAG, "tile " +  tile.getKey(getContext()));
    			//cczheng add for fix app uninstall show bug
                if (!checkTilePackage(tile.getPackageName())) {
                    Log.d(TAG, "Can't find package, probably uninstalled don't load");
                    continue;
                }//E check  Can't find package, probably uninstalled.
               
                if (!displayTile(tile)) {
                    continue;
                }
    
    			......
    }
    
     private boolean checkTilePackage(String packageName){
          try { 
               android.content.pm.PackageManager pm =  getContext().getPackageManager();
               pm.getApplicationInfo(packageName, android.content.pm.PackageManager.GET_UNINSTALLED_PACKAGES);
               android.util.Log.e("DashboardAdapter", packageName + " app exists show voip dashboard");
               return true; 
          }catch (Exception e){ 
              android.util.Log.e("DashboardAdapter", packageName + " app don't exists"); 
              return false; 
          } 
      }
    

    好了,至此需求已经搞定了。如果你想知道为啥这样改,请继续往下看。

    原理分析

    从启动开始说起

    进入setting的AndroidManifest.xml里看一看,找启动Activity

     <!-- Alias for launcher activity only, as this belongs to each profile. -->
            <activity-alias android:name="Settings"
                    android:label="@string/settings_label_launcher"
                    android:launchMode="singleTask"
                    android:targetActivity=".homepage.SettingsHomepageActivity">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
                    <category android:name="android.intent.category.DEFAULT" />
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
                <meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts"/>
            </activity-alias>
    

    发现启动Activity是Settings,但是前面的标签是activity-alias,所以这是另一个Activity的别名,然后它真实的启动Activity应该是targetActivity所标注的SettingsHomepageActivity。

    走进SettingsHomepageActivity.java

    vendor\mediatek\proprietary\packages\apps\MtkSettings\src\com\android\settings\homepage\SettingsHomepageActivity.java

    public class SettingsHomepageActivity extends FragmentActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
    
            setContentView(R.layout.settings_homepage_container);
            final View root = findViewById(R.id.settings_homepage_container);
            root.setSystemUiVisibility(
                    View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
    
            
            setHomepageContainerPaddingTop();
    
            final Toolbar toolbar = findViewById(R.id.search_action_bar);
            FeatureFactory.getFactory(this).getSearchFeatureProvider()
                    .initSearchToolbar(this /* activity */, toolbar, SettingsEnums.SETTINGS_HOMEPAGE);
    
            final ImageView avatarView = findViewById(R.id.account_avatar);
            final AvatarViewMixin avatarViewMixin = new AvatarViewMixin(this, avatarView);
            getLifecycle().addObserver(avatarViewMixin);
    
            if (!getSystemService(ActivityManager.class).isLowRamDevice()) {
                // Only allow contextual feature on high ram devices.
                showFragment(new ContextualCardsFragment(), R.id.contextual_cards_content);
            }
            showFragment(new TopLevelSettings(), R.id.main_content);
            ((FrameLayout) findViewById(R.id.main_content))
                    .getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING);
        }
    
        private void showFragment(Fragment fragment, int id) {
            final FragmentManager fragmentManager = getSupportFragmentManager();
            final FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
            final Fragment showFragment = fragmentManager.findFragmentById(id);
    
            if (showFragment == null) {
                fragmentTransaction.add(id, fragment);
            } else {
                fragmentTransaction.show(showFragment);
            }
            fragmentTransaction.commit();
        }
    
        @VisibleForTesting
        void setHomepageContainerPaddingTop() {
            final View view = this.findViewById(R.id.homepage_container);
    
            final int searchBarHeight = getResources().getDimensionPixelSize(R.dimen.search_bar_height);
            final int searchBarMargin = getResources().getDimensionPixelSize(R.dimen.search_bar_margin);
    
            // The top padding is the height of action bar(48dp) + top/bottom margins(16dp)
            final int paddingTop = searchBarHeight + searchBarMargin * 2;
            view.setPadding(0 /* left */, paddingTop, 0 /* right */, 0 /* bottom */);
        }
    }
    
    

    代码不多,布局文件对应 settings_homepage_container.xml, 布局加载完成后增加顶部padding为了给SearchActionBar预留空间,

    如果不需要SeacherActionBar直接将这部分代码注释即可。接下来看到新创建 TopLevelSettings 填充 main_content,主角登场啦。

    TopLevelSettings 就是我们看到的Settings主界面。

    进入TopLevelSettings

    vendor\mediatek\proprietary\packages\apps\MtkSettings\src\com\android\settings\homepage\TopLevelSettings.java

    public class TopLevelSettings extends DashboardFragment implements
            PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
    
        private static final String TAG = "TopLevelSettings";
    
        public TopLevelSettings() {
            final Bundle args = new Bundle();
            // Disable the search icon because this page uses a full search view in actionbar.
            args.putBoolean(NEED_SEARCH_ICON_IN_ACTION_BAR, false);
            setArguments(args);
        }
    
        @Override
        protected int getPreferenceScreenResId() {
            return R.xml.top_level_settings;
        }
    
    

    top_level_settings.xml

    vendor\mediatek\proprietary\packages\apps\MtkSettings\res\xml\top_level_settings.xml

    <PreferenceScreen
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:settings="http://schemas.android.com/apk/res-auto"
        android:key="top_level_settings">
    
        <Preference
            android:key="top_level_network"
            android:title="@string/network_dashboard_title"
            android:summary="@string/summary_placeholder"
            android:icon="@drawable/ic_homepage_network"
            android:order="-120"
            android:fragment="com.android.settings.network.NetworkDashboardFragment"
            settings:controller="com.android.settings.network.TopLevelNetworkEntryPreferenceController"/>
    
        <Preference
            android:key="top_level_connected_devices"
            android:title="@string/connected_devices_dashboard_title"
            android:summary="@string/summary_placeholder"
            android:icon="@drawable/ic_homepage_connected_device"
            android:order="-110"
            android:fragment="com.android.settings.connecteddevice.ConnectedDeviceDashboardFragment"
            settings:controller="com.android.settings.connecteddevice.TopLevelConnectedDevicesPreferenceController"/>
    
        <Preference
            android:key="top_level_apps_and_notifs"
            android:title="@string/app_and_notification_dashboard_title"
            android:summary="@string/app_and_notification_dashboard_summary"
            android:icon="@drawable/ic_homepage_apps"
            android:order="-100"
            android:fragment="com.android.settings.applications.AppAndNotificationDashboardFragment"/>
    
    

    可以看到主界面对应布局 top_level_settings.xml中都是一个个Preference,也就对应了主页面每一个条目,可以看到

    xml 中 Preference数目和主界面显示数目是不对等了,为啥呢?因为存在动态添加的,查阅 TopLevelSettings 代码发现没啥

    特殊而且代码量很少,看到 TopLevelSettings 继承 DashboardFragment,点进去看看

    DashboardFragment.java

    vendor\mediatek\proprietary\packages\apps\MtkSettings\src\com\android\settings\dashboard\DashboardFragment.java

    @Override
        public void onAttach(Context context) {
            super.onAttach(context);
            mSuppressInjectedTileKeys = Arrays.asList(context.getResources().getStringArray(
                    R.array.config_suppress_injected_tile_keys));
            mDashboardFeatureProvider = FeatureFactory.getFactory(context).
                    getDashboardFeatureProvider(context);
            final List<AbstractPreferenceController> controllers = new ArrayList<>();
            // Load preference controllers from code
            final List<AbstractPreferenceController> controllersFromCode =
                    createPreferenceControllers(context);
            // Load preference controllers from xml definition
            final List<BasePreferenceController> controllersFromXml = PreferenceControllerListHelper
                    .getPreferenceControllersFromXml(context, getPreferenceScreenResId());
            // Filter xml-based controllers in case a similar controller is created from code already.
            final List<BasePreferenceController> uniqueControllerFromXml =
                    PreferenceControllerListHelper.filterControllers(
                            controllersFromXml, controllersFromCode);
    
            // Add unique controllers to list.
            if (controllersFromCode != null) {
                controllers.addAll(controllersFromCode);
            }
            controllers.addAll(uniqueControllerFromXml);
    
          
        }
    
    

    注释已经写得很清楚了,分别从java代码和xml中加载PreferenceController,然后过滤去重最终加载页面显示。

    java代码加载

    createPreferenceControllers() return null,而且子类TopLevelSettings并未覆盖实现,所以 controllersFromCode 为 null

    xml加载

    getPreferenceControllersFromXml(context, getPreferenceScreenResId()), getPreferenceScreenResId对应刚刚的 top_level_settings

    具体的遍历解析xml文件代码就不看了,可以自行跟进去查看

    controllers 集合获取完成,那么这个 Controller 究竟有什么用呢?

    看Settings中的Preference你会发信几乎每个都对应一个 settings:controller 属性,xml中若没有那么也会在java代码中对应增加

    Controller 可以用来处理 Preference的显示和点击。扯远了回到主题,继续寻找和动态增加相关线索

     @Override
        public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
            refreshAllPreferences(getLogTag());
        }
    
    private void refreshAllPreferences(final String TAG) {
            final PreferenceScreen screen = getPreferenceScreen();
            // First remove old preferences.
            if (screen != null) {
                // Intentionally do not cache PreferenceScreen because it will be recreated later.
                screen.removeAll();
            }
    
            // Add resource based tiles.
            displayResourceTiles();
    
            refreshDashboardTiles(TAG);
    
            final Activity activity = getActivity();
            if (activity != null) {
                Log.d(TAG, "All preferences added, reporting fully drawn");
                activity.reportFullyDrawn();
            }
    
            updatePreferenceVisibility(mPreferenceControllers);
    }
    
    

    嗯,这下有点意思了,refreshAllPreferences() 一上来移除所有的Preference,通过 displayResourceTiles()

    加载指定xml中的所有Preference

    private void displayResourceTiles() {
            final int resId = getPreferenceScreenResId();
            if (resId <= 0) {
                return;
            }
            addPreferencesFromResource(resId);
            final PreferenceScreen screen = getPreferenceScreen();
            screen.setOnExpandButtonClickListener(this);
            mPreferenceControllers.values().stream().flatMap(Collection::stream).forEach(
                    controller -> controller.displayPreference(screen));
        }
    

    好像也不是我们要找的,再往下看 refreshDashboardTiles(TAG);

    void refreshDashboardTiles(final String TAG) {
            final PreferenceScreen screen = getPreferenceScreen();
    
            final DashboardCategory category =
                    mDashboardFeatureProvider.getTilesForCategory(getCategoryKey());
            Log.e(TAG, "refreshDashboardTiles key="+ getCategoryKey());
            if (category == null) {
                Log.d(TAG, "NO dashboard tiles for " + TAG);
                return;
            }
            final List<Tile> tiles = category.getTiles();
            if (tiles == null) {
                Log.d(TAG, "tile list is empty, skipping category " + category.key);
                return;
            }
             Log.e(TAG, "tile list size="+tiles.size());
            // Create a list to track which tiles are to be removed.
            final List<String> remove = new ArrayList<>(mDashboardTilePrefKeys);
    
            // There are dashboard tiles, so we need to install SummaryLoader.
            if (mSummaryLoader != null) {
                mSummaryLoader.release();
            }
            final Context context = getContext();
            mSummaryLoader = new SummaryLoader(getActivity(), getCategoryKey());
            mSummaryLoader.setSummaryConsumer(this);
            // Install dashboard tiles.
            final boolean forceRoundedIcons = shouldForceRoundedIcon();
            for (Tile tile : tiles) {
                final String key = mDashboardFeatureProvider.getDashboardKeyForTile(tile);
                if (TextUtils.isEmpty(key)) {
                    Log.d(TAG, "tile does not contain a key, skipping " + tile);
                    continue;
                }
    
                Log.i(TAG, "key " +  key);
                Log.d(TAG, "tile " +  tile.getKey(getContext()));
               
                if (!displayTile(tile)) {
                    continue;
                }
                if (mDashboardTilePrefKeys.contains(key)) {
                    // Have the key already, will rebind.
                    final Preference preference = screen.findPreference(key);
                    mDashboardFeatureProvider.bindPreferenceToTile(getActivity(), forceRoundedIcons,
                            getMetricsCategory(), preference, tile, key,
                            mPlaceholderPreferenceController.getOrder());
                } else {
                    // Don't have this key, add it.
                    final Preference pref = new Preference(getPrefContext());
                    mDashboardFeatureProvider.bindPreferenceToTile(getActivity(), forceRoundedIcons,
                            getMetricsCategory(), pref, tile, key,
                            mPlaceholderPreferenceController.getOrder());
                    screen.addPreference(pref);
                    mDashboardTilePrefKeys.add(key);
                }
                remove.remove(key);
            }
            // Finally remove tiles that are gone.
            for (String key : remove) {
                 Log.d(TAG, "remove tiles that are gone " +  key);
                mDashboardTilePrefKeys.remove(key);
                final Preference preference = screen.findPreference(key);
                if (preference != null) {
                    screen.removePreference(preference);
                }
            }
            mSummaryLoader.setListening(true);
        }
    
    

    哈哈哈,终于找到奥秘所在了,因为这个方法中有调用 addPreference(),页面中要想增加Preference条目必须调用此方法,

    接下来逐行来看这个方法都干什么了?

    final DashboardCategory category = mDashboardFeatureProvider.getTilesForCategory(getCategoryKey());

    来看传递参数 getCategoryKey()

    public String getCategoryKey() {
            return DashboardFragmentRegistry.PARENT_TO_CATEGORY_KEY_MAP.get(getClass().getName());
    }
    

    getClass().getName() 获取当前调用类名,我们从 TopLevelSettings 中进来的,那自然是它

    DashboardFragmentRegistry.PARENT_TO_CATEGORY_KEY_MAP 是静态MAP集合,看下初始赋值

    vendor\mediatek\proprietary\packages\apps\MtkSettings\src\com\android\settings\dashboard\DashboardFragmentRegistry.java

    
    public static final Map<String, String> PARENT_TO_CATEGORY_KEY_MAP;
    
    static {
            PARENT_TO_CATEGORY_KEY_MAP = new ArrayMap<>();
            PARENT_TO_CATEGORY_KEY_MAP.put(TopLevelSettings.class.getName(),
                    CategoryKey.CATEGORY_HOMEPAGE);
            PARENT_TO_CATEGORY_KEY_MAP.put(
                    NetworkDashboardFragment.class.getName(), CategoryKey.CATEGORY_NETWORK);
            PARENT_TO_CATEGORY_KEY_MAP.put(ConnectedDeviceDashboardFragment.class.getName(),
                    CategoryKey.CATEGORY_CONNECT);
            PARENT_TO_CATEGORY_KEY_MAP.put(AdvancedConnectedDeviceDashboardFragment.class.getName(),
                    CategoryKey.CATEGORY_DEVICE);
    
    		...
    
    

    看到 TopLevelSettings 对应 String 为 CategoryKey.CATEGORY_HOMEPAGE,也就是 getCategoryKey() 返回值

    com.android.settings.category.ia.homepage

    frameworks\base\packages\SettingsLib\src\com\android\settingslib\drawer\CategoryKey.java

    public final class CategoryKey {
    
        // Activities in this category shows up in Settings homepage.
        public static final String CATEGORY_HOMEPAGE = "com.android.settings.category.ia.homepage";
    
        // Top level category.
        public static final String CATEGORY_NETWORK = "com.android.settings.category.ia.wireless";
        
    

    进入 getTilesForCategory() 中获取 DashboardCategory

    vendor\mediatek\proprietary\packages\apps\MtkSettings\src\com\android\settings\dashboard\CategoryManager.java

    public synchronized DashboardCategory getTilesByCategory(Context context, String categoryKey) {
            tryInitCategories(context);
    
            return mCategoryByKeyMap.get(categoryKey);
        }
    

    可以看到从 mCategoryByKeyMap 中获取 key为com.android.settings.category.ia.homepage 对应 DashboardCategory

    mCategoryByKeyMap 赋值在 tryInitCategories() 中

    private synchronized void tryInitCategories(Context context, boolean forceClearCache) {
            if (mCategories == null) {
                if (forceClearCache) {
                    mTileByComponentCache.clear();
                }
                mCategoryByKeyMap.clear();
                mCategories = TileUtils.getCategories(context, mTileByComponentCache);
                for (DashboardCategory category : mCategories) {
                     android.util.Log.i("settingslib", "category.key="+category.key);
                    mCategoryByKeyMap.put(category.key, category);
                }
                backwardCompatCleanupForCategory(mTileByComponentCache, mCategoryByKeyMap);
                sortCategories(context, mCategoryByKeyMap);
                filterDuplicateTiles(mCategoryByKeyMap);
            }
        }
    

    获取 category 集合,编译集合依次往map中添加,继续跟 TileUtils.getCategories()

    frameworks\base\packages\SettingsLib\Tile\src\com\android\settingslib\drawer\TileUtils.java

        /**
         * Build a list of DashboardCategory.
         */
        public static List<DashboardCategory> getCategories(Context context,
                Map<Pair<String, String>, Tile> cache) {
            final long startTime = System.currentTimeMillis();
            boolean setup = Global.getInt(context.getContentResolver(), Global.DEVICE_PROVISIONED, 0)
                    != 0;
            ArrayList<Tile> tiles = new ArrayList<>();
            UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
            for (UserHandle user : userManager.getUserProfiles()) {
                // TODO: Needs much optimization, too many PM queries going on here.
                loge("getIdentifier="+user.getIdentifier());
                loge("getCurrentUser="+ActivityManager.getCurrentUser());//
                if (user.getIdentifier() == ActivityManager.getCurrentUser()) {
                    // Only add Settings for this user.
                    getTilesForAction(context, user, SETTINGS_ACTION, cache, null, tiles, true);
                    getTilesForAction(context, user, OPERATOR_SETTINGS, cache,
                            OPERATOR_DEFAULT_CATEGORY, tiles, false);
                    getTilesForAction(context, user, MANUFACTURER_SETTINGS, cache,
                            MANUFACTURER_DEFAULT_CATEGORY, tiles, false);
                }
                if (setup) {
                    getTilesForAction(context, user, EXTRA_SETTINGS_ACTION, cache, null, tiles, false);
                    getTilesForAction(context, user, IA_SETTINGS_ACTION, cache, null, tiles, false);
                }
            }
    
            HashMap<String, DashboardCategory> categoryMap = new HashMap<>();
            for (Tile tile : tiles) {
                final String categoryKey = tile.getCategory();
                DashboardCategory category = categoryMap.get(categoryKey);
                if (category == null) {
                    category = new DashboardCategory(categoryKey);
    
                    if (category == null) {
                        Log.w(LOG_TAG, "Couldn't find category " + categoryKey);
                        continue;
                    }
                    categoryMap.put(categoryKey, category);
                }
                category.addTile(tile);
            }
            ArrayList<DashboardCategory> categories = new ArrayList<>(categoryMap.values());
            for (DashboardCategory category : categories) {
                category.sortTiles();
            }
    
            if (DEBUG_TIMING) {
                Log.d(LOG_TAG, "getCategories took "
                        + (System.currentTimeMillis() - startTime) + " ms");
            }
            return categories;
        }
    

    可以看到开始创建空集合 tiles,通过调用getTilesForAction() 进行赋值。赋值后遍历 tiles,获取

    tile 中 DashboardCategory,判断 categoryMap 中是否包含,不包含则往里添加。最终创建 ArrayList categories,

    并赋值 categoryMap.values(),进行排序后 return categories

    核心还是在 tiles 赋值,再来看 getTilesForAction()

    static void getTilesForAction(Context context,
                UserHandle user, String action, Map<Pair<String, String>, Tile> addedCache,
                String defaultCategory, List<Tile> outTiles, boolean requireSettings) {
            loge("action="+action);
            final Intent intent = new Intent(action);
            if (requireSettings) {
                intent.setPackage(SETTING_PKG);
            }
            final PackageManager pm = context.getPackageManager();
            List<ResolveInfo> results = pm.queryIntentActivitiesAsUser(intent,
                    PackageManager.GET_META_DATA, user.getIdentifier());
            for (ResolveInfo resolved : results) {
                if (!resolved.system) {
                    // Do not allow any app to add to settings, only system ones.
                    Log.w(LOG_TAG, "not allow  app " + resolved.activityInfo.name);
                    continue;
                }
                Log.w(LOG_TAG, "resolved.targetUserId="+resolved.targetUserId);
                ActivityInfo activityInfo = resolved.activityInfo;
                Bundle metaData = activityInfo.metaData;
                String categoryKey = defaultCategory;
    
                // Load category
                if ((metaData == null || !metaData.containsKey(EXTRA_CATEGORY_KEY))
                        && categoryKey == null) {
                    Log.w(LOG_TAG, "Found " + resolved.activityInfo.name + " for intent "
                            + intent + " missing metadata "
                            + (metaData == null ? "" : EXTRA_CATEGORY_KEY));
                    loge("Found " + resolved.activityInfo.name + " for intent "
                            + intent + " missing metadata "
                            + (metaData == null ? "" : EXTRA_CATEGORY_KEY));
                    continue;
                } else {
                    categoryKey = metaData.getString(EXTRA_CATEGORY_KEY);
                }
    
                Pair<String, String> key = new Pair<>(activityInfo.packageName, activityInfo.name);
                Tile tile = addedCache.get(key);
                if (tile == null) {
                    tile = new Tile(activityInfo, categoryKey);
                    addedCache.put(key, tile);
                } else {
                    tile.setMetaData(metaData);
                }
    
                if (!tile.userHandle.contains(user)) {
                    tile.userHandle.add(user);
                }
                if (!outTiles.contains(tile)) {
                    outTiles.add(tile);
                }
                loge("tile key="+tile.getPackageName());
            }
        }
    

    通过 PackageManager 查询系统中所有带指定 Action 的 Intent 对应信息 ResolveInfo 集合,然后遍历该集合

    获取符合条件应用信息包名、类名、icon等构造 tile,最终添加进 outTiles 中。

    可以看到循环一开始就有硬性判断,if (!resolved.system)

    // Do not allow any app to add to settings, only system ones.

    必须是系统应用才能向Setting主界面中添加配置项,这显然不是我们希望的,我们既然是开放给客户的,自然不需要这个判断

    注释 continue 即可。

    上面说到必须是指定action,才能被 PackageManager 搜索到,来看下都有哪些Action

    private static final String SETTINGS_ACTION = "com.android.settings.action.SETTINGS";
    private static final String OPERATOR_SETTINGS = "com.android.settings.OPERATOR_APPLICATION_SETTING";
    private static final String MANUFACTURER_SETTINGS = "com.android.settings.MANUFACTURER_APPLICATION_SETTING";
    public static final String EXTRA_SETTINGS_ACTION = "com.android.settings.action.EXTRA_SETTINGS";
    public static final String IA_SETTINGS_ACTION = "com.android.settings.action.IA_SETTINGS";
    
    private static final String EXTRA_CATEGORY_KEY = "com.android.settings.category";
    public static final String META_DATA_KEY_ORDER = "com.android.settings.order";
    
    getTilesForAction(context, user, SETTINGS_ACTION, cache, null, tiles, true);
    getTilesForAction(context, user, OPERATOR_SETTINGS, cache, OPERATOR_DEFAULT_CATEGORY, tiles, false);
    getTilesForAction(context, user, MANUFACTURER_SETTINGS, cache, MANUFACTURER_DEFAULT_CATEGORY, tiles, false);
    
    getTilesForAction(context, user, EXTRA_SETTINGS_ACTION, cache, null, tiles, false);
    getTilesForAction(context, user, IA_SETTINGS_ACTION, cache, null, tiles, false);
    

    可以看到我们上面指定的就是 com.android.settings.action.EXTRA_SETTINGS,google 的 GMSCore app 采用的是

    com.android.settings.action.IA_SETTINGS

    配置了指定Action后,还需要配置 meta-data 节点,别忘记了 Settings 中匹配 category 通过 key=com.android.settings.category.ia.homepage

    // Load category
    if ((metaData == null || !metaData.containsKey(EXTRA_CATEGORY_KEY))
            && categoryKey == null) {
        Log.w(LOG_TAG, "Found " + resolved.activityInfo.name + " for intent "
                + intent + " missing metadata "
                + (metaData == null ? "" : EXTRA_CATEGORY_KEY));
        loge("Found " + resolved.activityInfo.name + " for intent "
                + intent + " missing metadata "
                + (metaData == null ? "" : EXTRA_CATEGORY_KEY));
        continue;
    } else {
        categoryKey = metaData.getString(EXTRA_CATEGORY_KEY);
    }
    

    所以要增加 meta-data 才能显示在主页中

     <meta-data
        android:name="com.android.settings.category"
        android:value="com.android.settings.category.ia.homepage" />
    

    通过Tile构造函数发现还有其它可选 meta-data 配置,

    com.android.settings.order 对应Preference排序

    com.android.settings.icon 对应Preference图标

    com.android.settings.summary 对应Preference子标题

    所以最终xml中配置为

    <activity android:name=".activity.SettingPreferenceActivity">
    			 <intent-filter >
                    <action android:name="com.android.settings.action.EXTRA_SETTINGS" />
                </intent-filter>
                <meta-data
                    android:name="com.android.settings.category"
                    android:value="com.android.settings.category.ia.homepage" />
                <meta-data
                    android:name="com.android.settings.order"
                    android:value="-150" />
                <meta-data
                    android:name="com.android.settings.icon"
                    android:resource="@mipmap/ic_icon" />
                <meta-data
                    android:name="com.android.settings.summary"
                    android:resource="@string/title_activity_settings" />
            </activity>
    

    嗯,数据加载搞清了,现在我们回到 DashboardFragment 中的 refreshDashboardTiles()

    如果未遍历到key=com.android.settings.category.ia.homepage 对应 DashboardCategory 则直接 return,

    无需刷新,比如当进入二级页面时,key将不再是com.android.settings.category.ia.homepage,若 category 中

    tile集合为空也直接return,因为没有需要添加的条目。接下来就是遍历 tiles 通过 PreferenceScreen.addPreference

    添加自定义条目了。

    展开全文
  • Android原生Settings源代码及所需Jar包

    热门讨论 2014-09-12 12:24:57
    Android原生Settings源代码及所需Jar包下载,直接导入到Eclipse,然后根据我给的截图配置好jar包,就可以编译生成apk,方便大家学习
  • settings简单那用法

    热门讨论 2012-03-01 14:57:39
    设置 保存 settings C#,简单用法
  • Android 7.0 Settings分析

    千次阅读 2018-07-26 15:13:01
    最近参加了settings的开发,由于代码还比较新, Android7.0 对Settings进行了重构,相比5.0,6.0而言,7.0的Settings有很大的不同,所以将开过程中的一些点点滴滴记录下来.此篇文章主要给大家介绍Settings相关的基础...

    最近参加了settings的开发,由于代码还比较新, Android7.0 对Settings进行了重构,相比5.0,6.0而言,7.0的Settings有很大的不同,所以将开过程中的一些点点滴滴记录下来.此篇文章主要给大家介绍Settings相关的基础知识:代码路径,手机中相关数据库、文件的路径,等。

    代码相关的路径:

    android\frameworks\base\packages\SettingsLib                          7.0重构Settings后多出来的部分
    android\frameworks\base\packages\SettingsProvider settings     数据库相关
    android\packages\apps\Settings Settings相关
    android\frameworks\base\core\java\android\provider settings     数据库字段定义相关
    android\packages\providers\MediaProvider                                 铃声相关
    android\packages\screensavers                                                   屏保相关
    android\packages\inputmethods                                                   输入法相关

     

    源码编译后apk生成目录:

    android\out\target\product\****\system\priv-app\Settings\Settings.apk
    android\out\target\product\****\system\priv-app\MediaProvider\MediaProvider.apk
    android\out\target\product\****\system\priv-app\SettingsProvider\SettingsProvider.apk

     

    cmd下,adb shell
    system/priv-app/Settings/Settings.apk
    system/priv-app/MediaProvider/MediaProvider.apk
    system/priv-app/SettingsProvider/SettingsProvider.apk

     

    Settings数据库

    Android L (5.1)及以前版本Settings相关的数据库在
    /data/data/com.android.providers.settings/databases/settings.db里,里面有三个表global, system, secure.
    查看数据库方式:
    (1)cmd下(不确认是否需配置sqlite3环境变量,如果不能使用sqlite3命令,请百度)
    adb shell sqlite3 data/data/com.android.providers.settings/databases/settings.db “select * from system”
    (2)或者
    adb pull data/data/com.android.providers.settings/databases/settings.db D:\xx 到本地,然后用SQLite Expert Professional 软件打开即可

    Android M (6.0)之后的谷歌对SettingsProvider进行了重构,所以Settings数据库的路径有了变化
    在手机目录/data/system/users/0路径下,以xml的形式存储数据,有settings_global.xml, settings_system.xml, settings_secure.xml
    其中不同用户的数据库放不同的用户路径下,如果没有创建新用户,则在/data/system/users/0下。
    修改数据库中的字段使用命令
    adb shell settings get global captive_portal_server
    adb shell settings put global captive_portal_server 0

    注:这里虽然手机目录中数据库存储是以xml的形式,但是代码中操作时操作的仍然是数据库。系统会先生成Settings.db数据库,然后将其中的内容保存到xml文件中,最后删除Settings.db数据库(SettingsProvider的功能,后续会讲到)。
    这里为何先生成数据库后又删除,主要是为了兼容之前的版本。
    为什么以xml的形式保存字段:节省资源,简单,安全(xml保存的路径相对于之前的数据库路径比较安全,大概是考虑到多用户吧)

     

    目录结构

    SettingsLib和Settings结构目录分工比较明确,目录一般都可以见名知意,如果你了解的话,看英文目录名就可以知道代码中的大概功能了,希望大家也养成良好的习惯,目录功能分工明确。

    1、Settings目录结构

    android\packages\apps\Settings
    - src
    - - com.android.settings Settings应用整体所用的java文件
    - - com.android.settings.accessibility 辅助功能相关
    - - com.android.settings.accounts 账户相关
    - - com.android.settings.application 应用管理相关
    - - com.android.settings.backup 备份相关
    - - com.android.settings.bluetooth 蓝牙相关
    - - com.android.settings.dashboard Settings主界面相关
    - - com.android.settings.datausage 数据流量相关
    - - com.android.settings.deletionhelper
    - - com.android.settings.deviceinfo 关于手机相关
    - - com.android.settings.display 显示相关
    - - com.android.settings.drawable 菜单图片相关
    - - com.android.settings.fingerprint 指纹相关
    - - com.android.settings.fuelgauge
    - - com.android.settings.gestures 手势相关
    - - com.android.settings.inputmethod 输入法相关
    - - com.android.settings.localepicker 语言相关
    - - com.android.settings.location 位置相关
    - - com.android.settings.nfc nfc相关
    - - com.android.settings.notification 通知相关相关
    - - com.android.settings.overlay
    - - com.android.settings.password 密码相关
    - - com.android.settings.print 打印相关
    - - com.android.settings.qstile
    - - com.android.settings.search 快速搜索相关
    - - com.android.settings.sim SIM卡相关
    - - com.android.settings.support
    - - com.android.settings.tts tts播报相关
    - - com.android.settings.users 用户相关
    - - com.android.settings.utils 工具类
    - - com.android.settings.voice
    - - com.android.settings.vpn2
    - - com.android.settings.wfd
    - - com.android.settings.widget 小部件相关
    - - com.android.settings.wifi wifi相关
    - AndroidManifest.xml 清单文件

    2、SettingsLib目录结构

    android\frameworks\base\packages\SettingsLib\src\com\android\settingslib
    - src
    - - com.android.settingslib.accessibility 辅助功能相关
    - - com.android.settingslib.accounts 账户相关
    - - com.android.settingslib.animation 动画相关
    - - com.android.settingslib.application 应用管理相关
    - - com.android.settingslib.bluetooth 蓝牙相关
    - - com.android.settingslib.datetime 日期相关
    - - com.android.settingslib.deviceinfo 关于手机相关
    - - com.android.settingslib.display 显示相关
    - - com.android.settingslib.drawable 菜单图片相关
    - - com.android.settingslib.drawer 侧滑菜单相关
    - - com.android.settings.dream 休眠相关
    - - com.android.settings.graph
    - - com.android.settings.location 位置相关
    - - com.android.settings.net 网络相关
    - - com.android.settings.users 用户相关
    - - com.android.settings.widget 小部件相关
    - - com.android.settings.wifi wifi相关
    - AndroidManifest.xml 清单文件

     

    以下是FAQ,来自MTK:

    how to modify the maximum connections of hotspot from frameworks?
    [DESCRIPTION]
       In general,hotspot's maximam connection is limited by hardware,so if you want to modify the number,must to make sure the performance of hardware is enough;
    [SOLUTION]
    To change the number to 10 as an example:
    /packages/apps/Settings/res_ext/values/mtk_arrays.xml
    <string-array name="wifi_ap_max_connection_entries">
    73 <item>1 user</item>
    74 <item>2 users</item>
    75 <item>3 users</item>
    76 <item>4 users</item>
    77 <item>5 users</item>
    78 <item>6 users</item>
    79 <item>7 users</item>
    80 <item>8 users</item>
    //add to ten
    <item>9 users</item>
    <item>10 users</item>
    //add
    81 </string-array>
    so you can choose "10 users " when you setup hotspot;

     ============================================================

    如何预置一个WIFI热点
     在此文档中会描述下面几个问题: 如何预置一个WIFI热点 — WIFI Setting界面默认就加入一个热点。
    [SOLUTION]
    JB版本:
    请在alps/external/wpa_supplicant_8/mtk-wpa_supplicant.conf 这个文件中增加
    networ={} 即可。
    network需要字段的含义可以查看wpa_supplicant_8/wpa_supplicant.conf文件中的注释“
    network block fields:”后的说明即可。
    KK&L版本:
    请在/hardware/mediatek/wlan/config/mtk-wpa_supplicant-overlay.conf文件中增加networ={} 即可。
    M 版本路径
    /vendor/mediatek/proprietary/hardware/connectivity/wlan/config/mtk-wpa_supplicantoverlay.
    conf
    如下面的例子,预置一个OPEN 和WPA2-PSK的网络:
    ctrl_interface=/data/misc/wpa_supplicant
    update_config=1
    device_name=rk30sdk
    manufacturer=rockchip
    model_name=ONE TOUCH EVO8HD
    model_number=ONE TOUCH EVO8HD
    serial_number=0123456789
    device_type=10-0050F204-5
    config_methods=physical_display virtual_push_button keypad
    network={
         ssid="aaaa"
         scan_ssid=1
         key_mgmt=NONE
    }
    network={
         ssid="bbbbb"
         scan_ssid=1
         psk="12345678"
         key_mgmt=WPA-PSK
    }

    ====================================================================

    how to set max bandwidth in 2G and 5G
    [DESCRIPTION]
    customer want to limit the bandwidth in different band as below: Maximum bandwidth : 40MHz for 2.4G, 20MHz for 5G

    [SOLUTION]
        1. find out the flie(wlan_lib.c)
        2. find out the function (wlanInitFeatureOption)
        3. modify codes and set those as you needs:
    example:
        prWifiVar->ucSta2gBandwidth = (UINT_8) wlanCfgGetUint32(prAdapter, "Sta2gBw",
        MAX_BW_20MHZ);
        prWifiVar->ucSta5gBandwidth = (UINT_8) wlanCfgGetUint32(prAdapter, "Sta5gBw",
        MAX_BW_80MHZ);
        prWifiVar->ucAp2gBandwidth = (UINT_8) wlanCfgGetUint32(prAdapter, "Ap2gBw",
        MAX_BW_20MHZ);
        prWifiVar->ucAp5gBandwidth = (UINT_8) wlanCfgGetUint32(prAdapter, "Ap5gBw",
        MAX_BW_40MHZ);
        prWifiVar->ucP2p2gBandwidth = (UINT_8) wlanCfgGetUint32(prAdapter, "P2p2gBw",
        MAX_BW_20MHZ);
        prWifiVar->ucP2p5gBandwidth = (UINT_8) wlanCfgGetUint32(prAdapter, "P2p5gBw",
        MAX_BW_40MHZ);
        typedef enum _ENUM_MAX_BANDWIDTH_SETTING_T {
        MAX_BW_20MHZ = 0,
        MAX_BW_40MHZ,
        MAX_BW_80MHZ,
        MAX_BW_160MHZ,
        MAX_BW_80_80_MHZ
    }

    =========================================================

    MTK wifi Roaming conditions
    [Question]
    Customer want to know machanism of roam between same SSID and different SSID
    [Answer]
       if the RCPI of signal lower than -70dbm. wifi will choice the best one(scores higher and better) from Condidates.
    and thescores of AP is decided by elements as those(u2ScoreBandwidth + u2ScoreChnlInfo + u2ScoreDeauth + u2ScoreProbeRsp+u2ScoreScanMiss + u2ScoreSnrRssi + u2ScoreStaCnt + u2ScoreSTBC + u2ScoreBand + u2BlackListScore) and the additional sroces will
    be added by 5G band or Current connected AP.

    ============================================================

    每次连接上网络会提示“已连接无法访问互联网”几秒钟后消失
    [DESCRIPTION]
       每次连接上网络会提示“已连接无法访问互联网”几秒钟后消失
       
    [SOLUTION]
       M版本开始连接WIFI AP,提示“已连接,没有网络”,约过5s左右切成“已连接”
      在L版時多了一個NetworkMonitor的module,負責當Wi-Fi連上後檢查該AP是否有captive portal能力,
      - 如果有, 提醒user在browser輸入username/password (如同mtkguest)
      - 如果沒有, 又區別分兩種case, 該AP有Internet or 該AP沒有Internet(意即validated = true/false)
     接著M版在networkCapability多了NET_CAPABILITY_VALIDATED的欄位將L版的validated結果帶入capability, 讓別的module更容易知道此Network是否有支援Internet
    如同L版本流程
    Wifi連上 WifiNetworkAgent的NetworkMonitor嘗試連connectivitycheck.gstatic.com如果有得到200, 代表有Internet能力
    ConnectivityService WifiNetworkAgent.networkCapability添加NET_CAPABILITY_VALIDATED的欄位如果沒得到response, 或者response延遲(大陸地區常遇到),延後networkCapability添加NET_CAPABILITY_VALIDATED的欄位

    ===============================================================================

    为什么add network 时点击security不显示密码输入框
    [DESCRIPTION]
        how to reproduce the problem?
        wifisettings -->add network--->choose different security ,but the password did not show;
    [SOLUTION]
        change code as follow:
        /packages/apps/Settings/src/com/android/settings/wifi/WifiConfigController.java
        public void onItemSelected(AdapterView<?> parent, View view, int position,long id)
       if (parent == mSecuritySpinner)
         change to
        if (parent == mSecuritySpinner || parent.getId() == R.id.wpa_security) reason: the cause of this problemis the id of "mSecuritySpinner" will be set to
        "R.id.wpa_security" when the software did not support WAPI ;
        int viewId = R.id.security;
        if (FeatureOption.MTK_WAPI_SUPPORT) {
            String type = SystemProperties.get(WLAN_PROP_KEY, DEFAULT_WLAN_PROP);
        if (type.equals(WIFI_WAPI)) {
        if (AccessPointExt.isWFATestSupported()) {
            viewId = R.id.security_wfa; // WIFI + WAPI, support // separate WPA2 PSK  security
        } else {
            viewId = R.id.security; // WIFI + WAPI
        }
        } else if (type.equals(WIFI)) {
            if (AccessPointExt.isWFATestSupported()) {
            viewId = R.id.wpa_security_wfa; // WIFI only, support
            // separate WPA2 PSK   // security
        } else {
            viewId = R.id.wpa_security; // WIFI only
        }
        } else if (type.equals(WAPI)) {
            viewId = R.id.wapi_security; // WAPI only
        }
        } else {
        if (AccessPointExt.isWFATestSupported()) {
            viewId = R.id.wpa_security_wfa; // WIFI only, support
            // separate WPA and WPA2 PSK // security
        } else {
            viewId = R.id.wpa_security; // WIFI only
        }
        }
        switchWlanSecuritySpinner((Spinner) mView.findViewById(viewId));

    =-==============================================================================

    应用如何定制ActionBar的背景颜色?
    [DESCRIPTION]
        应用如何定制ActionBar的背景颜色?
    [SOLUTION]
        ActionBar 默认使用的背景颜色是取自Style Widget.AppCompat.ActionBar,如果需要定制自己的应用的ActionBar的背景颜色,可以自定义一个Style文件,并将Widget.AppCompat.ActionBar设置
    为Parent, 然后再设定item background 为需要的值即可。参考例子如下:
    <?xml version="1.0" encoding="utf-8"?>
        <resources>
        <!-- the theme applied to the application or activity -->
        <style name="CustomActionBarTheme" parent="@style/Theme.AppCompat.Light">
            <item name="android:actionBarStyle">@style/MyActionBar</item>
            <item name="android:actionBarTabTextStyle">@style/TabTextStyle</item>
            <item name="android:actionMenuTextColor">@color/actionbar_text</item>
            <!-- Support library compatibility -->
            <item name="actionBarStyle">@style/MyActionBar</item>
            <item name="actionBarTabTextStyle">@style/TabTextStyle</item>
            <item name="actionMenuTextColor">@color/actionbar_text</item>
        </style>
        
        <!-- general styles for the action bar -->
        <style name="MyActionBar" parent="@style/Widget.AppCompat.ActionBar">
            <item name="android:titleTextStyle">@style/TitleTextStyle</item>
            <item name="android:background">@drawable/actionbar_background</item>
            <item name="android:backgroundStacked">@drawable/actionbar_background</item>
            <item name="android:backgroundSplit">@drawable/actionbar_background</item>
            <!-- Support library compatibility -->
            <item name="titleTextStyle">@style/TitleTextStyle</item>
            <item name="background">@drawable/actionbar_background</item>
            <item name="backgroundStacked">@drawable/actionbar_background</item>
            <item name="backgroundSplit">@drawable/actionbar_background</item>
        </style>
        <!-- action bar title text -->
        <style name="TitleTextStyle" parent="@style/TextAppearance.AppCompat.Widget.ActionBar.Title">
            <item name="android:textColor">@color/actionbar_text</item>
        </style>
        <!-- action bar tab text -->
        <style name="TabTextStyle" parent="@style/Widget.AppCompat.ActionBar.TabText">
            <item name="android:textColor">@color/actionbar_text</item>
        </style>
    </resources>

    在您的app manifest file, 可以让整个app使用客制化的Theme <application android:theme="@style/CustomActionBarTheme" ... />
    或者让 activity 使用客制化的Theme<activity android:theme="@style/CustomActionBarTheme" ... />
    ActionBar的其他属性修改,也可以参考该方法,具体可以参考Android developer上的说明。
    http://developer.android.com/guide/topics/ui/actionbar.html#Style

    ==================================================================================

    如何将一个app 设置为持久app, 不被
    low memory kill 关闭
    [Description]
    如何将一个app 设置为常住app, 不被low memory kill 关闭
    [Keyword]
    app 持久 persistent
    [Solution]
    1. 将app 的manifest.xml 中的 application 中添加属性
    android:persistent="true"
    2. 对这个APP 使用platform 的签名
    3. 放置在system/app 下面
    注: 一个app 被设置为 persistent 后,将很难被low memory kill 杀掉(oom_adj=-
    12),请在设置之前仔细确认是否必须,否则将浪费掉memory。

    =========================================================================

    如何设置进程/线程 的调度模式
    (sched policy),优先级(priority)
    [Title]
    如何设置进程/线程 的调度模式,优先级
    [Description]
    如何将进程的优先级调整,提高,降低;如何将设置进程进程的调度模式,如实时进程(实时轮转RR,实时FIFO)
    linux 中调度模式有: SCHED_OTHER, SCHED_IDLE, SCHED_BATCH 以及 SCHED_FIFO,SCHED_RR; 前
    3种为CFS 调度模式,后2种为实时调度模式
    [Keyword]
    进程优先级 进程调度 实时进程 实时线程 rt thread process setpriority sched_setscheduler
    SCHED_FIFO SCHED_RR
    [Solution]
    Java API:
    在java 层,
    设置线程优先级:
    * 使用java 默认的Thread.setPriority(int priority); priority 的值为[1,10], 即此priority
    为java spec 定义, 可参考Thread.java
    java property : 1 2 3 4 5 6 7 8 9 10
    linux nice: 19 16 13 10 0 -2 -4 -5 -6 -8
    * 使用android.os.Process 的API:
    public static final native void setThreadPriority(int tid, int priority);
    public static final native void setThreadPriority(int priority)
    两个API 中的priority 为标准的linux nice 值 [-20,19]
    * @param tid The identifier of the thread/process to change.
    * @param priority A Linux priority level, from -20 for highest scheduling
    设置线程调度模式:
    * 标准java Spec 中并没有直接的API 可供使用,在android 中, android.os.Process 提供API:
    public static final native void setThreadScheduler(int tid, int policy, int priority)
    throws IllegalArgumentException;
    对应它的三个参数分别为:
    * @param tid The identifier of the thread/process to change.
    * @param policy A Linux scheduling policy such as SCHED_OTHER etc.
    * @param priority A Linux priority level in a range appropriate for the given policy.
    在native 层:
    设置线程优先级:
    * setprority(int which, int who, int niceval);
    @which 为PRIO_PROCESS, PRIO_PGRP, PRIO_USER; 默认一般使用PRIO_PROCESS 即只设置单个
    thread, 如果是PRIO_PGRP 即整个进程, 如同是PRIO_USER 则所有USER 都为此进程USER 的进程。
    @who 为对应的线程, 如果为0 即当前线程
    @niceval 为对应的nice 值[-20,19]
    设置线程调度模式:
    #include <sched.h>
    struct sched_param {
    ...
    int sched_priority;
    ...
    };
    int sched_setscheduler(pid_t pid, int policy, const struct sched_param *param);
    @pid 为对应的线程, 如果为0, 即当前的线程
    @policy 为SCHED_OTHER, SCHED_IDLE, SCHED_BATCH 以及 SCHED_FIFO,SCHED_RR 中的一种
    @param 目前唯一需要设置的就是这个param 里面的sched_priority,为实时进程优先级[1,99] 如
    果是SCHED_OTHER, SCHED_IDLE, SCHED_BATCH, 那么sched_priority 必须为0.
    具体的相关约束可以参考linux man: 如: http://man7.org/linux/manpages/
    man2/sched_setscheduler.2.html

    ========================================================================================

    如何在编译的时候修改xlog选项
    [DESCRIPTION]
    当希望过滤某些log时,可以通过在编译的时候,修改xlog选项来实现
    [SOLUTION]
    修改mediatek/external/xlog/tools/tag-setting.xlog 或tag-default.xlog. 会优
    先考虑/tag-setting.xlog
    的设置.
    (ICS/ICS2版本,在这里:mediatek/source/external/xlog/tools/tagsetting.
    xlog )
    available level:
    verbose debug

    available level:
    verbose debug info warn error assert on off

    例如:如果需要关闭所有的log做法如下:
    修改merge-xlog-filter-tags.py
    default_level =None => default_level = "off"

    ====================================================================

    如何确认故障机的system分区是否被
    破坏或者刷机
    [DESCRIPTION]
    当故障机 发生随机乱死(NE或者KE),则一个排查重点就是手机中的system分区中的档案是否有被
    刷过或者破坏。因此,需要通过Flash Tool把system.img从故障机中readback回来,并且用
    ext2explore解开,然后用工具(Beyond Compare)比较system分区中的任何档案是否有被刷过或者
    破坏。
    [KEY WORDS]
    system分区 readback 刷机
    [SOLUTION]
    步骤一:分别从故障机和正常机器中readback并解开system.img档案。该步骤可参考如下的FAQ:
    《FAQ10664 如何在windows下解开readback回来的ext image》
    步骤二:用Beyond Compare工具打开 正常机器与故障机器的system分区档案
    步骤三:在Beyond Compare图形界面上选中 待比较的所有folder/file,然后点击鼠标右键,并在
    弹出的选项中 选中Compare Contents。该步骤如下图所示:

     

    步骤四:选中 Binary Comparison,然后点击Start,则执行compare动作。该步骤如下图所示:

     

    假如所有档案内容完全一致,则说明system分区没有被破坏或者刷过,参考下图所示:

     

     

    展开全文
  • 安卓Settings 系统自带设置软件源码
  • Android 7.1 Settings详解

    万次阅读 2018-01-08 09:28:46
    最近一直在看settings的问题,觉得不错,就研究了下,写出来方便以后查找问题,不用每次都去重新看,如有不对的地方,欢迎纠正。 在Android N 上Settings是带有侧拉菜单的,我们先从界面的角度大致看下Settings是...

    最近一直在看settings的问题,觉得不错,就研究了下,写出来方便以后查找问题,不用每次都去重新看,如有不对的地方,欢迎纠正。
    在Android N 上Settings是带有侧拉菜单的,我们先从界面的角度大致看下Settings是怎么显示出来,然后再看下view对应的数据是如何加载而来的,先来看看设置的界面如下:
    图一
    从图片可以看出主界面有点类似listview的感觉,只不过所有的item分为四类,依次为无线和网络、设备、个人、系统,接着我们就从源码的角度看下界面是如何显示出来的。Settings源码分为两个部分,分别为
    “packages/apps/Settings”和“frameworks/base/packages/SettingsLib”,主界面对应的布局文件如下:

    frameworks/base/packages/SettingsLib/res/layout/settings_with_drawer.xml
        <android.support.v4.widget.DrawerLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/drawer_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="?android:attr/colorPrimaryDark">
        <!-- 设置主界面 -->
        <LinearLayout
            android:id="@+id/content_parent"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            android:fitsSystemWindows="true" >
            <FrameLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                style="?android:attr/actionBarStyle">
                <Toolbar
                    xmlns:android="http://schemas.android.com/apk/res/android"
                    android:id="@+id/action_bar"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:navigationContentDescription="@*android:string/action_bar_up_description"
                    android:theme="?android:attr/actionBarTheme"
                    style="?android:attr/toolbarStyle"
                    android:background="?android:attr/colorPrimary" />
            </FrameLayout>
            <FrameLayout
                android:id="@+id/content_header_container"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                style="?android:attr/actionBarStyle" />
            <FrameLayout
                android:id="@+id/content_frame"
                android:layout_width="match_parent"
                android:layout_height="fill_parent"
                android:background="?android:attr/windowBackground" />
        </LinearLayout>
        <!-- 侧拉抽屉 -->
        <ListView android:id="@+id/left_drawer"
            android:layout_width="300dp"
            android:layout_height="match_parent"
            android:layout_gravity="start"
            android:choiceMode="singleChoice"
            android:divider="@android:color/transparent"
            android:dividerHeight="0dp"
            android:background="?android:attr/colorBackground" />
        </android.support.v4.widget.DrawerLayout>

    从布局文件可看出,整个布局为DrawerLayout,主界面为一个FrameLayout,还有个left_drawer对应侧边的侧滑栏,界面显示如下:
    设置侧拉菜单
    左边的侧滑栏比较简单就不罗嗦了,主要看下settings主界面是如何显示出来的,设置主界面入口为SettingsActivity.java是SettingsDrawerActivity.java的子类

    packages/apps/Settings/src/com/android/settings/SettingsActivity.java
        @Override
        protected void onCreate(Bundle savedState) {
            super.onCreate(savedState);
    
            ...
            setContentView(mIsShowingDashboard ?
                    R.layout.settings_main_dashboard : R.layout.settings_main_prefs);加载主界面,为一个fragment
    
            mContent = (ViewGroup) findViewById(R.id.main_content);
    
            getFragmentManager().addOnBackStackChangedListener(this);
    
    
            if (savedState != null) {
                // We are restarting from a previous saved state; used that to initialize, instead
                // of starting fresh.
                mSearchMenuItemExpanded = savedState.getBoolean(SAVE_KEY_SEARCH_MENU_EXPANDED);
                mSearchQuery = savedState.getString(SAVE_KEY_SEARCH_QUERY);
                setTitleFromIntent(intent);
    
                ArrayList<DashboardCategory> categories =
                        savedState.getParcelableArrayList(SAVE_KEY_CATEGORIES);
                if (categories != null) {
                    mCategories.clear();
                    mCategories.addAll(categories);
                    setTitleFromBackStack();
                }
    
                mDisplayHomeAsUpEnabled = savedState.getBoolean(SAVE_KEY_SHOW_HOME_AS_UP);
                mDisplaySearch = savedState.getBoolean(SAVE_KEY_SHOW_SEARCH);
    
            } else {
                if (!mIsShowingDashboard) {
                    mDisplaySearch = false;
                    // UP will be shown only if it is a sub settings
                    if (mIsShortcut) {
                        mDisplayHomeAsUpEnabled = isSubSettings;
                    } else if (isSubSettings) {
                        mDisplayHomeAsUpEnabled = true;
                    } else {
                        mDisplayHomeAsUpEnabled = false;
                    }
                    setTitleFromIntent(intent);
    
                    Bundle initialArguments = intent.getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS);
                    switchToFragment(initialFragmentName, initialArguments, true, false,
                            mInitialTitleResId, mInitialTitle, false);
                } else {
                    // No UP affordance if we are displaying the main Dashboard
                    mDisplayHomeAsUpEnabled = false;
                    // Show Search affordance
                    mDisplaySearch = true;
                    mInitialTitleResId = R.string.dashboard_title;
    
                    // add argument to indicate which settings tab should be initially selected
                    final Bundle args = new Bundle();
                    final String extraName = DashboardContainerFragment.EXTRA_SELECT_SETTINGS_TAB;
                    args.putString(extraName, intent.getStringExtra(extraName));
    
                    switchToFragment(DashboardContainerFragment.class.getName(), args, false, false,mInitialTitleResId, mInitialTitle, false);切换到DashboardContainerFragment
                }
            }
            ...
        }

    在onCreate中加载了一个R.layout.settings_main_dashboard,布局文件很简单里面就一个FrameLayout,然后通过
    switchToFragment,切换到DashboardContainerFragment,需要注意的是在调用switchToFragment切换到DashboardContainerFragment之前会检查Fragment是否有效,通过判断当前fragmentName是否为SettingsActivity.ENTRY_FRAGMENTS数组里定义的,否则会抛出IllegalArgumentException “Invalid fragment for this activity: “,到这里我们继续接着DashboardContainerFragment分析,DashboardContainerFragment的onCreateView如下:

    packages/apps/Settings/src/com/android/settings/dashboard/DashboardContainerFragment.java
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
            final View content = inflater.inflate(R.layout.dashboard_container, parent, false);加载一个自定义的viewpager
            mViewPager = (RtlCompatibleViewPager) content.findViewById(R.id.pager);
            mPagerAdapter = new DashboardViewPagerAdapter(getContext(),
                    getChildFragmentManager(), mViewPager);
            mViewPager.setAdapter(mPagerAdapter);
            mViewPager.addOnPageChangeListener(
                    new TabChangeListener((SettingsActivity) getActivity()));
    
            // check if support tab needs to be selected
            final String selectedTab = getArguments().
                getString(EXTRA_SELECT_SETTINGS_TAB, ARG_SUMMARY_TAB);
            if (TextUtils.equals(selectedTab, ARG_SUPPORT_TAB)) {
                mViewPager.setCurrentItem(INDEX_SUPPORT_FRAGMENT);
            } else {
                mViewPager.setCurrentItem(INDEX_SUMMARY_FRAGMENT);
            }
    
            mHeaderView = inflater.inflate(R.layout.dashboard_container_header, parent, false);
            ((SlidingTabLayout) mHeaderView).setViewPager(mViewPager);
            return content;
        }

    加载了一个RtlCompatibleViewPager,然后设置adapter为DashboardViewPagerAdapter,addOnPageChangeListener为TabChangeListener,DashboardViewPagerAdapter为FragmentPagerAdapter的子类,其实现如下:

    packages/apps/Settings/src/com/android/settings/dashboard/DashboardContainerFragment.java
        private static final class DashboardViewPagerAdapter extends FragmentPagerAdapter {
    
            private final Context mContext;
            private final SupportFeatureProvider mSupportFeatureProvider;
            private final RtlCompatibleViewPager mViewPager;
    
            public DashboardViewPagerAdapter(Context context, FragmentManager fragmentManager,
                    RtlCompatibleViewPager viewPager) {
                super(fragmentManager);
                mContext = context;
                mSupportFeatureProvider =
                        FeatureFactory.getFactory(context).getSupportFeatureProvider(context);
                mViewPager = viewPager;
            }
    
            @Override
            public CharSequence getPageTitle(int position) {
                switch (position) {
                    case INDEX_SUMMARY_FRAGMENT:
                        return mContext.getString(R.string.page_tab_title_summary);
                    case INDEX_SUPPORT_FRAGMENT:
                        return mContext.getString(R.string.page_tab_title_support);
                }
                return super.getPageTitle(position);
            }
    
            @Override
            public Fragment getItem(int position) {
                switch (position) {返回DashboardSummary
                    case INDEX_SUMMARY_FRAGMENT:
                        return new DashboardSummary();
                    case INDEX_SUPPORT_FRAGMENT:
                        return new SupportFragment();
                    default:
                        throw new IllegalArgumentException(
                                String.format(
                                        "Position %d does not map to a valid dashboard fragment",
                                        position));
                }
            }
    
            @Override
            public Object instantiateItem(ViewGroup container, int position) {
                return super.instantiateItem(container,
                        mViewPager.getRtlAwareIndex(position));
            }
    
            @Override
            public int getCount() {
                return mSupportFeatureProvider == null ? 1 : 2;
            }
        }

    mSupportFeatureProvider为null,默认只有一页即显示DashboardSummary,在DashboardSummary@onCreateView里填充了R.layout.dashboard,这个布局文件主要包含一个FocusRecyclerView

    packages/apps/Settings/src/com/android/settings/dashboard/DashboardSummary.java
        @Override
        public void onViewCreated(View view, Bundle bundle) {
            long startTime = System.currentTimeMillis();
            mDashboard = (FocusRecyclerView) view.findViewById(R.id.dashboard_container);
            mLayoutManager = new LinearLayoutManager(getContext());
            mLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
            if (bundle != null) {
                int scrollPosition = bundle.getInt(EXTRA_SCROLL_POSITION);
                mLayoutManager.scrollToPosition(scrollPosition);
            }
            mDashboard.setLayoutManager(mLayoutManager);
            mDashboard.setHasFixedSize(true);
            mDashboard.setListener(this);
            mDashboard.addItemDecoration(new DashboardDecorator(getContext()));设置分割线
            mAdapter = new DashboardAdapter(getContext(), mSuggestionParser, bundle,
                    mConditionManager.getConditions());
            mDashboard.setAdapter(mAdapter);设置adapter
            mSummaryLoader.setAdapter(mAdapter);
            ConditionAdapterUtils.addDismiss(mDashboard);
            if (DEBUG_TIMING) Log.d(TAG, "onViewCreated took "
                    + (System.currentTimeMillis() - startTime) + " ms");
            rebuildUI();
        }

    在这儿设置RecyclerView的setLayoutManager为垂直线性布局,设置adapter为DashboardAdapter以及rebuildUI,接下来主要看下DashboardAdapter,DashboardAdapter为RecyclerView.Adapter的子类,通过onCreateViewHolder和onBindViewHolder来创建和显示view

    packages/apps/Settings/src/com/android/settings/dashboard/DashboardAdapter.java
    
        @Override
        public DashboardItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            return new DashboardItemHolder(LayoutInflater.from(parent.getContext()).inflate(
                    viewType, parent, false), (viewType == R.layout.dashboard_tile_switch));
        }
    
        @Override
        public void onBindViewHolder(DashboardItemHolder holder, int position) {
            switch (mTypes.get(position)) {
                case R.layout.dashboard_category:对应类别
                    onBindCategory(holder, (DashboardCategory) mItems.get(position));
                    break;
                case R.layout.dashboard_tile:对应每个子item
                    final Tile tile = (Tile) mItems.get(position);
                    onBindTile(holder, tile);
                    holder.itemView.setTag(tile);
                    holder.itemView.setOnClickListener(this);
                    break;
                case R.layout.dashboard_tile_switch:
                    final Tile tileSitch = (Tile) mItems.get(position);
                    mLte4GEnablerHolder = holder;
                    onBindTile(holder, tileSitch);
                    holder.itemView.setOnClickListener(this);
                    mSw = (Switch) holder.itemView.findViewById(R.id.switchWidget);
                    mLte4GEnabler.setSwitch(mSw);
                    holder.itemView.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                           mSw.setChecked(!mSw.isChecked());
                           set4GEnableSummary(mSw.isChecked());
                        }
                    });
                    updateLte4GEnabler();
                    break;
                case R.layout.suggestion_header:
                    onBindSuggestionHeader(holder);
                    break;
                case R.layout.suggestion_tile:
                    final Tile suggestion = (Tile) mItems.get(position);
                    onBindTile(holder, suggestion);
                    holder.itemView.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            MetricsLogger.action(mContext, MetricsEvent.ACTION_SETTINGS_SUGGESTION,
                                    DashboardAdapter.getSuggestionIdentifier(mContext, suggestion));
                            ((SettingsActivity) mContext).startSuggestion(suggestion.intent);
                        }
                    });
                    holder.itemView.findViewById(R.id.overflow).setOnClickListener(
                            new View.OnClickListener() {
                                @Override
                                public void onClick(View v) {
                                    showRemoveOption(v, suggestion);
                                }
                            });
                    break;
                case R.layout.see_all:
                    onBindSeeAll(holder);
                    break;
                case R.layout.condition_card:
                    ConditionAdapterUtils.bindViews((Condition) mItems.get(position), holder,
                            mItems.get(position) == mExpandedCondition, this,
                            new View.OnClickListener() {
                                @Override
                                public void onClick(View v) {
                                    onExpandClick(v);
                                }
                            });
                    break;
            }
        }
    
        @Override
        public int getItemViewType(int position) {
            return mTypes.get(position);
        }
    
        @Override
        public int getItemCount() {
            return mIds.size();
        }

    在onBindViewHolder中通过type类型来创建不同的view类型并设置相应的点击事件R.layout.dashboard_category,对应着前面提到的四种类别(无线和网络、设备、个人、系统),R.layout.dashboard_tile对应着每个类别下的item(WLAN、蓝牙、流量使用情况等),每个item点击后,会调用 ((SettingsActivity) mContext).openTile((Tile) v.getTag())跳转到对应的界面,讲到这整个界面的显示就说的差不多了,接下来看下界面对应的数据是怎么来的。
    在DashboardSummary@rebuildUI里会调用DashboardAdapter@setCategories的

    packages/apps/Settings/src/com/android/settings/dashboard/DashboardAdapter.java
        public void setCategories(List<DashboardCategory> categories) {
            mCategories = categories;
    
            // TODO: Better place for tinting?
            TypedValue tintColor = new TypedValue();
            mContext.getTheme().resolveAttribute(com.android.internal.R.attr.colorAccent,
                    tintColor, true);
            for (int i = 0; i < categories.size(); i++) {
                for (int j = 0; j < categories.get(i).tiles.size(); j++) {
                    Tile tile = categories.get(i).tiles.get(j);
    
                    if (!mContext.getPackageName().equals(
                            tile.intent.getComponent().getPackageName())) {
                        // If this drawable is coming from outside Settings, tint it to match the
                        // color.
                        tile.icon.setTint(tintColor.data);
                    }
                }
            }
            recountItems();  //根据categories重新构建item,为DashboardAdapter提供数据
        }
        private void recountItems() {
            reset();
            boolean hasConditions = false;
            for (int i = 0; mConditions != null && i < mConditions.size(); i++) {
                boolean shouldShow = mConditions.get(i).shouldShow();
                hasConditions |= shouldShow;
                countItem(mConditions.get(i), R.layout.condition_card, shouldShow, NS_CONDITION);
            }
            boolean hasSuggestions = mSuggestions != null && mSuggestions.size() != 0;
            countItem(null, R.layout.dashboard_spacer, hasConditions && hasSuggestions, NS_SPACER);
            countItem(null, R.layout.suggestion_header, hasSuggestions, NS_SPACER);
            resetCount();
            if (mSuggestions != null) {
                int maxSuggestions = getDisplayableSuggestionCount();
                for (int i = 0; i < mSuggestions.size(); i++) {
                    countItem(mSuggestions.get(i), R.layout.suggestion_tile, i < maxSuggestions,
                            NS_SUGGESTION);
                }
            }
            resetCount();
            for (int i = 0; mCategories != null && i < mCategories.size(); i++) {
                DashboardCategory category = mCategories.get(i);
                countItem(category, R.layout.dashboard_category, mIsShowingAll, NS_ITEMS);
                for (int j = 0; j < category.tiles.size(); j++) {
                    Tile tile = category.tiles.get(j);
                    if (tile.intent.getComponent().getClassName().contains(LTE_4G_ACTIVITY)) {
                        countItem(tile, R.layout.dashboard_tile_switch, mIsShowingAll ||
                                ArrayUtils.contains(DashboardSummary.INITIAL_ITEMS,
                                tile.intent.getComponent().getClassName()), NS_ITEMS);
                    } else {
                            countItem(tile, R.layout.dashboard_tile, mIsShowingAll
                                || ArrayUtils.contains(DashboardSummary.INITIAL_ITEMS,
                                tile.intent.getComponent().getClassName()), NS_ITEMS);
                        }
                    }
                }
            }
            notifyDataSetChanged();
        }

    那么DashboardAdapter@setCategories(List <DashboardCategory> categories) 传入的categories从何而来来呢

        private void rebuildUI() {
            if (!isAdded()) {
                Log.w(TAG, "Cannot build the DashboardSummary UI yet as the Fragment is not added");
                return;
            }
    
            List<DashboardCategory> categories =
                    ((SettingsActivity) getActivity()).getDashboardCategories();
            mAdapter.setCategories(categories);
    
            // recheck to see if any suggestions have been changed.
            new SuggestionLoader().execute();
        }

    在DashboardSummary@rebuildUI中我们知道,categories是调用SettingsActivity的方法获取的,getDashboardCategories这个方法是定义在SettingsActivity的父类SettingsDrawerActivity.java里定义的,如下:

    frameworks/base/packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerActivity.java
        public List<DashboardCategory> getDashboardCategories() {
            if (sDashboardCategories == null) {
                sTileCache = new HashMap<>();
                sConfigTracker = new InterestingConfigChanges();
                // Apply initial current config.
                sConfigTracker.applyNewConfig(getResources());
                sDashboardCategories = TileUtils.getCategories(this, sTileCache);
            }
            return sDashboardCategories;
        }

    sDashboardCategories是通过TileUtils.getCategories赋值的,接着我们继续跟下这个方法

    frameworks/base/packages/SettingsLib/src/com/android/settingslib/drawer/TileUtils.java
        public static List<DashboardCategory> getCategories(Context context,
                HashMap<Pair<String, String>, Tile> cache) {
            final long startTime = System.currentTimeMillis();
            boolean setup = Global.getInt(context.getContentResolver(), Global.DEVICE_PROVISIONED, 0)
                    != 0;
            ArrayList<Tile> tiles = new ArrayList<>();
            UserManager userManager = UserManager.get(context);
            for (UserHandle user : userManager.getUserProfiles()) {
                // TODO: Needs much optimization, too many PM queries going on here.
                if (user.getIdentifier() == ActivityManager.getCurrentUser()) {
                    // Only add Settings for this user.
                    //解析AndroidManifest.xml
                    getTilesForAction(context, user, SETTINGS_ACTION, cache, null, tiles, true);
                    getTilesForAction(context, user, OPERATOR_SETTINGS, cache,
                            OPERATOR_DEFAULT_CATEGORY, tiles, false);
                    getTilesForAction(context, user, MANUFACTURER_SETTINGS, cache,
                            MANUFACTURER_DEFAULT_CATEGORY, tiles, false);
                }
                if (setup) {
                    getTilesForAction(context, user, EXTRA_SETTINGS_ACTION, cache, null, tiles, false);
                }
            }
            HashMap<String, DashboardCategory> categoryMap = new HashMap<>();
            for (Tile tile : tiles) {
                DashboardCategory category = categoryMap.get(tile.category);
                if (category == null) {
                    //通过已有的category作为action去查找DashboardCategory,即(无线网络、设备、个人、系统)
                    category = createCategory(context, tile.category);
                    if (category == null) {
                        Log.w(LOG_TAG, "Couldn't find category " + tile.category);
                        continue;
                    }
                    categoryMap.put(category.key, category);
                }
                category.addTile(tile);
            }
            ArrayList<DashboardCategory> categories = new ArrayList<>(categoryMap.values());
            for (DashboardCategory category : categories) {
                Collections.sort(category.tiles, TILE_COMPARATOR);
            }
            Collections.sort(categories, CATEGORY_COMPARATOR);
            if (DEBUG_TIMING) Log.d(LOG_TAG, "getCategories took "
                    + (System.currentTimeMillis() - startTime) + " ms");
            return categories;
        }

    在这个方法中主要通过getTilesForIntent获取所有的Tile,其实主要就是通过PackageManager@queryIntentActivitiesAsUser通过传入的intnet解析AndroidManifest.xml配置的每项,解析设置中每个item(WLAN、蓝牙、流量使用情况、显示、通知…)

    frameworks/base/packages/SettingsLib/src/com/android/settingslib/drawer/TileUtils.java
        public static void getTilesForIntent(Context context, UserHandle user, Intent intent,
                Map<Pair<String, String>, Tile> addedCache, String defaultCategory, List<Tile> outTiles,
                boolean usePriority, boolean checkCategory) {
            PackageManager pm = context.getPackageManager();
             /*<action android:name="com.android.settings.action.SETTINGS" /> 根据action去查找所有匹配的项*/
            List<ResolveInfo> results = pm.queryIntentActivitiesAsUser(intent,
                    PackageManager.GET_META_DATA, user.getIdentifier());
            for (ResolveInfo resolved : results) {
                if (!resolved.system) {
                    // Do not allow any app to add to settings, only system ones.
                    continue;
                }
                ActivityInfo activityInfo = resolved.activityInfo;
                /*解析xml中的meta-data节点*/
                Bundle metaData = activityInfo.metaData;
                String categoryKey = defaultCategory;
                if (checkCategory && ((metaData == null) || !metaData.containsKey(EXTRA_CATEGORY_KEY))
                        && categoryKey == null) {
                    Log.w(LOG_TAG, "Found " + resolved.activityInfo.name + " for intent "
                            + intent + " missing metadata "
                            + (metaData == null ? "" : EXTRA_CATEGORY_KEY));
                    continue;
                } else {
                    categoryKey = metaData.getString(EXTRA_CATEGORY_KEY);
                }
                Pair<String, String> key = new Pair<String, String>(activityInfo.packageName,
                        activityInfo.name);
                Tile tile = addedCache.get(key);
                if (tile == null) {
                    tile = new Tile();
                    tile.intent = new Intent().setClassName(
                            activityInfo.packageName, activityInfo.name);
                    tile.category = categoryKey;
                    tile.priority = usePriority ? resolved.priority : 0;
                    tile.metaData = activityInfo.metaData;
                    updateTileData(context, tile, activityInfo, activityInfo.applicationInfo,
                            pm);
                    if (DEBUG) Log.d(LOG_TAG, "Adding tile " + tile.title);
    
                    addedCache.put(key, tile);
                }
                if (!tile.userHandle.contains(user)) {
                    tile.userHandle.add(user);
                }
                if (!outTiles.contains(tile)) {
                    outTiles.add(tile);
                }
            }
        }

    对应解析的文件如下:

      packages/apps/Settings/AndroidManifest.xml
        <activity android:name="Settings$WifiSettingsActivity"
                android:taskAffinity=""
                android:label="@string/wifi_settings"
                android:icon="@drawable/ic_settings_wireless"
                android:configChanges="orientation|keyboardHidden|screenSize">
            <intent-filter android:priority="1">
                <action android:name="android.settings.WIFI_SETTINGS" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.VOICE_LAUNCH" />
                <category android:name="com.android.settings.SHORTCUT" />
            </intent-filter>
            <intent-filter android:priority="4">
                <action android:name="com.android.settings.action.SETTINGS" />
            </intent-filter>
            <meta-data android:name="com.android.settings.category"
                android:value="com.android.settings.category.wireless" />
            <meta-data android:name="com.android.settings.FRAGMENT_CLASS"
                android:value="com.android.settings.wifi.WifiSettings" />
            <meta-data android:name="com.android.settings.PRIMARY_PROFILE_CONTROLLED"
                android:value="true" />
        </activity>

    解析并创建对应类别,如无线和网络,设备、个人、系统

    frameworks/base/packages/SettingsLib/src/com/android/settingslib/drawer/TileUtils.java
         private static DashboardCategory createCategory(Context context, String categoryKey) {
            DashboardCategory category = new DashboardCategory();
            category.key = categoryKey;
            PackageManager pm = context.getPackageManager();
            List<ResolveInfo> results = pm.queryIntentActivities(new Intent(categoryKey), 0);
            if (results.size() == 0) {
                return null;
            }
            for (ResolveInfo resolved : results) {
                if (!resolved.system) {
                    // Do not allow any app to add to settings, only system ones.
                    continue;
                }
                category.title = resolved.activityInfo.loadLabel(pm);
                category.priority = SETTING_PKG.equals(
                        resolved.activityInfo.applicationInfo.packageName) ? resolved.priority : 0;
                if (DEBUG) Log.d(LOG_TAG, "Adding category " + category.title +" ,categoryKey="+categoryKey);
            }
    
            return category;
        }
    

    对应解析的xml文件

     packages/apps/Settings/AndroidManifest.xml
        <activity android:name=".Settings$WirelessSettings"
                android:label="@string/header_category_wireless_networks">
                <intent-filter android:priority="4">
                    <action android:name="com.android.settings.category.wireless" />
                    <category android:name="android.intent.category.DEFAULT" />
                </intent-filter>
        </activity>

    所有的data都是通过解析AndroidManifest.xml文件,然后把对应数据显示在界面的,讲到这就分解的差不多了,简单总结下:
    1、设置对应的界面是SettingsActivity extends SettingsDrawerActivity,在SettingsActivity最后是加载了DashboardSummary,这个fragment加载了一个FocusRecyclerView,并设置adapter为DashboardAdapter
    2、DashboardAdapter的数据是通过TileUtils@getCategories里的getTilesForAction和createCategory解析AndroidManifest.xml文件,返回List < DashboardCategory >供DashboardAdapter使用

    展开全文
  • Settings.settings 位于 Visual Basic 项目的 My Project 文件夹和 Visual C# 项目的 Properties 文件夹中。 然后,“项目设计器”在项目的根文件夹中搜索其他设置文件。 因此,您应当将自定义的设置文件放在此根...
  • android setting之Settings.system设置

    万次阅读 2019-06-22 13:15:24
    android setting 之Settings.system
  • Maven配置文件settings.xml详解

    万次阅读 多人点赞 2018-11-28 18:28:06
    settings.xml 作为Maven的配置文件,对应的两个层级的配置文件的路径会在 1. Maven用户层级配置文件 用户层级 配置文件是提供给某个具体的用户,通常该配置文件路径为: ${user.home} /.m2/settings.xml ${...
  • ms-settings是什么

    千次阅读 2020-05-04 20:18:56
    ms-settings 遇到了两个问题,记录一下 1)windows桌面右键菜单–>显示设置或者个性化–>报错:ms-settings:personalization-background 2)win10桌面右下角网络图标–>网络和internet设置点击无反应 第二...
  • settings.jar

    热门讨论 2015-08-04 22:15:54
    Android studio 升级之后,想导入上个版本的设置所用的jar包,即settings.jar
  • Maven的仓库和settings.xml配置文件

    万次阅读 多人点赞 2018-10-30 23:01:20
    (尊重劳动成果,转载请注明出处:https://blog.csdn.net/qq_25827845/article/details/83549846冷血之心的博客) ...Maven的仓库和settings.xml配置文件 Maven的坐标与依赖 Maven的生命周期...
  • android 7.0中Settings新功能全面解析

    万次阅读 2017-02-27 19:26:19
    Settings N预览android N 在Settings中作了一些调整,如上面的截图。 - 增加了侧滑菜单,采用v4下的DrawerLayout来实现; - 在Settings主界面增加了Condition,能够在设置列表中显示状态; - 在Settings主界面...
  • Android O Settings界面简介

    千次阅读 2018-06-12 17:51:38
    在介绍Android O之前,先要介绍Android N的Settings模块,在N上面谷歌对架构做了调整,Settings增加了一个侧滑按钮。android N 在Settings中作了一些调整,如上面的截图。- 增加了侧滑菜单,采用v4下的DrawerLayout...
  • Android Settings总结

    千次阅读 2016-05-07 10:19:07
    1.Settings简介Settings,包括手机各项属性的基本调整和功能的开关,是用户根据个人喜好对手机进行定制的最方便的入口,也是用户在日常生活中使用频率最高的模块之一。因此,它的稳定性、修改定制,对于开发者来说尤...
  • Android APK开发基础—— Settings

    千次阅读 2018-07-02 14:37:01
    那么如何实现了,其实android SDK给我们提供了android.provider.Settings类,该类提供android系统各个页面的跳转常量,:     package android.provider; import android.content.ContentResolver; im...
  • Android系统APP之SettingsProvider

    万次阅读 多人点赞 2017-03-01 14:53:31
    Android系统APP之SettingsProvider 设置共享 系统设置 Android系统APP之SettingsProvider 前言 SettingsProvider概览 主要源码 数据分类 AndroidManifestxml配置 SettingsProvider的启动过程 封装Settings...
  • Android9.0 Settings 修改踩坑记录

    千次阅读 2019-11-12 18:22:45
    看了下 Android10.0 下 Settings 的 mk,发现已经全部替换为 androidx Android.mk LOCAL_STATIC_ANDROID_LIBRARIES : = \ androidx - constraintlayout_constraintlayout \ androidx . slice_slice - ...
  • | section of this document (settings.xml) - will be discussed later. Another way essentially | relies on the detection of a system property, either matching a particular value for the property, | ...
  • android 8.1Settings添加设置项

    千次阅读 2019-10-09 16:52:30
    OS:MTK 8.1 需求:设置中添加一级菜单,在添加的一级菜单下再添加一个二...packages/apps/Settings/src/com/android/settings/Settings.java public static class ScanSettingActivity extends SettingsActivit...
  • IntelliJ IDEA 自带Maven插件找不到settings.xml配置文件

    万次阅读 多人点赞 2020-02-21 20:04:28
    一、概述 Apache Maven 是一套软件工程管理和整合工具。基于工程对象模型(POM)的概念,通过一个中央...我们使用的开发工具 IDEA 也自带了 Maven 插件,但是问题来了(IntelliJ IDEA 自带Maven插件找不到settings...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 901,399
精华内容 360,559
关键字:

settings