android4.4的文件管理器documentsui源码解析

frgopf2h 9年前

 

在4.4以上的版本中如果通过如下的Intent调用Activity:

final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);  // The MIME data type filter  intent.setType("*/*");  // Only return URIs that can be opened with ContentResolver  intent.addCategory(Intent.CATEGORY_OPENABLE);  startActivity(intent);

则会自动调用系统自带的documentsui文件管理器。因为documentsui的manifest中没有带category等于category.LAUNCHER的属性,因此documentsui是不会显示在LAUNCHER桌面上的。

documentsui的界面如下:

为了研究在android中资源文件是如何访问的的,我决定研究一下其代码。源代码地址

https://github.com/OWLeeMod/android_packages_apps_DocumentsUI


将代码下载下来之后,发现起代码结构非常复杂,这篇文章只是一个初步的分析。

入口:DocumentsActivity。DocumentsActivity的布局是由DrawerLayout构成的,左边是文件类别的菜单,右边是相应的目录树。

onCreate方法中:

// Hide roots when we're managing a specific root  if (mState.action == ACTION_MANAGE) {      if (mShowAsDialog) {          findViewById(R.id.dialog_roots).setVisibility(View.GONE);      } else {          mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);      }  }  if (mState.action == ACTION_CREATE) {      final String mimeType = getIntent().getType();      final String title = getIntent().getStringExtra(Intent.EXTRA_TITLE);      SaveFragment.show(getFragmentManager(), mimeType, title);  }  if (mState.action == ACTION_GET_CONTENT) {      final Intent moreApps = new Intent(getIntent());      moreApps.setComponent(null);      moreApps.setPackage(null);      RootsFragment.show(getFragmentManager(), moreApps);  } else if (mState.action == ACTION_OPEN || mState.action == ACTION_CREATE      || mState.action == ACTION_STANDALONE) {      RootsFragment.show(getFragmentManager(), null);  }  if (!mState.restored) {      if (mState.action == ACTION_MANAGE) {          final Uri rootUri = getIntent().getData();          if (rootUri != null) {              new RestoreRootTask(rootUri).executeOnExecutor(getCurrentExecutor());          } else {              new RestoreStackTask().execute();          }      } else {          new RestoreStackTask().execute();      }  } else {      onCurrentDirectoryChanged(ANIM_NONE);  }

其中有个判断条件是mState.action == ACTION_GET_CONTENT表示调用documentsui的人想获得的是文件系统的内容,也就是说本文开始那样的调用方式将会转到这个条件中来,执行

if (mState.action == ACTION_GET_CONTENT) {      final Intent moreApps = new Intent(getIntent());      moreApps.setComponent(null);      moreApps.setPackage(null);      RootsFragment.show(getFragmentManager(), moreApps);  }

中的代码。本文也主要是围绕documentsui的这一功能来分析的。除此之外从上面的代码还可以看出documentsui还有些其他的功能对应的是ACTION_CREATE,ACTION_MANAGE等,目前不清楚这些功能具体做什么。

回到mState.action == ACTION_GET_CONTENT这个条件中,他调用了RootsFragment中的show方法:

public static void show(FragmentManager fm, Intent includeApps) {      final Bundle args = new Bundle();      args.putParcelable(EXTRA_INCLUDE_APPS, includeApps);      final RootsFragment fragment = new RootsFragment();      fragment.setArguments(args);      final FragmentTransaction ft = fm.beginTransaction();      ft.replace(R.id.container_roots, fragment);      ft.commitAllowingStateLoss();  }

可以看出这是为了显示RootsFragment自己,其实RootsFragment就是DrawerLayout的菜单部分。

点击某个菜单之后(如图片)右边的目录树做相应的切换,那么这个过程是如何进行的呢?

RootsFragment中,菜单列表是一个ListView当然点击菜单就会进入到listvIew的OnItemClickListener中:

private OnItemClickListener mItemListener = new OnItemClickListener() {      @Override      public void onItemClick(AdapterView<?> parent, View view, int position, long id) {          final DocumentsActivity activity = DocumentsActivity.get(RootsFragment.this);          final Item item = mAdapter.getItem(position);          if (item instanceof RootItem) {              activity.onRootPicked(((RootItem) item).root, true);          } else if (item instanceof AppItem) {              activity.onAppPicked(((AppItem) item).info);          } else {              throw new IllegalStateException("Unknown root: " + item);          }      }  };

我们注意上面的界面图,可以看到左边菜单不仅仅有关于文件类型的,还有可以选择应用程序的。因此点击菜单之后上面的代码首先判断了你点击的RootItem类型,这里我们不去研究用户点击应用程序的情况。

当用户点击的是文件类型(如图片)菜单,则会调用DocumentsActivity 的onRootPicked:

public void onRootPicked(RootInfo root, boolean closeDrawer) {      // Clear entire backstack and start in new root      mState.stack.root = root;      mState.stack.clear();      mState.stackTouched = true;      if (!mRoots.isRecentsRoot(root)) {          new PickRootTask(root).executeOnExecutor(getCurrentExecutor());      } else {          onCurrentDirectoryChanged(ANIM_SIDE);      }      if (closeDrawer) {          setRootsDrawerOpen(false);      }  }


这里又有一个条件分支,判断了是否是“最近”菜单(isRecentsRoot,注意比照上面的界面图),如果是则执行new PickRootTask(root).executeOnExecutor(getCurrentExecutor());不是则调用onCurrentDirectoryChanged方法。因为分支过多我只研究最基本的,也就是列出目录文件的功能所以忽略“最近”菜单被点击的情况,进入到onCurrentDirectoryChanged中(其实反之最终也还是要进入到onCurrentDirectoryChanged中),很烦的是onCurrentDirectoryChanged中又有很多分支情况,但可以肯定的是列出目录文件的功能是调用

        DirectoryFragment.showNormal(fm, root, cwd, anim);

完成的。所以我的重点就在DirectoryFragment这个类中了。

我想了解在DirectoryFragment中是如何列出文件和目录 同时是如何显示缩略图的。


DirectoryFragment是使用loader机制来加载内容的。

getLoaderManager().restartLoader(mLoaderId, null, mCallbacks);

其中mCallbacks如下:

mCallbacks = new LoaderCallbacks<DirectoryResult>() {      @Override      public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) {          final String query = getArguments().getString(EXTRA_QUERY);          Uri contentsUri;          switch (mType) {              case TYPE_NORMAL:                  contentsUri = DocumentsContract.buildChildDocumentsUri(                          doc.authority, doc.documentId);                  if (state.action == ACTION_MANAGE) {                      contentsUri = DocumentsContract.setManageMode(contentsUri);                  }                  return new DirectoryLoader(                          context, mType, root, doc, contentsUri, state.userSortOrder);              case TYPE_SEARCH:                  contentsUri = DocumentsContract.buildSearchDocumentsUri(                          root.authority, root.rootId, query);                  if (state.action == ACTION_MANAGE) {                      contentsUri = DocumentsContract.setManageMode(contentsUri);                  }                  return new DirectoryLoader(                          context, mType, root, doc, contentsUri, state.userSortOrder);              case TYPE_RECENT_OPEN:                  final RootsCache roots = DocumentsApplication.getRootsCache(context);                  return new RecentLoader(context, roots, state);              default:                  throw new IllegalStateException("Unknown type " + mType);          }      }      @Override      public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {          if (!isAdded()) return;          mAdapter.swapResult(result);          // Push latest state up to UI          // TODO: if mode change was racing with us, don't overwrite it          if (result.mode != MODE_UNKNOWN) {              state.derivedMode = result.mode;          }          state.derivedSortOrder = result.sortOrder;          ((DocumentsActivity) context).onStateChanged();          updateDisplayState();          // When launched into empty recents, show drawer          if (mType == TYPE_RECENT_OPEN && mAdapter.isEmpty() && !state.stackTouched) {              ((DocumentsActivity) context).setRootsDrawerOpen(true);          }          // Restore any previous instance state          final SparseArray<Parcelable> container = state.dirState.remove(mStateKey);          if (container != null && !getArguments().getBoolean(EXTRA_IGNORE_STATE, false)) {              getView().restoreHierarchyState(container);          } else if (mLastSortOrder != state.derivedSortOrder) {              mListView.smoothScrollToPosition(0);              mGridView.smoothScrollToPosition(0);          }          mLastSortOrder = state.derivedSortOrder;      }      @Override      public void onLoaderReset(Loader<DirectoryResult> loader) {          mAdapter.swapResult(null);      }  };

其中很重要的是启用了DirectoryLoader类。DirectoryLoader完成了目录的加载。本文不对其深入讲解。当loader加载完成(onLoadFinished)会将结果result传递给mAdaptermAdapter.swapResult(result),这个result的类型是DirectoryResult。上面说了我想知道文件或者目录的缩略图是如何加载的,所以需要了解这个mAdapter的内部情况。

mAdapter的声明如下:

private DocumentsAdapter mAdapter;

DocumentsAdapter 是DirectoryFragment的一个内部类,我直接跳到他的getview方法:

@Override  public View getView(int position, View convertView, ViewGroup parent) {      if (position < mCursorCount) {          return getDocumentView(position, convertView, parent);      } else {          position -= mCursorCount;          convertView = mFooters.get(position).getView(convertView, parent);          // Only the view itself is disabled; contents inside shouldn't          // be dimmed.          convertView.setEnabled(false);          return convertView;      }  }


貌似没什么东西,继续跟着跳到getDocumentView,其中有这样的一段代码:

if (showThumbnail) {       final Uri uri = DocumentsContract.buildDocumentUri(docAuthority, docId);       final Bitmap cachedResult = thumbs.get(uri);       if (cachedResult != null) {           iconThumb.setImageBitmap(cachedResult);           cacheHit = true;       } else {           iconThumb.setImageDrawable(null);           final ThumbnailAsyncTask task = new ThumbnailAsyncTask(                   uri, iconMime, iconThumb, mThumbSize);           iconThumb.setTag(task);           ProviderExecutor.forAuthority(docAuthority).execute(task);       }   }

缩略图获得的过程是:首先从thumbs这个ThumbnailCache
中获取缓存图片,定义如下

final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache(                      context, mThumbSize);

如果缓存中没找到,则开启ThumbnailAsyncTask

然后再继续分析ThumbnailAsyncTask吧 总之这是一个很繁琐的过程。

本文只是理出一条分析的线索,希望对大家还是有所帮助。