DirectoryFragment.java revision 9fb567b59112f99e64e0bff6f343188331bad28d
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                final 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                final Comparator<Document> sortOrder;
157                if (state.sortOrder == DisplayState.SORT_ORDER_DATE || mType == TYPE_RECENT_OPEN) {
158                    sortOrder = new Document.DateComparator();
159                } else if (state.sortOrder == DisplayState.SORT_ORDER_NAME) {
160                    sortOrder = new Document.NameComparator();
161                } else if (state.sortOrder == DisplayState.SORT_ORDER_SIZE) {
162                    sortOrder = new Document.SizeComparator();
163                } else {
164                    throw new IllegalArgumentException("Unknown sort order " + state.sortOrder);
165                }
166
167                return new DirectoryLoader(context, contentsUri, mType, null, sortOrder);
168            }
169
170            @Override
171            public void onLoadFinished(Loader<List<Document>> loader, List<Document> data) {
172                mAdapter.swapDocuments(data);
173            }
174
175            @Override
176            public void onLoaderReset(Loader<List<Document>> loader) {
177                mAdapter.swapDocuments(null);
178            }
179        };
180
181        updateDisplayState();
182
183        return view;
184    }
185
186    @Override
187    public void onStart() {
188        super.onStart();
189
190        final Context context = getActivity();
191        getDisplayState(this).showSize = SettingsActivity.getDisplayFileSize(context);
192
193        getLoaderManager().restartLoader(mLoaderId, getArguments(), mCallbacks);
194    }
195
196    @Override
197    public void onStop() {
198        super.onStop();
199        getLoaderManager().destroyLoader(mLoaderId);
200    }
201
202    public void updateDisplayState() {
203        final DisplayState state = getDisplayState(this);
204
205        // TODO: avoid kicking loader when nothing changed
206        getLoaderManager().restartLoader(mLoaderId, getArguments(), mCallbacks);
207        mListView.smoothScrollToPosition(0);
208        mGridView.smoothScrollToPosition(0);
209
210        mListView.setVisibility(state.mode == DisplayState.MODE_LIST ? View.VISIBLE : View.GONE);
211        mGridView.setVisibility(state.mode == DisplayState.MODE_GRID ? View.VISIBLE : View.GONE);
212
213        final int choiceMode;
214        if (state.allowMultiple) {
215            choiceMode = ListView.CHOICE_MODE_MULTIPLE_MODAL;
216        } else {
217            choiceMode = ListView.CHOICE_MODE_NONE;
218        }
219
220        if (state.mode == DisplayState.MODE_GRID) {
221            mListView.setAdapter(null);
222            mListView.setChoiceMode(ListView.CHOICE_MODE_NONE);
223            mGridView.setAdapter(mAdapter);
224            mGridView.setColumnWidth(getResources().getDimensionPixelSize(R.dimen.grid_width));
225            mGridView.setNumColumns(GridView.AUTO_FIT);
226            mGridView.setChoiceMode(choiceMode);
227            mCurrentView = mGridView;
228        } else if (state.mode == DisplayState.MODE_LIST) {
229            mGridView.setAdapter(null);
230            mGridView.setChoiceMode(ListView.CHOICE_MODE_NONE);
231            mListView.setAdapter(mAdapter);
232            mListView.setChoiceMode(choiceMode);
233            mCurrentView = mListView;
234        } else {
235            throw new IllegalStateException();
236        }
237    }
238
239    private OnItemClickListener mItemListener = new OnItemClickListener() {
240        @Override
241        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
242            final Document doc = mAdapter.getItem(position);
243            ((DocumentsActivity) getActivity()).onDocumentPicked(doc);
244        }
245    };
246
247    private MultiChoiceModeListener mMultiListener = new MultiChoiceModeListener() {
248        @Override
249        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
250            mode.getMenuInflater().inflate(R.menu.mode_directory, menu);
251            return true;
252        }
253
254        @Override
255        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
256            return true;
257        }
258
259        @Override
260        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
261            if (item.getItemId() == R.id.menu_open) {
262                final Uri uri = getArguments().getParcelable(EXTRA_URI);
263                final SparseBooleanArray checked = mCurrentView.getCheckedItemPositions();
264                final ArrayList<Document> docs = Lists.newArrayList();
265
266                final int size = checked.size();
267                for (int i = 0; i < size; i++) {
268                    if (checked.valueAt(i)) {
269                        final Document doc = mAdapter.getItem(checked.keyAt(i));
270                        docs.add(doc);
271                    }
272                }
273
274                ((DocumentsActivity) getActivity()).onDocumentsPicked(docs);
275                return true;
276            } else {
277                return false;
278            }
279        }
280
281        @Override
282        public void onDestroyActionMode(ActionMode mode) {
283            // ignored
284        }
285
286        @Override
287        public void onItemCheckedStateChanged(
288                ActionMode mode, int position, long id, boolean checked) {
289            if (checked) {
290                // Directories cannot be checked
291                final Document doc = mAdapter.getItem(position);
292                if (doc.isDirectory()) {
293                    mCurrentView.setItemChecked(position, false);
294                }
295            }
296
297            mode.setTitle(getResources()
298                    .getString(R.string.mode_selected_count, mCurrentView.getCheckedItemCount()));
299        }
300    };
301
302    private static DisplayState getDisplayState(Fragment fragment) {
303        return ((DocumentsActivity) fragment.getActivity()).getDisplayState();
304    }
305
306    private class DocumentsAdapter extends BaseAdapter {
307        private List<Document> mDocuments;
308
309        public DocumentsAdapter() {
310        }
311
312        public void swapDocuments(List<Document> documents) {
313            mDocuments = documents;
314
315            if (documents != null && documents.isEmpty()) {
316                mEmptyView.setVisibility(View.VISIBLE);
317            } else {
318                mEmptyView.setVisibility(View.GONE);
319            }
320
321            notifyDataSetChanged();
322        }
323
324        @Override
325        public View getView(int position, View convertView, ViewGroup parent) {
326            final Context context = parent.getContext();
327            final DisplayState state = getDisplayState(DirectoryFragment.this);
328
329            if (convertView == null) {
330                final LayoutInflater inflater = LayoutInflater.from(context);
331                if (state.mode == DisplayState.MODE_LIST) {
332                    convertView = inflater.inflate(R.layout.item_doc_list, parent, false);
333                } else if (state.mode == DisplayState.MODE_GRID) {
334                    convertView = inflater.inflate(R.layout.item_doc_grid, parent, false);
335                } else {
336                    throw new IllegalStateException();
337                }
338            }
339
340            final Document doc = getItem(position);
341
342            final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon);
343            final TextView title = (TextView) convertView.findViewById(android.R.id.title);
344            final View summaryGrid = convertView.findViewById(R.id.summary_grid);
345            final ImageView icon1 = (ImageView) convertView.findViewById(android.R.id.icon1);
346            final TextView summary = (TextView) convertView.findViewById(android.R.id.summary);
347            final TextView date = (TextView) convertView.findViewById(R.id.date);
348            final TextView size = (TextView) convertView.findViewById(R.id.size);
349
350            if (doc.isThumbnailSupported()) {
351                // TODO: load thumbnails async
352                icon.setImageURI(doc.uri);
353            } else {
354                icon.setImageDrawable(RootsCache.resolveDocumentIcon(
355                        context, doc.uri.getAuthority(), doc.mimeType));
356            }
357
358            title.setText(doc.displayName);
359
360            if (mType == TYPE_NORMAL || mType == TYPE_SEARCH) {
361                icon1.setVisibility(View.GONE);
362                if (doc.summary != null) {
363                    summary.setText(doc.summary);
364                    summary.setVisibility(View.VISIBLE);
365                } else {
366                    summary.setVisibility(View.INVISIBLE);
367                }
368            } else if (mType == TYPE_RECENT_OPEN) {
369                final Root root = RootsCache.findRoot(context, doc);
370                icon1.setVisibility(View.VISIBLE);
371                icon1.setImageDrawable(root.icon);
372                summary.setText(root.getDirectoryString());
373                summary.setVisibility(View.VISIBLE);
374            }
375
376            if (summaryGrid != null) {
377                summaryGrid.setVisibility(
378                        (summary.getVisibility() == View.VISIBLE) ? View.VISIBLE : View.GONE);
379            }
380
381            // TODO: omit year from format
382            date.setText(DateUtils.formatSameDayTime(
383                    doc.lastModified, System.currentTimeMillis(), DateFormat.SHORT,
384                    DateFormat.SHORT));
385
386            if (state.showSize) {
387                size.setVisibility(View.VISIBLE);
388                if (doc.isDirectory()) {
389                    size.setText(null);
390                } else {
391                    size.setText(Formatter.formatFileSize(context, doc.size));
392                }
393            } else {
394                size.setVisibility(View.GONE);
395            }
396
397            return convertView;
398        }
399
400        @Override
401        public int getCount() {
402            return mDocuments != null ? mDocuments.size() : 0;
403        }
404
405        @Override
406        public Document getItem(int position) {
407            return mDocuments.get(position);
408        }
409
410        @Override
411        public long getItemId(int position) {
412            return getItem(position).uri.hashCode();
413        }
414
415        @Override
416        public boolean areAllItemsEnabled() {
417            return false;
418        }
419
420        @Override
421        public boolean isEnabled(int position) {
422            final Document doc = getItem(position);
423            return mFilter.apply(doc);
424        }
425    }
426}
427