DirectoryFragment.java revision 92d7e697a864a3e18bef4ef256bb3eb339a66b4e
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.CursorLoader;
25import android.content.Loader;
26import android.database.Cursor;
27import android.net.Uri;
28import android.os.Bundle;
29import android.provider.DocumentsContract;
30import android.provider.DocumentsContract.DocumentColumns;
31import android.text.format.DateUtils;
32import android.util.SparseBooleanArray;
33import android.view.ActionMode;
34import android.view.LayoutInflater;
35import android.view.Menu;
36import android.view.MenuInflater;
37import android.view.MenuItem;
38import android.view.View;
39import android.view.ViewGroup;
40import android.widget.AbsListView;
41import android.widget.AbsListView.MultiChoiceModeListener;
42import android.widget.AdapterView;
43import android.widget.AdapterView.OnItemClickListener;
44import android.widget.CursorAdapter;
45import android.widget.GridView;
46import android.widget.ImageView;
47import android.widget.ListView;
48import android.widget.TextView;
49
50import com.android.documentsui.DocumentsActivity.DisplayState;
51import com.android.documentsui.DocumentsActivity.Document;
52import com.google.android.collect.Lists;
53
54import java.util.ArrayList;
55
56/**
57 * Display the documents inside a single directory.
58 */
59public class DirectoryFragment extends Fragment {
60
61    // TODO: show storage backend in item views when requested
62    // TODO: apply sort order locally
63    // TODO: apply MIME filtering locally
64
65    private ListView mListView;
66    private GridView mGridView;
67
68    private AbsListView mCurrentView;
69
70    private DocumentsAdapter mAdapter;
71    private LoaderCallbacks<Cursor> mCallbacks;
72
73    private int mFlags;
74
75    private static final String EXTRA_ROOT_URI = "rootUri";
76    private static final String EXTRA_DOCS_URI = "docsUri";
77
78    private static final int LOADER_DOCUMENTS = 2;
79
80    public static void show(FragmentManager fm, Uri rootUri, Uri docsUri, String displayName,
81            boolean addToBackStack) {
82        final Bundle args = new Bundle();
83        args.putParcelable(EXTRA_ROOT_URI, rootUri);
84        args.putParcelable(EXTRA_DOCS_URI, docsUri);
85
86        final DirectoryFragment fragment = new DirectoryFragment();
87        fragment.setArguments(args);
88
89        final FragmentTransaction ft = fm.beginTransaction();
90        ft.replace(R.id.directory, fragment);
91        if (addToBackStack) {
92            ft.addToBackStack(displayName);
93        }
94        ft.setBreadCrumbTitle(displayName);
95        ft.commitAllowingStateLoss();
96    }
97
98    @Override
99    public void onCreate(Bundle savedInstanceState) {
100        super.onCreate(savedInstanceState);
101        setHasOptionsMenu(true);
102    }
103
104    @Override
105    public View onCreateView(
106            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
107        final Context context = inflater.getContext();
108
109        final View view = inflater.inflate(R.layout.fragment_directory, container, false);
110
111        mListView = (ListView) view.findViewById(R.id.list);
112        mListView.setOnItemClickListener(mItemListener);
113        mListView.setMultiChoiceModeListener(mMultiListener);
114
115        mGridView = (GridView) view.findViewById(R.id.grid);
116        mGridView.setOnItemClickListener(mItemListener);
117        mGridView.setMultiChoiceModeListener(mMultiListener);
118
119        mAdapter = new DocumentsAdapter(context);
120        updateMode();
121
122        // TODO: migrate flags query to loader
123        final Uri docsUri = getArguments().getParcelable(EXTRA_DOCS_URI);
124        mFlags = getDocumentFlags(context, docsUri);
125
126        mCallbacks = new LoaderCallbacks<Cursor>() {
127            @Override
128            public Loader<Cursor> onCreateLoader(int id, Bundle args) {
129                final DisplayState state = getDisplayState(DirectoryFragment.this);
130                final String sortOrder;
131                if (state.sortBy == DisplayState.SORT_BY_NAME) {
132                    sortOrder = DocumentColumns.DISPLAY_NAME + " ASC";
133                } else if (state.sortBy == DisplayState.SORT_BY_DATE) {
134                    sortOrder = DocumentColumns.LAST_MODIFIED + " DESC";
135                } else {
136                    sortOrder = null;
137                }
138
139                final Uri contentsUri;
140                if (docsUri.getQueryParameter(DocumentsContract.PARAM_QUERY) != null) {
141                    contentsUri = docsUri;
142                } else {
143                    contentsUri = DocumentsContract.buildContentsUri(docsUri);
144                }
145
146                return new CursorLoader(context, contentsUri, null, null, null, sortOrder);
147            }
148
149            @Override
150            public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
151                mAdapter.swapCursor(data);
152            }
153
154            @Override
155            public void onLoaderReset(Loader<Cursor> loader) {
156                mAdapter.swapCursor(null);
157            }
158        };
159
160        return view;
161    }
162
163    @Override
164    public void onStart() {
165        super.onStart();
166        getLoaderManager().restartLoader(LOADER_DOCUMENTS, getArguments(), mCallbacks);
167
168        // TODO: clean up tracking of current directory
169        final Uri docsUri = getArguments().getParcelable(EXTRA_DOCS_URI);
170        ((DocumentsActivity) getActivity()).onDirectoryChanged(docsUri, mFlags);
171    }
172
173    @Override
174    public void onStop() {
175        super.onStop();
176        getLoaderManager().destroyLoader(LOADER_DOCUMENTS);
177    }
178
179    @Override
180    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
181        super.onCreateOptionsMenu(menu, inflater);
182        inflater.inflate(R.menu.directory, menu);
183    }
184
185    @Override
186    public void onPrepareOptionsMenu(Menu menu) {
187        super.onPrepareOptionsMenu(menu);
188        final DisplayState state = getDisplayState(this);
189        menu.findItem(R.id.menu_grid).setVisible(state.mode != DisplayState.MODE_GRID);
190        menu.findItem(R.id.menu_list).setVisible(state.mode != DisplayState.MODE_LIST);
191    }
192
193    @Override
194    public boolean onOptionsItemSelected(MenuItem item) {
195        final DisplayState state = getDisplayState(this);
196        final int id = item.getItemId();
197        if (id == R.id.menu_grid) {
198            state.mode = DisplayState.MODE_GRID;
199            updateMode();
200            getFragmentManager().invalidateOptionsMenu();
201            return true;
202        } else if (id == R.id.menu_list) {
203            state.mode = DisplayState.MODE_LIST;
204            updateMode();
205            getFragmentManager().invalidateOptionsMenu();
206            return true;
207        } else {
208            return super.onOptionsItemSelected(item);
209        }
210    }
211
212    private void updateMode() {
213        final DisplayState state = getDisplayState(this);
214
215        mListView.setVisibility(state.mode == DisplayState.MODE_LIST ? View.VISIBLE : View.GONE);
216        mGridView.setVisibility(state.mode == DisplayState.MODE_GRID ? View.VISIBLE : View.GONE);
217
218        final int choiceMode;
219        if (state.allowMultiple) {
220            choiceMode = ListView.CHOICE_MODE_MULTIPLE_MODAL;
221        } else {
222            choiceMode = ListView.CHOICE_MODE_NONE;
223        }
224
225        if (state.mode == DisplayState.MODE_GRID) {
226            mListView.setAdapter(null);
227            mListView.setChoiceMode(ListView.CHOICE_MODE_NONE);
228            mGridView.setAdapter(mAdapter);
229            mGridView.setColumnWidth(getResources().getDimensionPixelSize(R.dimen.grid_width));
230            mGridView.setNumColumns(GridView.AUTO_FIT);
231            mGridView.setChoiceMode(choiceMode);
232            mCurrentView = mGridView;
233        } else if (state.mode == DisplayState.MODE_LIST) {
234            mGridView.setAdapter(null);
235            mGridView.setChoiceMode(ListView.CHOICE_MODE_NONE);
236            mListView.setAdapter(mAdapter);
237            mListView.setChoiceMode(choiceMode);
238            mCurrentView = mListView;
239        } else {
240            throw new IllegalStateException();
241        }
242    }
243
244    public void updateSortBy() {
245        getLoaderManager().restartLoader(LOADER_DOCUMENTS, getArguments(), mCallbacks);
246    }
247
248    private OnItemClickListener mItemListener = new OnItemClickListener() {
249        @Override
250        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
251            final Cursor cursor = (Cursor) mAdapter.getItem(position);
252            final Uri rootUri = getArguments().getParcelable(EXTRA_ROOT_URI);
253            final Document doc = Document.fromCursor(rootUri, cursor);
254            ((DocumentsActivity) getActivity()).onDocumentPicked(doc);
255        }
256    };
257
258    private MultiChoiceModeListener mMultiListener = new MultiChoiceModeListener() {
259        @Override
260        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
261            mode.getMenuInflater().inflate(R.menu.mode_directory, menu);
262            return true;
263        }
264
265        @Override
266        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
267            return true;
268        }
269
270        @Override
271        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
272            if (item.getItemId() == R.id.menu_open) {
273                final Uri rootUri = getArguments().getParcelable(EXTRA_ROOT_URI);
274                final SparseBooleanArray checked = mCurrentView.getCheckedItemPositions();
275                final ArrayList<Document> docs = Lists.newArrayList();
276
277                final int size = checked.size();
278                for (int i = 0; i < size; i++) {
279                    if (checked.valueAt(i)) {
280                        final Cursor cursor = (Cursor) mAdapter.getItem(checked.keyAt(i));
281                        docs.add(Document.fromCursor(rootUri, cursor));
282                    }
283                }
284
285                ((DocumentsActivity) getActivity()).onDocumentsPicked(docs);
286                return true;
287            } else {
288                return false;
289            }
290        }
291
292        @Override
293        public void onDestroyActionMode(ActionMode mode) {
294            // ignored
295        }
296
297        @Override
298        public void onItemCheckedStateChanged(
299                ActionMode mode, int position, long id, boolean checked) {
300            if (checked) {
301                final Cursor cursor = (Cursor) mAdapter.getItem(position);
302                final String mimeType = getCursorString(cursor, DocumentColumns.MIME_TYPE);
303
304                // Directories cannot be checked
305                if (DocumentsContract.MIME_TYPE_DIRECTORY.equals(mimeType)) {
306                    mCurrentView.setItemChecked(position, false);
307                }
308            }
309
310            mode.setTitle(getResources()
311                    .getString(R.string.mode_selected_count, mCurrentView.getCheckedItemCount()));
312        }
313    };
314
315    private static DisplayState getDisplayState(Fragment fragment) {
316        return ((DocumentsActivity) fragment.getActivity()).getDisplayState();
317    }
318
319    private class DocumentsAdapter extends CursorAdapter {
320        public DocumentsAdapter(Context context) {
321            super(context, null, false);
322        }
323
324        @Override
325        public View newView(Context context, Cursor cursor, ViewGroup parent) {
326            final LayoutInflater inflater = LayoutInflater.from(context);
327            final DisplayState state = getDisplayState(DirectoryFragment.this);
328            if (state.mode == DisplayState.MODE_LIST) {
329                return inflater.inflate(R.layout.item_doc_list, parent, false);
330            } else if (state.mode == DisplayState.MODE_GRID) {
331                return inflater.inflate(R.layout.item_doc_grid, parent, false);
332            } else {
333                throw new IllegalStateException();
334            }
335        }
336
337        @Override
338        public void bindView(View view, Context context, Cursor cursor) {
339            final TextView title = (TextView) view.findViewById(android.R.id.title);
340            final TextView summary = (TextView) view.findViewById(android.R.id.summary);
341            final ImageView icon = (ImageView) view.findViewById(android.R.id.icon);
342
343            final String docId = getCursorString(cursor, DocumentColumns.DOC_ID);
344            final String displayName = getCursorString(cursor, DocumentColumns.DISPLAY_NAME);
345            final String mimeType = getCursorString(cursor, DocumentColumns.MIME_TYPE);
346            final long lastModified = getCursorLong(cursor, DocumentColumns.LAST_MODIFIED);
347            final int flags = getCursorInt(cursor, DocumentColumns.FLAGS);
348
349            final Uri rootUri = getArguments().getParcelable(EXTRA_ROOT_URI);
350            final String authority = rootUri.getAuthority();
351
352            if ((flags & DocumentsContract.FLAG_SUPPORTS_THUMBNAIL) != 0) {
353                final Uri childUri = DocumentsContract.buildDocumentUri(rootUri, docId);
354                icon.setImageURI(childUri);
355            } else {
356                icon.setImageDrawable(
357                        DocumentsActivity.resolveDocumentIcon(context, authority, mimeType));
358            }
359
360            title.setText(displayName);
361            if (summary != null) {
362                summary.setText(DateUtils.getRelativeTimeSpanString(lastModified));
363            }
364        }
365    }
366
367    private static int getDocumentFlags(Context context, Uri uri) {
368        final Cursor cursor = context.getContentResolver().query(uri, new String[] {
369                DocumentColumns.FLAGS }, null, null, null);
370        try {
371            if (cursor.moveToFirst()) {
372                return getCursorInt(cursor, DocumentColumns.FLAGS);
373            } else {
374                return 0;
375            }
376        } finally {
377            cursor.close();
378        }
379    }
380
381    public static String getCursorString(Cursor cursor, String columnName) {
382        return cursor.getString(cursor.getColumnIndex(columnName));
383    }
384
385    public static long getCursorLong(Cursor cursor, String columnName) {
386        return cursor.getLong(cursor.getColumnIndex(columnName));
387    }
388
389    public static int getCursorInt(Cursor cursor, String columnName) {
390        return cursor.getInt(cursor.getColumnIndex(columnName));
391    }
392}
393