静态壁纸的设置

Posted by DoubleWay on May 27, 2020

静态壁纸的设置

静态壁纸就是在SystemUI中名为ImageWallpaper的特殊动态壁纸,继承了WallpaperService,实现的架构就是动态壁纸的架构,只不过它实现的内容就是一张静态图片,将它和动态壁纸分开来是因为Android给ImageWallpaper提供了很多方便的API

API29之前

静态壁纸的设置不像动态壁纸一样需要签名级系统权限,仅仅需要android.permission.SET_WALLPAPER权限就可以通过WallpaperManager设置静态壁纸,WallpaperManager可以设置壁纸文件的位置,在它设置完壁纸的位置后,Android提供了WallpaperObserver,在WallpaperManagerService中(WallpaperManagerService.WallpaperObserver)

private class WallpaperObserver extends FileObserver {
    final int mUserId;
    final WallpaperData mWallpaper;
    final File mWallpaperDir;
    final File mWallpaperFile;
    final File mWallpaperLockFile;

    public WallpaperObserver(WallpaperData wallpaper) {
        super(getWallpaperDir(wallpaper.userId).getAbsolutePath(),
                CLOSE_WRITE | MOVED_TO | DELETE | DELETE_SELF);
        mUserId = wallpaper.userId;
        mWallpaperDir = getWallpaperDir(wallpaper.userId);
        mWallpaper = wallpaper;
        mWallpaperFile = new File(mWallpaperDir, WALLPAPER);
        mWallpaperLockFile = new File(mWallpaperDir, WALLPAPER_LOCK_ORIG);
    }

这个类继承自FileObserver,这是android提供的一个工具类,用来监听文件系统文件的增加,修改删除等操作,FileObserver可以监听一个文件或者是文件夹,当它发生变化的时候,onEvent方法里面回调根据参数就能获得这些操作的文件路径,主要监听的参数是CLOSE_WRITE,DELETE,DELETE_SELF,后两种是因为位图被保存在文件系统中,保护非常弱,需要处理其被删除的情况

public void onEvent(int event, String path) {
            if (path == null) {
                return;
            }
            final boolean moved = (event == MOVED_TO);
            final boolean written = (event == CLOSE_WRITE || moved);
            final File changedFile = new File(mWallpaperDir, path);

在onEvent中最后调用的是bindWallpaperComponentLocked方法,这和动态壁纸的设置是一样的,这也就意味着在壁纸文件位置重新设置之后,不会通知原有的DrawableEngine(ImageWallpaper.DrawableEngine)进行更新和重绘,而是销毁原有的。重新创建,

DrawableEngin设置的流程如下;

mLoader = new AsyncTask<Void, Void, Bitmap>() {
                @Override
                protected Bitmap doInBackground(Void... params) {
                    Throwable exception;
                    try {
                        if (needsReset) {
                            mWallpaperManager.forgetLoadedWallpaper();
                        }
  1. 调用wallpaperManager的forgetLoadedWallpaper,将mDefaultWallpaper和mCachedWallpaper设置成null。这两个变量一个存储系统默认的壁纸,另一个存储现有的设置壁纸
void forgetLoadedWallpaper() {
            synchronized (this) {
                mCachedWallpaper = null;
                mCachedWallpaperUserId = 0;
                mDefaultWallpaper = null;
            }
        }
  1. 调用WallpaperManager.getBitmap获取作为壁纸的位图,在这个方法里又会调用Globals.peekWallpaperBitmap方法。在其中有两套保证静态壁纸稳定运行的机制:缓存以及备用方案。它会尽量返回已经加载的位图,如果失败就会返回默认的位图。
  }
                        return mWallpaperManager.getBitmap();
                    } catch (RuntimeException | OutOfMemoryError e) {
                        exception = e;
                    }
 public Bitmap getBitmap() {
        return getBitmapAsUser(mContext.getUserId());
    }
 */
    public Bitmap getBitmapAsUser(int userId) {
        return sGlobals.peekWallpaperBitmap(mContext, true, FLAG_SYSTEM, userId);
    }
  1. 获取被设置为壁纸的位图使用的是Globals.getCurrentWallpaperLocked方法。这个方法会去前面WallpaperManager设置的文件位置加载位图并设置为壁纸。
public Bitmap peekWallpaperBitmap(Context context, boolean returnDefault,
                @SetWallpaperFlags int which, int userId) {
              //省略代码
                try {
                    mCachedWallpaper = getCurrentWallpaperLocked(context, userId);
                    mCachedWallpaperUserId = userId;
                } catch (OutOfMemoryError e) {

API29

静态壁纸的服务仍然位于SytemUI中,ImageWallpaper继承自WallpaperService,在其onCreateEngine方法中创建的Engine从DrawableEngine换成了GLEngine:

 class GLEngine extends Engine implements GLWallpaperRenderer.SurfaceProxy, StateListener

这个类继承了 Engine类,并且实现了GLWallpaperRenderer.SurfaceProxy,StateListener接口,GLWallpaperRenderer是一个渲染器,负责发送一个openGL调用来渲染一个帧,而SurfaceProxy拥有surfaceHolder的代理,通过这个代理能将openGL的渲染帧发送到serface上。

StateListener接口位于StatusBarStateController中,用于接受状态栏状态更新以及Dozing 的状态改变。

在GLEngine构造方法中调用了ImageWallpaperRenderer,ImageWallpaperRenderer继承自GLWallpaperRenderer,在它的构造方法中调用loadBitmap方法进行了Bitmap的加载。

  public ImageWallpaperRenderer(Context context, SurfaceProxy proxy) {
        mWallpaperManager = context.getSystemService(WallpaperManager.class);
     //。。省略代码
    if (loadBitmap()) {
        // Compute threshold of the image, this is an async work.
        mImageProcessHelper.start(mBitmap);
    }
}

首先就是调用了WallpaperManager的getBitmap方法去获取Bitmap。

 private boolean loadBitmap() {
        if (mWallpaperManager != null && mBitmap == null) {
         //省略代码
              mBitmap = mWallpaperManager.getBitmap();
         } 
          mWallpaperManager.forgetLoadedWallpaper();
          if (mBitmap != null) {
              mSurfaceSize.set(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
          }
        }
        return mBitmap != null;
    }

getBitmap中又调用了Wallpaper的静态内部类Globals的peekWallpaperBitmap方法,其同旧版本一样提供了两套保证尽态比值稳定运行的机制:缓存和备用方案。如果缓存为空的话就调用getCurrentWallpaperLocked方法来从文件中获取Bitmap。

  @UnsupportedAppUsage
    public Bitmap getBitmap(boolean hardware) {
        return getBitmapAsUpeekWallpaperBitmapser(mContext.getUserId(), hardware);
    }

public Bitmap getBitmapAsUser(int userId, boolean hardware) {
    return sGlobals.peekWallpaperBitmap(mContext, true, FLAG_SYSTEM, userId, hardware);
}

从文件中获取Bitmap的流程相对于老版本没有改变太多,只是不再直接在WallpaperManagerService.getWallpaper方法中调用getWallpaperDir来获取文件路径以及句柄了,而是将这个获取过程移到了WallpaperData的初始化中。而Wallpaper对象又会在loadSetttingLocked方法中被创建。

private WallpaperData getWallpaperSafeLocked(int userId, int which) {
        //..........
            loadSettingsLocked(userId, false);
            wallpaper = whichSet.get(userId);  
            //................
        return wallpaper;
    }

在loadSettingsLocked方法中做了这些事:

  • 从mWallpaperMap中尝试获取Wallpaper,如果为空开始WallpaperData的初始化(一般刚开机这个肯定为空)
  private void loadSettingsLocked(int userId, boolean keepDimensionHints) {
        JournaledFile journal = makeJournaledFile(userId);
        FileInputStream stream = null;
        File file = journal.chooseForRead();  
    WallpaperData wallpaper = mWallpaperMap.get(userId);
    if (wallpaper == null) {
        // Do this once per boot
        migrateFromOld();
  • 调用migrateFromOld方法对WALLPAPER_CROP,WALLPAPER进行赋值,这两个参数用来指定WallpaperData初始化时的inputFileName,cropFileName
 private void migrateFromOld() {
        // Pre-N, what existed is the one we're now using as the display crop
        File preNWallpaper = new File(getWallpaperDir(0), WALLPAPER_CROP);
        // In the very-long-ago, imagery lived with the settings app
        File originalWallpaper = new File(WallpaperBackupHelper.WALLPAPER_IMAGE_KEY);
        File newWallpaper = new File(getWallpaperDir(0), WALLPAPER);
  • 获取完wallpaper之后放入mWallpaperMap中,如果cropWallpaper不存在,就会调用generateCrop方法对原始的Wallpaper进行裁剪,如果连wallpaper文件都不存在就会采用调用默认壁纸的逻辑
 wallpaper = new WallpaperData(userId, WALLPAPER, WALLPAPER_CROP);
            wallpaper.allowBackup = true;
            mWallpaperMap.put(userId, wallpaper);
            if (!wallpaper.cropExists()) {
                if (wallpaper.sourceExists()) {
                    generateCrop(wallpaper);
                } else {
                    Slog.i(TAG, "No static wallpaper imagery; defaults will be shown");
                }
  • 获取完Wallpaper,且确认存在,就会读取xml文件中对壁纸的设定,设置wallpaper中的各种参数
   stream = new FileInputStream(file);
         XmlPullParser parser = Xml.newPullParser();
         parser.setInput(stream, StandardCharsets.UTF_8.name());
        int type;
        do {
            type = parser.next();
            if (type == XmlPullParser.START_TAG) {
                String tag = parser.getName();
                if ("wp".equals(tag)) {
                    // Common to system + lock wallpapers
                    parseWallpaperAttributes(parser, wallpaper, keepDimensionHints);

                    // A system wallpaper might also be a live wallpaper
                    String comp = parser.getAttributeValue(null, "component");
                    wallpaper.nextWallpaperComponent = comp != null
                            ? ComponentName.unflattenFromString(comp)
                            : null;
                    if (wallpaper.nextWallpaperComponent == null
                            || "android".equals(wallpaper.nextWallpaperComponent
                                    .getPackageName())) {
                        wallpaper.nextWallpaperComponent = mImageWallpaper;
                    }

接收到返回的文件句柄之后,就会在Globals类中使用BitmapFactory.decodeFileDescriptor进行解码生成Bitmap文件然后一路返回到ImageWallpaperRender中。然后就会调用forgetLoadedWallpaper方法删除掉所有上一张加载的壁纸的内部引用。调用这个方法对于那些只想短暂拥有壁纸,想要减少内存消耗的应用是有用的。但是在调用这个方法之后想要调用这张壁纸又要从硬盘中重新读取。

都结束之后就将Surface的大小调整为Bitmap的宽高。至此Bitmap的获取就结束了。

获取完Bitmap,之后的壁纸渲染工作GLEngine都会交给GLWallpaperRenderer的各种方法,由其发送OpenGL调用来渲染,并通过SurfaceProxy来对SurfaceHolder进行处理,来将壁纸渲染到Surface上。设置壁纸的过程和老版本基本一致