DirectoryFragment.java revision 54ca29a5b94c2edf461c5433825d4ae17469fd7c
1/* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.documentsui; 18 19import android.app.Fragment; 20import android.app.FragmentManager; 21import android.app.FragmentTransaction; 22import android.app.LoaderManager.LoaderCallbacks; 23import android.content.Context; 24import android.content.Loader; 25import android.net.Uri; 26import android.os.Bundle; 27import android.provider.DocumentsContract; 28import android.text.format.DateUtils; 29import android.text.format.Formatter; 30import android.util.SparseBooleanArray; 31import android.view.ActionMode; 32import android.view.LayoutInflater; 33import android.view.Menu; 34import android.view.MenuItem; 35import android.view.View; 36import android.view.ViewGroup; 37import android.widget.AbsListView; 38import android.widget.AbsListView.MultiChoiceModeListener; 39import android.widget.AdapterView; 40import android.widget.AdapterView.OnItemClickListener; 41import android.widget.BaseAdapter; 42import android.widget.GridView; 43import android.widget.ImageView; 44import android.widget.ListView; 45import android.widget.TextView; 46 47import com.android.documentsui.DocumentsActivity.DisplayState; 48import com.android.documentsui.model.Document; 49import com.android.documentsui.model.Root; 50import com.android.internal.util.Predicate; 51import com.google.android.collect.Lists; 52 53import java.text.DateFormat; 54import java.util.ArrayList; 55import java.util.Comparator; 56import java.util.List; 57import java.util.concurrent.atomic.AtomicInteger; 58 59/** 60 * Display the documents inside a single directory. 61 */ 62public class DirectoryFragment extends Fragment { 63 64 private View mEmptyView; 65 private ListView mListView; 66 private GridView mGridView; 67 68 private AbsListView mCurrentView; 69 70 private Predicate<Document> mFilter; 71 72 public static final int TYPE_NORMAL = 1; 73 public static final int TYPE_SEARCH = 2; 74 public static final int TYPE_RECENT_OPEN = 3; 75 76 private int mType = TYPE_NORMAL; 77 78 private DocumentsAdapter mAdapter; 79 private LoaderCallbacks<List<Document>> mCallbacks; 80 81 private static final String EXTRA_TYPE = "type"; 82 private static final String EXTRA_URI = "uri"; 83 84 private static AtomicInteger sLoaderId = new AtomicInteger(4000); 85 86 private final int mLoaderId = sLoaderId.incrementAndGet(); 87 88 public static void showNormal(FragmentManager fm, Uri uri) { 89 show(fm, TYPE_NORMAL, uri); 90 } 91 92 public static void showSearch(FragmentManager fm, Uri uri, String query) { 93 final Uri searchUri = DocumentsContract.buildSearchUri(uri, query); 94 show(fm, TYPE_SEARCH, searchUri); 95 } 96 97 public static void showRecentsOpen(FragmentManager fm) { 98 show(fm, TYPE_RECENT_OPEN, null); 99 } 100 101 private static void show(FragmentManager fm, int type, Uri uri) { 102 final Bundle args = new Bundle(); 103 args.putInt(EXTRA_TYPE, type); 104 args.putParcelable(EXTRA_URI, uri); 105 106 final DirectoryFragment fragment = new DirectoryFragment(); 107 fragment.setArguments(args); 108 109 final FragmentTransaction ft = fm.beginTransaction(); 110 ft.replace(R.id.container_directory, fragment); 111 ft.commitAllowingStateLoss(); 112 } 113 114 public static DirectoryFragment get(FragmentManager fm) { 115 // TODO: deal with multiple directories shown at once 116 return (DirectoryFragment) fm.findFragmentById(R.id.container_directory); 117 } 118 119 @Override 120 public View onCreateView( 121 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 122 final Context context = inflater.getContext(); 123 124 final View view = inflater.inflate(R.layout.fragment_directory, container, false); 125 126 mEmptyView = view.findViewById(android.R.id.empty); 127 128 mListView = (ListView) view.findViewById(R.id.list); 129 mListView.setOnItemClickListener(mItemListener); 130 mListView.setMultiChoiceModeListener(mMultiListener); 131 132 mGridView = (GridView) view.findViewById(R.id.grid); 133 mGridView.setOnItemClickListener(mItemListener); 134 mGridView.setMultiChoiceModeListener(mMultiListener); 135 136 mAdapter = new DocumentsAdapter(); 137 138 final Uri uri = getArguments().getParcelable(EXTRA_URI); 139 mType = getArguments().getInt(EXTRA_TYPE); 140 141 mCallbacks = new LoaderCallbacks<List<Document>>() { 142 @Override 143 public Loader<List<Document>> onCreateLoader(int id, Bundle args) { 144 final DisplayState state = getDisplayState(DirectoryFragment.this); 145 mFilter = new MimePredicate(state.acceptMimes); 146 147 Uri contentsUri; 148 if (mType == TYPE_NORMAL) { 149 contentsUri = DocumentsContract.buildContentsUri(uri); 150 } else if (mType == TYPE_RECENT_OPEN) { 151 contentsUri = RecentsProvider.buildRecentOpen(); 152 } else { 153 contentsUri = uri; 154 } 155 156 if (state.localOnly) { 157 contentsUri = DocumentsContract.setLocalOnly(contentsUri); 158 } 159 160 final Comparator<Document> sortOrder; 161 if (state.sortOrder == DisplayState.SORT_ORDER_DATE || mType == TYPE_RECENT_OPEN) { 162 sortOrder = new Document.DateComparator(); 163 } else if (state.sortOrder == DisplayState.SORT_ORDER_NAME) { 164 sortOrder = new Document.NameComparator(); 165 } else if (state.sortOrder == DisplayState.SORT_ORDER_SIZE) { 166 sortOrder = new Document.SizeComparator(); 167 } else { 168 throw new IllegalArgumentException("Unknown sort order " + state.sortOrder); 169 } 170 171 return new DirectoryLoader(context, contentsUri, mType, null, sortOrder); 172 } 173 174 @Override 175 public void onLoadFinished(Loader<List<Document>> loader, List<Document> data) { 176 mAdapter.swapDocuments(data); 177 } 178 179 @Override 180 public void onLoaderReset(Loader<List<Document>> loader) { 181 mAdapter.swapDocuments(null); 182 } 183 }; 184 185 updateDisplayState(); 186 187 return view; 188 } 189 190 @Override 191 public void onStart() { 192 super.onStart(); 193 194 final Context context = getActivity(); 195 getDisplayState(this).showSize = SettingsActivity.getDisplayFileSize(context); 196 197 getLoaderManager().restartLoader(mLoaderId, getArguments(), mCallbacks); 198 } 199 200 @Override 201 public void onStop() { 202 super.onStop(); 203 getLoaderManager().destroyLoader(mLoaderId); 204 } 205 206 public void updateDisplayState() { 207 final DisplayState state = getDisplayState(this); 208 209 // TODO: avoid kicking loader when nothing changed 210 getLoaderManager().restartLoader(mLoaderId, getArguments(), mCallbacks); 211 mListView.smoothScrollToPosition(0); 212 mGridView.smoothScrollToPosition(0); 213 214 mListView.setVisibility(state.mode == DisplayState.MODE_LIST ? View.VISIBLE : View.GONE); 215 mGridView.setVisibility(state.mode == DisplayState.MODE_GRID ? View.VISIBLE : View.GONE); 216 217 final int choiceMode; 218 if (state.allowMultiple) { 219 choiceMode = ListView.CHOICE_MODE_MULTIPLE_MODAL; 220 } else { 221 choiceMode = ListView.CHOICE_MODE_NONE; 222 } 223 224 if (state.mode == DisplayState.MODE_GRID) { 225 mListView.setAdapter(null); 226 mListView.setChoiceMode(ListView.CHOICE_MODE_NONE); 227 mGridView.setAdapter(mAdapter); 228 mGridView.setColumnWidth(getResources().getDimensionPixelSize(R.dimen.grid_width)); 229 mGridView.setNumColumns(GridView.AUTO_FIT); 230 mGridView.setChoiceMode(choiceMode); 231 mCurrentView = mGridView; 232 } else if (state.mode == DisplayState.MODE_LIST) { 233 mGridView.setAdapter(null); 234 mGridView.setChoiceMode(ListView.CHOICE_MODE_NONE); 235 mListView.setAdapter(mAdapter); 236 mListView.setChoiceMode(choiceMode); 237 mCurrentView = mListView; 238 } else { 239 throw new IllegalStateException(); 240 } 241 } 242 243 private OnItemClickListener mItemListener = new OnItemClickListener() { 244 @Override 245 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 246 final Document doc = mAdapter.getItem(position); 247 ((DocumentsActivity) getActivity()).onDocumentPicked(doc); 248 } 249 }; 250 251 private MultiChoiceModeListener mMultiListener = new MultiChoiceModeListener() { 252 @Override 253 public boolean onCreateActionMode(ActionMode mode, Menu menu) { 254 mode.getMenuInflater().inflate(R.menu.mode_directory, menu); 255 return true; 256 } 257 258 @Override 259 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 260 return true; 261 } 262 263 @Override 264 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 265 if (item.getItemId() == R.id.menu_open) { 266 final Uri uri = getArguments().getParcelable(EXTRA_URI); 267 final SparseBooleanArray checked = mCurrentView.getCheckedItemPositions(); 268 final ArrayList<Document> docs = Lists.newArrayList(); 269 270 final int size = checked.size(); 271 for (int i = 0; i < size; i++) { 272 if (checked.valueAt(i)) { 273 final Document doc = mAdapter.getItem(checked.keyAt(i)); 274 docs.add(doc); 275 } 276 } 277 278 ((DocumentsActivity) getActivity()).onDocumentsPicked(docs); 279 return true; 280 } else { 281 return false; 282 } 283 } 284 285 @Override 286 public void onDestroyActionMode(ActionMode mode) { 287 // ignored 288 } 289 290 @Override 291 public void onItemCheckedStateChanged( 292 ActionMode mode, int position, long id, boolean checked) { 293 if (checked) { 294 // Directories cannot be checked 295 final Document doc = mAdapter.getItem(position); 296 if (doc.isDirectory()) { 297 mCurrentView.setItemChecked(position, false); 298 } 299 } 300 301 mode.setTitle(getResources() 302 .getString(R.string.mode_selected_count, mCurrentView.getCheckedItemCount())); 303 } 304 }; 305 306 private static DisplayState getDisplayState(Fragment fragment) { 307 return ((DocumentsActivity) fragment.getActivity()).getDisplayState(); 308 } 309 310 private class DocumentsAdapter extends BaseAdapter { 311 private List<Document> mDocuments; 312 313 public DocumentsAdapter() { 314 } 315 316 public void swapDocuments(List<Document> documents) { 317 mDocuments = documents; 318 319 if (documents != null && documents.isEmpty()) { 320 mEmptyView.setVisibility(View.VISIBLE); 321 } else { 322 mEmptyView.setVisibility(View.GONE); 323 } 324 325 notifyDataSetChanged(); 326 } 327 328 @Override 329 public View getView(int position, View convertView, ViewGroup parent) { 330 final Context context = parent.getContext(); 331 final DisplayState state = getDisplayState(DirectoryFragment.this); 332 333 if (convertView == null) { 334 final LayoutInflater inflater = LayoutInflater.from(context); 335 if (state.mode == DisplayState.MODE_LIST) { 336 convertView = inflater.inflate(R.layout.item_doc_list, parent, false); 337 } else if (state.mode == DisplayState.MODE_GRID) { 338 convertView = inflater.inflate(R.layout.item_doc_grid, parent, false); 339 } else { 340 throw new IllegalStateException(); 341 } 342 } 343 344 final Document doc = getItem(position); 345 346 final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon); 347 final TextView title = (TextView) convertView.findViewById(android.R.id.title); 348 final View summaryGrid = convertView.findViewById(R.id.summary_grid); 349 final ImageView icon1 = (ImageView) convertView.findViewById(android.R.id.icon1); 350 final TextView summary = (TextView) convertView.findViewById(android.R.id.summary); 351 final TextView date = (TextView) convertView.findViewById(R.id.date); 352 final TextView size = (TextView) convertView.findViewById(R.id.size); 353 354 if (doc.isThumbnailSupported()) { 355 // TODO: load thumbnails async 356 icon.setImageURI(doc.uri); 357 } else { 358 icon.setImageDrawable(RootsCache.resolveDocumentIcon( 359 context, doc.uri.getAuthority(), doc.mimeType)); 360 } 361 362 title.setText(doc.displayName); 363 364 if (mType == TYPE_NORMAL || mType == TYPE_SEARCH) { 365 icon1.setVisibility(View.GONE); 366 if (doc.summary != null) { 367 summary.setText(doc.summary); 368 summary.setVisibility(View.VISIBLE); 369 } else { 370 summary.setVisibility(View.INVISIBLE); 371 } 372 } else if (mType == TYPE_RECENT_OPEN) { 373 final Root root = RootsCache.findRoot(context, doc); 374 icon1.setVisibility(View.VISIBLE); 375 icon1.setImageDrawable(root.icon); 376 summary.setText(root.getDirectoryString()); 377 summary.setVisibility(View.VISIBLE); 378 } 379 380 if (summaryGrid != null) { 381 summaryGrid.setVisibility( 382 (summary.getVisibility() == View.VISIBLE) ? View.VISIBLE : View.GONE); 383 } 384 385 // TODO: omit year from format 386 date.setText(DateUtils.formatSameDayTime( 387 doc.lastModified, System.currentTimeMillis(), DateFormat.SHORT, 388 DateFormat.SHORT)); 389 390 if (state.showSize) { 391 size.setVisibility(View.VISIBLE); 392 if (doc.isDirectory()) { 393 size.setText(null); 394 } else { 395 size.setText(Formatter.formatFileSize(context, doc.size)); 396 } 397 } else { 398 size.setVisibility(View.GONE); 399 } 400 401 return convertView; 402 } 403 404 @Override 405 public int getCount() { 406 return mDocuments != null ? mDocuments.size() : 0; 407 } 408 409 @Override 410 public Document getItem(int position) { 411 return mDocuments.get(position); 412 } 413 414 @Override 415 public long getItemId(int position) { 416 return getItem(position).uri.hashCode(); 417 } 418 419 @Override 420 public boolean areAllItemsEnabled() { 421 return false; 422 } 423 424 @Override 425 public boolean isEnabled(int position) { 426 final Document doc = getItem(position); 427 return mFilter.apply(doc); 428 } 429 } 430} 431