blob: 8722082856a6b3e7a4d715f732ee4e04ca13a4bb [file] [log] [blame]
Jeff Sharkeye22d02e2013-04-26 16:54:55 -07001/*
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
Jeff Sharkey311a7d82015-04-11 21:27:21 -070019import static com.android.documentsui.BaseActivity.State.ACTION_BROWSE;
20import static com.android.documentsui.BaseActivity.State.ACTION_BROWSE_ALL;
Steve McKayd0a2a2c2015-03-25 14:35:33 -070021import static com.android.documentsui.BaseActivity.State.ACTION_CREATE;
22import static com.android.documentsui.BaseActivity.State.ACTION_MANAGE;
23import static com.android.documentsui.BaseActivity.State.MODE_GRID;
24import static com.android.documentsui.BaseActivity.State.MODE_LIST;
25import static com.android.documentsui.BaseActivity.State.MODE_UNKNOWN;
26import static com.android.documentsui.BaseActivity.State.SORT_ORDER_UNKNOWN;
Steve McKayfefcd702015-08-20 16:19:38 +000027import static com.android.documentsui.Shared.TAG;
Jeff Sharkeyac9e6272013-08-31 21:27:44 -070028import static com.android.documentsui.model.DocumentInfo.getCursorInt;
29import static com.android.documentsui.model.DocumentInfo.getCursorLong;
30import static com.android.documentsui.model.DocumentInfo.getCursorString;
Steve McKayef280152015-06-11 10:10:49 -070031import static com.android.internal.util.Preconditions.checkNotNull;
Steve McKayd57f5fa2015-07-23 16:33:41 -070032import static com.android.internal.util.Preconditions.checkState;
Steve McKay0599a442015-05-05 14:50:00 -070033
Ben Kwaf5858932015-04-07 15:43:39 -070034import android.app.Activity;
Jeff Sharkeyf63b7772013-10-01 17:57:41 -070035import android.app.ActivityManager;
Jeff Sharkey09c10bf2013-06-30 20:02:59 -070036import android.app.Fragment;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070037import android.app.FragmentManager;
38import android.app.FragmentTransaction;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070039import android.app.LoaderManager.LoaderCallbacks;
Vladislav Kaznacheev89b90332015-05-01 13:46:57 -070040import android.content.ClipData;
Jeff Sharkey3fd11772013-09-30 14:26:27 -070041import android.content.ContentProviderClient;
Jeff Sharkey873daa32013-08-18 17:38:20 -070042import android.content.ContentResolver;
Jeff Sharkeyd10f0492013-09-09 17:35:46 -070043import android.content.ContentValues;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070044import android.content.Context;
Jeff Sharkey873daa32013-08-18 17:38:20 -070045import android.content.Intent;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070046import android.content.Loader;
Jeff Sharkey083d7e12014-07-27 21:01:45 -070047import android.content.res.Resources;
Jeff Sharkeyac9e6272013-08-31 21:27:44 -070048import android.database.Cursor;
Jeff Sharkey8a8fb672013-05-07 12:41:33 -070049import android.graphics.Bitmap;
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -070050import android.graphics.Canvas;
Jeff Sharkey8a8fb672013-05-07 12:41:33 -070051import android.graphics.Point;
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -070052import android.graphics.drawable.Drawable;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070053import android.net.Uri;
Jeff Sharkey8a8fb672013-05-07 12:41:33 -070054import android.os.AsyncTask;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070055import android.os.Bundle;
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -070056import android.os.CancellationSignal;
Makoto Onuki77778752015-07-01 14:55:14 -070057import android.os.Handler;
58import android.os.Looper;
Jeff Sharkeye39a89b2013-10-29 11:56:37 -070059import android.os.OperationCanceledException;
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -070060import android.os.Parcelable;
Steve McKay8e258c62015-05-06 14:27:57 -070061import android.os.SystemProperties;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070062import android.provider.DocumentsContract;
Jeff Sharkeyac9e6272013-08-31 21:27:44 -070063import android.provider.DocumentsContract.Document;
Ben Kwa24be5d32015-08-27 16:04:46 -070064import android.support.annotation.Nullable;
Ben Kwa7461a952015-09-01 11:03:01 -070065import android.support.annotation.VisibleForTesting;
Ben Kwa91923182015-08-27 16:06:33 -070066import android.support.design.widget.Snackbar;
Steve McKayef280152015-06-11 10:10:49 -070067import android.support.v7.widget.GridLayoutManager;
68import android.support.v7.widget.LinearLayoutManager;
69import android.support.v7.widget.RecyclerView;
70import android.support.v7.widget.RecyclerView.LayoutManager;
Steve McKayef280152015-06-11 10:10:49 -070071import android.support.v7.widget.RecyclerView.RecyclerListener;
72import android.support.v7.widget.RecyclerView.ViewHolder;
Jeff Sharkey6d579272015-06-11 09:16:19 -070073import android.text.TextUtils;
Jeff Sharkey09c10bf2013-06-30 20:02:59 -070074import android.text.format.DateUtils;
Jeff Sharkey2e694f82013-08-06 16:26:14 -070075import android.text.format.Formatter;
Jeff Sharkey8a8fb672013-05-07 12:41:33 -070076import android.text.format.Time;
77import android.util.Log;
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -070078import android.util.SparseArray;
Ben Kwa91923182015-08-27 16:06:33 -070079import android.util.SparseBooleanArray;
Ben Kwa0574b182015-09-08 07:31:19 -070080import android.util.TypedValue;
Jeff Sharkeyc317af82013-07-01 16:56:54 -070081import android.view.ActionMode;
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -070082import android.view.DragEvent;
Steve McKayef280152015-06-11 10:10:49 -070083import android.view.GestureDetector;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070084import android.view.LayoutInflater;
Jeff Sharkey09c10bf2013-06-30 20:02:59 -070085import android.view.Menu;
Jeff Sharkey09c10bf2013-06-30 20:02:59 -070086import android.view.MenuItem;
Steve McKayef280152015-06-11 10:10:49 -070087import android.view.MotionEvent;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070088import android.view.View;
Steve McKayd57f5fa2015-07-23 16:33:41 -070089import android.view.View.OnLayoutChangeListener;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070090import android.view.ViewGroup;
Vladislav Kaznacheev9400b892015-09-04 09:17:37 -070091import android.view.ViewParent;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070092import android.widget.ImageView;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070093import android.widget.TextView;
Jeff Sharkey873daa32013-08-18 17:38:20 -070094import android.widget.Toast;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070095
Steve McKay351a7492015-08-04 10:11:01 -070096import com.android.documentsui.BaseActivity.DocumentContext;
Steve McKayd0a2a2c2015-03-25 14:35:33 -070097import com.android.documentsui.BaseActivity.State;
Steve McKayef280152015-06-11 10:10:49 -070098import com.android.documentsui.MultiSelectManager.Selection;
Jeff Sharkey753a3ae2013-10-22 17:09:44 -070099import com.android.documentsui.ProviderExecutor.Preemptable;
Jeff Sharkeyd10f0492013-09-09 17:35:46 -0700100import com.android.documentsui.RecentsProvider.StateColumns;
Jeff Sharkey724deeb2013-08-31 15:02:20 -0700101import com.android.documentsui.model.DocumentInfo;
Tomasz Mikolajewski9452c442015-04-14 16:32:41 +0900102import com.android.documentsui.model.DocumentStack;
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700103import com.android.documentsui.model.RootInfo;
Steve McKay1f199482015-05-20 15:58:42 -0700104import com.android.internal.util.Preconditions;
Steve McKayfefcd702015-08-20 16:19:38 +0000105import com.google.common.collect.Lists;
Jeff Sharkeyc317af82013-07-01 16:56:54 -0700106
107import java.util.ArrayList;
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -0700108import java.util.Collections;
Jeff Sharkeya5defe32013-08-05 17:56:48 -0700109import java.util.List;
Jeff Sharkey09c10bf2013-06-30 20:02:59 -0700110
111/**
112 * Display the documents inside a single directory.
113 */
114public class DirectoryFragment extends Fragment {
115
Jeff Sharkeya5defe32013-08-05 17:56:48 -0700116 public static final int TYPE_NORMAL = 1;
117 public static final int TYPE_SEARCH = 2;
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700118 public static final int TYPE_RECENT_OPEN = 3;
Jeff Sharkey5b535922013-08-02 15:55:26 -0700119
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700120 public static final int ANIM_NONE = 1;
121 public static final int ANIM_SIDE = 2;
122 public static final int ANIM_DOWN = 3;
123 public static final int ANIM_UP = 4;
124
Ben Kwaf5858932015-04-07 15:43:39 -0700125 public static final int REQUEST_COPY_DESTINATION = 1;
126
Steve McKayef280152015-06-11 10:10:49 -0700127 private static final int LOADER_ID = 42;
128 private static final boolean DEBUG = false;
Steve McKay8e258c62015-05-06 14:27:57 -0700129 private static final boolean DEBUG_ENABLE_DND = false;
130
Jeff Sharkey2e694f82013-08-06 16:26:14 -0700131 private static final String EXTRA_TYPE = "type";
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700132 private static final String EXTRA_ROOT = "root";
133 private static final String EXTRA_DOC = "doc";
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700134 private static final String EXTRA_QUERY = "query";
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700135 private static final String EXTRA_IGNORE_STATE = "ignoreState";
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700136
Ben Kwa7461a952015-09-01 11:03:01 -0700137 private Model mModel;
Ben Kwa24be5d32015-08-27 16:04:46 -0700138
Steve McKayef280152015-06-11 10:10:49 -0700139 private final Handler mHandler = new Handler(Looper.getMainLooper());
Jeff Sharkey2e694f82013-08-06 16:26:14 -0700140
Steve McKayef280152015-06-11 10:10:49 -0700141 private View mEmptyView;
142 private RecyclerView mRecView;
143
144 private int mType = TYPE_NORMAL;
145 private String mStateKey;
146
147 private int mLastMode = MODE_UNKNOWN;
148 private int mLastSortOrder = SORT_ORDER_UNKNOWN;
149 private boolean mLastShowSize;
150 private boolean mHideGridTitles;
151 private boolean mSvelteRecents;
152 private Point mThumbSize;
153 private DocumentsAdapter mAdapter;
154 private LoaderCallbacks<DirectoryResult> mCallbacks;
Steve McKay1f199482015-05-20 15:58:42 -0700155 private FragmentTuner mFragmentTuner;
156 private DocumentClipper mClipper;
Steve McKayef280152015-06-11 10:10:49 -0700157 // These are lazily initialized.
Steve McKayd57f5fa2015-07-23 16:33:41 -0700158 private LinearLayoutManager mListLayout;
159 private GridLayoutManager mGridLayout;
Steve McKayd57f5fa2015-07-23 16:33:41 -0700160 private int mColumnCount = 1; // This will get updated when layout changes.
Steve McKay1f199482015-05-20 15:58:42 -0700161
Ben Kwac42fa402015-09-16 08:04:37 -0700162 private MessageBar mMessageBar;
163
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700164 public static void showNormal(FragmentManager fm, RootInfo root, DocumentInfo doc, int anim) {
165 show(fm, TYPE_NORMAL, root, doc, null, anim);
Jeff Sharkey2e694f82013-08-06 16:26:14 -0700166 }
167
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700168 public static void showSearch(FragmentManager fm, RootInfo root, String query, int anim) {
169 show(fm, TYPE_SEARCH, root, null, query, anim);
Jeff Sharkey2e694f82013-08-06 16:26:14 -0700170 }
171
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700172 public static void showRecentsOpen(FragmentManager fm, int anim) {
173 show(fm, TYPE_RECENT_OPEN, null, null, null, anim);
Jeff Sharkey2e694f82013-08-06 16:26:14 -0700174 }
175
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700176 private static void show(FragmentManager fm, int type, RootInfo root, DocumentInfo doc,
177 String query, int anim) {
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700178 final Bundle args = new Bundle();
Jeff Sharkey2e694f82013-08-06 16:26:14 -0700179 args.putInt(EXTRA_TYPE, type);
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700180 args.putParcelable(EXTRA_ROOT, root);
181 args.putParcelable(EXTRA_DOC, doc);
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700182 args.putString(EXTRA_QUERY, query);
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700183
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700184 final FragmentTransaction ft = fm.beginTransaction();
185 switch (anim) {
186 case ANIM_SIDE:
187 args.putBoolean(EXTRA_IGNORE_STATE, true);
188 break;
189 case ANIM_DOWN:
190 args.putBoolean(EXTRA_IGNORE_STATE, true);
191 ft.setCustomAnimations(R.animator.dir_down, R.animator.dir_frozen);
192 break;
193 case ANIM_UP:
194 ft.setCustomAnimations(R.animator.dir_frozen, R.animator.dir_up);
195 break;
196 }
197
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700198 final DirectoryFragment fragment = new DirectoryFragment();
199 fragment.setArguments(args);
200
Jeff Sharkey76112212013-08-06 11:26:10 -0700201 ft.replace(R.id.container_directory, fragment);
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700202 ft.commitAllowingStateLoss();
203 }
204
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700205 private static String buildStateKey(RootInfo root, DocumentInfo doc) {
206 final StringBuilder builder = new StringBuilder();
207 builder.append(root != null ? root.authority : "null").append(';');
208 builder.append(root != null ? root.rootId : "null").append(';');
209 builder.append(doc != null ? doc.documentId : "null");
210 return builder.toString();
211 }
212
Jeff Sharkeya5defe32013-08-05 17:56:48 -0700213 public static DirectoryFragment get(FragmentManager fm) {
214 // TODO: deal with multiple directories shown at once
Jeff Sharkey76112212013-08-06 11:26:10 -0700215 return (DirectoryFragment) fm.findFragmentById(R.id.container_directory);
Jeff Sharkeya5defe32013-08-05 17:56:48 -0700216 }
217
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700218 @Override
219 public View onCreateView(
220 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
221 final Context context = inflater.getContext();
Jeff Sharkey083d7e12014-07-27 21:01:45 -0700222 final Resources res = context.getResources();
Jeff Sharkey09c10bf2013-06-30 20:02:59 -0700223 final View view = inflater.inflate(R.layout.fragment_directory, container, false);
224
Ben Kwac42fa402015-09-16 08:04:37 -0700225 mMessageBar = MessageBar.create(getChildFragmentManager());
226
Jeff Sharkeyc6cbdf12013-08-07 16:22:02 -0700227 mEmptyView = view.findViewById(android.R.id.empty);
228
Steve McKayef280152015-06-11 10:10:49 -0700229 mRecView = (RecyclerView) view.findViewById(R.id.recyclerView);
230 mRecView.setRecyclerListener(
231 new RecyclerListener() {
232 @Override
233 public void onViewRecycled(ViewHolder holder) {
234 cancelThumbnailTask(holder.itemView);
235 }
236 });
Steve McKay8e258c62015-05-06 14:27:57 -0700237
Steve McKayd57f5fa2015-07-23 16:33:41 -0700238 // TODO: Rather than update columns on layout changes, push this
239 // code (or something like it) into GridLayoutManager.
240 mRecView.addOnLayoutChangeListener(
241 new OnLayoutChangeListener() {
242
243 @Override
244 public void onLayoutChange(
245 View v, int left, int top, int right, int bottom, int oldLeft,
246 int oldTop, int oldRight, int oldBottom) {
Steve McKayfefcd702015-08-20 16:19:38 +0000247 mColumnCount = calculateColumnCount();
Steve McKayd57f5fa2015-07-23 16:33:41 -0700248 if (mGridLayout != null) {
249 mGridLayout.setSpanCount(mColumnCount);
250 }
251 }
252 });
253
254 // TODO: Add a divider between views (which might use RecyclerView.ItemDecoration).
Steve McKay8e258c62015-05-06 14:27:57 -0700255 if (DEBUG_ENABLE_DND) {
Steve McKayef280152015-06-11 10:10:49 -0700256 setupDragAndDropOnDirectoryView(mRecView);
Steve McKay8e258c62015-05-06 14:27:57 -0700257 }
Jeff Sharkey09c10bf2013-06-30 20:02:59 -0700258
Jeff Sharkeyac9e6272013-08-31 21:27:44 -0700259 return view;
260 }
Jeff Sharkey09c10bf2013-06-30 20:02:59 -0700261
Jeff Sharkeyac9e6272013-08-31 21:27:44 -0700262 @Override
Jeff Sharkeyf63b7772013-10-01 17:57:41 -0700263 public void onDestroyView() {
264 super.onDestroyView();
265
266 // Cancel any outstanding thumbnail requests
Steve McKayef280152015-06-11 10:10:49 -0700267 final int count = mRecView.getChildCount();
Jeff Sharkeyf63b7772013-10-01 17:57:41 -0700268 for (int i = 0; i < count; i++) {
Steve McKayef280152015-06-11 10:10:49 -0700269 final View view = mRecView.getChildAt(i);
270 cancelThumbnailTask(view);
Jeff Sharkeyf63b7772013-10-01 17:57:41 -0700271 }
Jeff Sharkeyfaaeb392013-10-04 14:44:56 -0700272
Steve McKayef280152015-06-11 10:10:49 -0700273 // Clear any outstanding selection
Ben Kwa24be5d32015-08-27 16:04:46 -0700274 mModel.clearSelection();
Jeff Sharkeyf63b7772013-10-01 17:57:41 -0700275 }
276
277 @Override
Jeff Sharkeyac9e6272013-08-31 21:27:44 -0700278 public void onActivityCreated(Bundle savedInstanceState) {
279 super.onActivityCreated(savedInstanceState);
280
281 final Context context = getActivity();
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700282 final State state = getDisplayState(DirectoryFragment.this);
Jeff Sharkeyac9e6272013-08-31 21:27:44 -0700283
Jeff Sharkey9656a532013-09-13 13:42:19 -0700284 final RootInfo root = getArguments().getParcelable(EXTRA_ROOT);
285 final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC);
286
Steve McKayef280152015-06-11 10:10:49 -0700287 mAdapter = new DocumentsAdapter(context);
288 mRecView.setAdapter(mAdapter);
289
290 GestureDetector.SimpleOnGestureListener listener =
291 new GestureDetector.SimpleOnGestureListener() {
292 @Override
293 public boolean onSingleTapUp(MotionEvent e) {
294 return DirectoryFragment.this.onSingleTapUp(e);
295 }
Steve McKay93d8ef42015-07-30 12:27:44 -0700296 @Override
297 public boolean onDoubleTap(MotionEvent e) {
298 Log.d(TAG, "Handling double tap.");
299 return DirectoryFragment.this.onDoubleTap(e);
300 }
Steve McKayef280152015-06-11 10:10:49 -0700301 };
302
Ben Kwa24be5d32015-08-27 16:04:46 -0700303 // TODO: instead of inserting the view into the constructor, extract listener-creation code
304 // and set the listener on the view after the fact. Then the view doesn't need to be passed
305 // into the selection manager which is passed into the model.
306 MultiSelectManager selMgr= new MultiSelectManager(
Steve McKay57394872015-08-12 14:48:34 -0700307 mRecView,
308 listener,
309 state.allowMultiple
310 ? MultiSelectManager.MODE_MULTIPLE
311 : MultiSelectManager.MODE_SINGLE);
Ben Kwa24be5d32015-08-27 16:04:46 -0700312 selMgr.addCallback(new SelectionModeListener());
Ben Kwa7461a952015-09-01 11:03:01 -0700313
314 mModel = new Model(context, selMgr);
Ben Kwa24be5d32015-08-27 16:04:46 -0700315 mModel.setSelectionManager(selMgr);
Ben Kwa7461a952015-09-01 11:03:01 -0700316 mModel.addUpdateListener(mAdapter);
Steve McKayef280152015-06-11 10:10:49 -0700317
Jeff Sharkey2e694f82013-08-06 16:26:14 -0700318 mType = getArguments().getInt(EXTRA_TYPE);
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700319 mStateKey = buildStateKey(root, doc);
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700320
Steve McKay1f199482015-05-20 15:58:42 -0700321 mFragmentTuner = pickFragmentTuner(state);
322 mClipper = new DocumentClipper(context);
323
Jeff Sharkey9656a532013-09-13 13:42:19 -0700324 if (mType == TYPE_RECENT_OPEN) {
325 // Hide titles when showing recents for picking images/videos
326 mHideGridTitles = MimePredicate.mimeMatches(
327 MimePredicate.VISUAL_MIMES, state.acceptMimes);
328 } else {
329 mHideGridTitles = (doc != null) && doc.isGridTitlesHidden();
330 }
331
Jeff Sharkeyf63b7772013-10-01 17:57:41 -0700332 final ActivityManager am = (ActivityManager) context.getSystemService(
333 Context.ACTIVITY_SERVICE);
334 mSvelteRecents = am.isLowRamDevice() && (mType == TYPE_RECENT_OPEN);
335
Jeff Sharkey46899c82013-08-18 22:26:48 -0700336 mCallbacks = new LoaderCallbacks<DirectoryResult>() {
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700337 @Override
Jeff Sharkey46899c82013-08-18 22:26:48 -0700338 public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) {
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700339 final String query = getArguments().getString(EXTRA_QUERY);
Jeff Sharkey46165b52013-07-31 20:53:22 -0700340
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700341 Uri contentsUri;
342 switch (mType) {
343 case TYPE_NORMAL:
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700344 contentsUri = DocumentsContract.buildChildDocumentsUri(
345 doc.authority, doc.documentId);
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -0700346 if (state.action == ACTION_MANAGE) {
347 contentsUri = DocumentsContract.setManageMode(contentsUri);
348 }
Jeff Sharkeyd10f0492013-09-09 17:35:46 -0700349 return new DirectoryLoader(
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -0700350 context, mType, root, doc, contentsUri, state.userSortOrder);
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700351 case TYPE_SEARCH:
352 contentsUri = DocumentsContract.buildSearchDocumentsUri(
Jeff Sharkey0e8c8712013-09-12 21:59:06 -0700353 root.authority, root.rootId, query);
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -0700354 if (state.action == ACTION_MANAGE) {
355 contentsUri = DocumentsContract.setManageMode(contentsUri);
356 }
Jeff Sharkeyd10f0492013-09-09 17:35:46 -0700357 return new DirectoryLoader(
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -0700358 context, mType, root, doc, contentsUri, state.userSortOrder);
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700359 case TYPE_RECENT_OPEN:
Jeff Sharkey1c903cc2013-09-02 17:19:40 -0700360 final RootsCache roots = DocumentsApplication.getRootsCache(context);
Jeff Sharkey8b997042013-09-19 15:25:56 -0700361 return new RecentLoader(context, roots, state);
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700362 default:
363 throw new IllegalStateException("Unknown type " + mType);
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700364 }
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700365 }
366
367 @Override
Jeff Sharkey46899c82013-08-18 22:26:48 -0700368 public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
Makoto Onuki77778752015-07-01 14:55:14 -0700369 if (result == null || result.exception != null) {
370 // onBackPressed does a fragment transaction, which can't be done inside
371 // onLoadFinished
372 mHandler.post(new Runnable() {
373 @Override
374 public void run() {
375 final Activity activity = getActivity();
376 if (activity != null) {
377 activity.onBackPressed();
378 }
379 }
380 });
381 return;
382 }
383
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700384 if (!isAdded()) return;
385
Ben Kwa24be5d32015-08-27 16:04:46 -0700386 mModel.update(result);
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700387
388 // Push latest state up to UI
389 // TODO: if mode change was racing with us, don't overwrite it
Jeff Sharkey7d58fc62013-09-12 16:25:02 -0700390 if (result.mode != MODE_UNKNOWN) {
391 state.derivedMode = result.mode;
392 }
Jeff Sharkeyd10f0492013-09-09 17:35:46 -0700393 state.derivedSortOrder = result.sortOrder;
Steve McKayd0a2a2c2015-03-25 14:35:33 -0700394 ((BaseActivity) context).onStateChanged();
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700395
396 updateDisplayState();
397
Jeff Sharkey25f10b32013-10-07 14:08:17 -0700398 // When launched into empty recents, show drawer
Ben Kwa24be5d32015-08-27 16:04:46 -0700399 if (mType == TYPE_RECENT_OPEN && mModel.isEmpty() && !state.stackTouched &&
Steve McKayb68dd222015-04-20 17:18:15 -0700400 context instanceof DocumentsActivity) {
401 ((DocumentsActivity) context).setRootsDrawerOpen(true);
Jeff Sharkey25f10b32013-10-07 14:08:17 -0700402 }
403
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700404 // Restore any previous instance state
405 final SparseArray<Parcelable> container = state.dirState.remove(mStateKey);
406 if (container != null && !getArguments().getBoolean(EXTRA_IGNORE_STATE, false)) {
407 getView().restoreHierarchyState(container);
408 } else if (mLastSortOrder != state.derivedSortOrder) {
Steve McKayef280152015-06-11 10:10:49 -0700409 mRecView.smoothScrollToPosition(0);
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700410 }
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700411
412 mLastSortOrder = state.derivedSortOrder;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700413 }
414
415 @Override
Jeff Sharkey46899c82013-08-18 22:26:48 -0700416 public void onLoaderReset(Loader<DirectoryResult> loader) {
Ben Kwa24be5d32015-08-27 16:04:46 -0700417 mModel.update(null);
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700418 }
419 };
420
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700421 // Kick off loader at least once
Steve McKayef280152015-06-11 10:10:49 -0700422 getLoaderManager().restartLoader(LOADER_ID, null, mCallbacks);
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700423
Kyle Horimoto426bd0d2015-07-29 15:33:49 -0700424 mFragmentTuner.afterActivityCreated(this);
Jeff Sharkey2e694f82013-08-06 16:26:14 -0700425 updateDisplayState();
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700426 }
427
Jeff Sharkey42d26792013-09-06 13:22:09 -0700428 @Override
Ben Kwaf5858932015-04-07 15:43:39 -0700429 public void onActivityResult(int requestCode, int resultCode, Intent data) {
Ben Kwaf5858932015-04-07 15:43:39 -0700430 // There's only one request code right now. Replace this with a switch statement or
431 // something more scalable when more codes are added.
432 if (requestCode != REQUEST_COPY_DESTINATION) {
433 return;
434 }
435 if (resultCode == Activity.RESULT_CANCELED || data == null) {
436 // User pressed the back button or otherwise cancelled the destination pick. Don't
437 // proceed with the copy.
438 return;
439 }
440
Tomasz Mikolajewski9452c442015-04-14 16:32:41 +0900441 CopyService.start(getActivity(), getDisplayState(this).selectedDocumentsForCopy,
Ben Kwacb4461f2015-05-05 11:50:11 -0700442 (DocumentStack) data.getParcelableExtra(CopyService.EXTRA_STACK),
443 data.getIntExtra(CopyService.EXTRA_TRANSFER_MODE, CopyService.TRANSFER_MODE_NONE));
Ben Kwaf5858932015-04-07 15:43:39 -0700444 }
445
Steve McKayef280152015-06-11 10:10:49 -0700446 private int getEventAdapterPosition(MotionEvent e) {
447 View view = mRecView.findChildViewUnder(e.getX(), e.getY());
448 return view != null ? mRecView.getChildAdapterPosition(view) : RecyclerView.NO_POSITION;
449 }
450
451 private boolean onSingleTapUp(MotionEvent e) {
Ben Kwa24be5d32015-08-27 16:04:46 -0700452 if (Events.isTouchEvent(e) && mModel.getSelection().isEmpty()) {
Steve McKay93d8ef42015-07-30 12:27:44 -0700453 int position = getEventAdapterPosition(e);
454 if (position != RecyclerView.NO_POSITION) {
455 return handleViewItem(position);
Steve McKayef280152015-06-11 10:10:49 -0700456 }
457 }
Steve McKay93d8ef42015-07-30 12:27:44 -0700458 return false;
459 }
Steve McKayef280152015-06-11 10:10:49 -0700460
Steve McKay93d8ef42015-07-30 12:27:44 -0700461 protected boolean onDoubleTap(MotionEvent e) {
462 if (Events.isMouseEvent(e)) {
463 Log.d(TAG, "Handling double tap from mouse.");
464 int position = getEventAdapterPosition(e);
465 if (position != RecyclerView.NO_POSITION) {
466 return handleViewItem(position);
467 }
468 }
469 return false;
470 }
471
472 private boolean handleViewItem(int position) {
Ben Kwa24be5d32015-08-27 16:04:46 -0700473 final Cursor cursor = mModel.getItem(position);
Steve McKay93d8ef42015-07-30 12:27:44 -0700474 checkNotNull(cursor, "Cursor cannot be null.");
475 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
476 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
477 if (isDocumentEnabled(docMimeType, docFlags)) {
478 final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
Ben Kwa24be5d32015-08-27 16:04:46 -0700479 ((BaseActivity) getActivity()).onDocumentPicked(doc, mModel);
480 mModel.clearSelection();
Steve McKay93d8ef42015-07-30 12:27:44 -0700481 return true;
482 }
Steve McKayef280152015-06-11 10:10:49 -0700483 return false;
484 }
485
Ben Kwaf5858932015-04-07 15:43:39 -0700486 @Override
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700487 public void onStop() {
488 super.onStop();
489
490 // Remember last scroll location
491 final SparseArray<Parcelable> container = new SparseArray<Parcelable>();
492 getView().saveHierarchyState(container);
493 final State state = getDisplayState(this);
494 state.dirState.put(mStateKey, container);
495 }
496
497 @Override
Jeff Sharkey7d58fc62013-09-12 16:25:02 -0700498 public void onResume() {
499 super.onResume();
Jeff Sharkey42d26792013-09-06 13:22:09 -0700500 updateDisplayState();
501 }
502
Jeff Sharkeye8d13ea2014-08-08 15:10:03 -0700503 public void onDisplayStateChanged() {
504 updateDisplayState();
505 }
506
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700507 public void onUserSortOrderChanged() {
Jeff Sharkeyd10f0492013-09-09 17:35:46 -0700508 // Sort order change always triggers reload; we'll trigger state change
509 // on the flip side.
Steve McKayef280152015-06-11 10:10:49 -0700510 getLoaderManager().restartLoader(LOADER_ID, null, mCallbacks);
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700511 }
512
513 public void onUserModeChanged() {
Jeff Sharkeyd10f0492013-09-09 17:35:46 -0700514 final ContentResolver resolver = getActivity().getContentResolver();
515 final State state = getDisplayState(this);
516
517 final RootInfo root = getArguments().getParcelable(EXTRA_ROOT);
518 final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC);
519
Jeff Sharkey0e8c8712013-09-12 21:59:06 -0700520 if (root != null && doc != null) {
Jeff Sharkey7d58fc62013-09-12 16:25:02 -0700521 final Uri stateUri = RecentsProvider.buildState(
522 root.authority, root.rootId, doc.documentId);
523 final ContentValues values = new ContentValues();
524 values.put(StateColumns.MODE, state.userMode);
Jeff Sharkeyd10f0492013-09-09 17:35:46 -0700525
Jeff Sharkey7d58fc62013-09-12 16:25:02 -0700526 new AsyncTask<Void, Void, Void>() {
527 @Override
528 protected Void doInBackground(Void... params) {
529 resolver.insert(stateUri, values);
530 return null;
531 }
532 }.execute();
533 }
Jeff Sharkeyd10f0492013-09-09 17:35:46 -0700534
535 // Mode change is just visual change; no need to kick loader, and
536 // deliver change event immediately.
537 state.derivedMode = state.userMode;
Steve McKayd0a2a2c2015-03-25 14:35:33 -0700538 ((BaseActivity) getActivity()).onStateChanged();
Jeff Sharkeyd10f0492013-09-09 17:35:46 -0700539
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700540 updateDisplayState();
541 }
542
543 private void updateDisplayState() {
Jeff Sharkeyb3620442013-09-01 18:41:04 -0700544 final State state = getDisplayState(this);
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700545
Jeff Sharkey5e1884d2013-09-10 17:56:39 -0700546 if (mLastMode == state.derivedMode && mLastShowSize == state.showSize) return;
Jeff Sharkeyd10f0492013-09-09 17:35:46 -0700547 mLastMode = state.derivedMode;
Jeff Sharkey5e1884d2013-09-10 17:56:39 -0700548 mLastShowSize = state.showSize;
Jeff Sharkey09c10bf2013-06-30 20:02:59 -0700549
Steve McKayef280152015-06-11 10:10:49 -0700550 updateLayout(state.derivedMode);
Jeff Sharkey09c10bf2013-06-30 20:02:59 -0700551
Steve McKayef280152015-06-11 10:10:49 -0700552 mRecView.setAdapter(mAdapter);
553 }
554
555 /**
556 * Returns a {@code LayoutManager} for {@code mode}, lazily initializing
557 * classes as needed.
558 */
559 private void updateLayout(int mode) {
Jeff Sharkey8a8fb672013-05-07 12:41:33 -0700560 final int thumbSize;
Steve McKayef280152015-06-11 10:10:49 -0700561
562 final LayoutManager layout;
563 switch (mode) {
564 case MODE_GRID:
Steve McKayef280152015-06-11 10:10:49 -0700565 thumbSize = getResources().getDimensionPixelSize(R.dimen.grid_width);
Steve McKaya9be7182015-07-22 16:03:35 -0700566 if (mGridLayout == null) {
Steve McKayd57f5fa2015-07-23 16:33:41 -0700567 mGridLayout = new GridLayoutManager(getContext(), mColumnCount );
Steve McKaya9be7182015-07-22 16:03:35 -0700568 }
Steve McKayef280152015-06-11 10:10:49 -0700569 layout = mGridLayout;
570 break;
571 case MODE_LIST:
Steve McKaya9be7182015-07-22 16:03:35 -0700572 thumbSize = getResources().getDimensionPixelSize(R.dimen.icon_size);
Steve McKayef280152015-06-11 10:10:49 -0700573 if (mListLayout == null) {
574 mListLayout = new LinearLayoutManager(getContext());
575 }
Steve McKayef280152015-06-11 10:10:49 -0700576 layout = mListLayout;
577 break;
578 case MODE_UNKNOWN:
579 default:
580 throw new IllegalArgumentException("Unsupported layout mode: " + mode);
Jeff Sharkey09c10bf2013-06-30 20:02:59 -0700581 }
Jeff Sharkey8a8fb672013-05-07 12:41:33 -0700582
Steve McKayef280152015-06-11 10:10:49 -0700583 mRecView.setLayoutManager(layout);
Kyle Horimoto2da6e4a2015-08-27 16:44:00 -0700584 // TODO: Once b/23691541 is resolved, use a listener within MultiSelectManager instead of
585 // imperatively calling this function.
Steve McKay9058e042015-09-01 12:31:24 -0700586 mModel.mSelectionManager.handleLayoutChanged();
Steve McKayef280152015-06-11 10:10:49 -0700587 // setting layout manager automatically invalidates existing ViewHolders.
Jeff Sharkey8a8fb672013-05-07 12:41:33 -0700588 mThumbSize = new Point(thumbSize, thumbSize);
Jeff Sharkey09c10bf2013-06-30 20:02:59 -0700589 }
590
Steve McKayfefcd702015-08-20 16:19:38 +0000591 private int calculateColumnCount() {
592 int cellWidth = getResources().getDimensionPixelSize(R.dimen.grid_width);
593 int cellMargin = 2 * getResources().getDimensionPixelSize(R.dimen.grid_item_margin);
Steve McKayd57f5fa2015-07-23 16:33:41 -0700594 int viewPadding = mRecView.getPaddingLeft() + mRecView.getPaddingRight();
Steve McKayfefcd702015-08-20 16:19:38 +0000595
Steve McKayd57f5fa2015-07-23 16:33:41 -0700596 checkState(mRecView.getWidth() > 0);
597 int columnCount = Math.max(1,
Steve McKayfefcd702015-08-20 16:19:38 +0000598 (mRecView.getWidth() - viewPadding) / (cellWidth + cellMargin));
599
Steve McKayd57f5fa2015-07-23 16:33:41 -0700600 return columnCount;
601 }
602
Steve McKayef280152015-06-11 10:10:49 -0700603 /**
604 * Manages the integration between our ActionMode and MultiSelectManager, initiating
605 * ActionMode when there is a selection, canceling it when there is no selection,
606 * and clearing selection when action mode is explicitly exited by the user.
607 */
608 private final class SelectionModeListener
609 implements MultiSelectManager.Callback, ActionMode.Callback {
610
611 private Selection mSelected = new Selection();
612 private ActionMode mActionMode;
Steve McKay4f4232d2015-07-22 12:13:46 -0700613 private int mNoDeleteCount = 0;
614 private Menu mMenu;
Steve McKayef280152015-06-11 10:10:49 -0700615
Jeff Sharkey09c10bf2013-06-30 20:02:59 -0700616 @Override
Steve McKayef280152015-06-11 10:10:49 -0700617 public boolean onBeforeItemStateChange(int position, boolean selected) {
Ben Kwac42fa402015-09-16 08:04:37 -0700618 // Directories cannot be checked
Steve McKayef280152015-06-11 10:10:49 -0700619 if (selected) {
Ben Kwa24be5d32015-08-27 16:04:46 -0700620 final Cursor cursor = mModel.getItem(position);
Steve McKayef280152015-06-11 10:10:49 -0700621 checkNotNull(cursor, "Cursor cannot be null.");
Jeff Sharkey7cf49032013-09-26 10:54:16 -0700622 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
623 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
Steve McKayef280152015-06-11 10:10:49 -0700624 return isDocumentEnabled(docMimeType, docFlags);
625 }
626 return true;
627 }
628
629 @Override
630 public void onItemStateChanged(int position, boolean selected) {
Steve McKay4f4232d2015-07-22 12:13:46 -0700631
Ben Kwa24be5d32015-08-27 16:04:46 -0700632 final Cursor cursor = mModel.getItem(position);
Steve McKay4f4232d2015-07-22 12:13:46 -0700633 checkNotNull(cursor, "Cursor cannot be null.");
634
635 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
636 if ((docFlags & Document.FLAG_SUPPORTS_DELETE) == 0) {
637 mNoDeleteCount += selected ? 1 : -1;
638 }
Steve McKay57394872015-08-12 14:48:34 -0700639 }
Steve McKay4f4232d2015-07-22 12:13:46 -0700640
Steve McKay57394872015-08-12 14:48:34 -0700641 @Override
642 public void onSelectionChanged() {
Ben Kwa24be5d32015-08-27 16:04:46 -0700643 mModel.getSelection(mSelected);
Ben Kwafe18c1b2015-09-11 15:40:18 -0700644 TypedValue color = new TypedValue();
Steve McKay4f4232d2015-07-22 12:13:46 -0700645 if (mSelected.size() > 0) {
Steve McKayef280152015-06-11 10:10:49 -0700646 if (DEBUG) Log.d(TAG, "Maybe starting action mode.");
647 if (mActionMode == null) {
648 if (DEBUG) Log.d(TAG, "Yeah. Starting action mode.");
649 mActionMode = getActivity().startActionMode(this);
Jeff Sharkeyaed873d2013-09-09 16:51:06 -0700650 }
Ben Kwafe18c1b2015-09-11 15:40:18 -0700651 getActivity().getTheme().resolveAttribute(
652 R.attr.colorActionMode, color, true);
Steve McKay4f4232d2015-07-22 12:13:46 -0700653 updateActionMenu();
654 } else {
655 if (DEBUG) Log.d(TAG, "Finishing action mode.");
656 if (mActionMode != null) {
657 mActionMode.finish();
658 }
Ben Kwa0574b182015-09-08 07:31:19 -0700659 getActivity().getTheme().resolveAttribute(
660 android.R.attr.colorPrimaryDark, color, true);
Jeff Sharkeyf339f252013-08-15 16:17:41 -0700661 }
Ben Kwafe18c1b2015-09-11 15:40:18 -0700662 getActivity().getWindow().setStatusBarColor(color.data);
Jeff Sharkey09c10bf2013-06-30 20:02:59 -0700663
Steve McKayef280152015-06-11 10:10:49 -0700664 if (mActionMode != null) {
665 mActionMode.setTitle(TextUtils.formatSelectedCount(mSelected.size()));
666 }
667 }
668
669 // Called when the user exits the action mode
670 @Override
671 public void onDestroyActionMode(ActionMode mode) {
672 if (DEBUG) Log.d(TAG, "Handling action mode destroyed.");
673 mActionMode = null;
674 // clear selection
Ben Kwa24be5d32015-08-27 16:04:46 -0700675 mModel.clearSelection();
Steve McKay4f4232d2015-07-22 12:13:46 -0700676 mSelected.clear();
677 mNoDeleteCount = 0;
Steve McKayef280152015-06-11 10:10:49 -0700678 }
679
Jeff Sharkeyc317af82013-07-01 16:56:54 -0700680 @Override
681 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
682 mode.getMenuInflater().inflate(R.menu.mode_directory, menu);
Ben Kwa24be5d32015-08-27 16:04:46 -0700683 mode.setTitle(TextUtils.formatSelectedCount(mModel.getSelection().size()));
684 return mModel.getSelection().size() > 0;
Jeff Sharkeyc317af82013-07-01 16:56:54 -0700685 }
686
687 @Override
688 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
Steve McKay4f4232d2015-07-22 12:13:46 -0700689 mMenu = menu;
690 updateActionMenu();
Jeff Sharkeyc317af82013-07-01 16:56:54 -0700691 return true;
692 }
693
Steve McKay4f4232d2015-07-22 12:13:46 -0700694 private void updateActionMenu() {
695 checkNotNull(mMenu);
696 // Delegate update logic to our owning action, since specialized logic is desired.
697 mFragmentTuner.updateActionMenu(mMenu, mType, mNoDeleteCount == 0);
698 }
699
Jeff Sharkeyc317af82013-07-01 16:56:54 -0700700 @Override
Steve McKayef280152015-06-11 10:10:49 -0700701 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
Steve McKay1f199482015-05-20 15:58:42 -0700702
Ben Kwa24be5d32015-08-27 16:04:46 -0700703 Selection selection = mModel.getSelection(new Selection());
Jeff Sharkeyc317af82013-07-01 16:56:54 -0700704
Jeff Sharkey873daa32013-08-18 17:38:20 -0700705 final int id = item.getItemId();
706 if (id == R.id.menu_open) {
Steve McKayef280152015-06-11 10:10:49 -0700707 openDocuments(selection);
Jeff Sharkeyb3620442013-09-01 18:41:04 -0700708 mode.finish();
Jeff Sharkeyc317af82013-07-01 16:56:54 -0700709 return true;
Jeff Sharkey873daa32013-08-18 17:38:20 -0700710
711 } else if (id == R.id.menu_share) {
Steve McKayef280152015-06-11 10:10:49 -0700712 shareDocuments(selection);
Jeff Sharkeyb3620442013-09-01 18:41:04 -0700713 mode.finish();
Jeff Sharkey873daa32013-08-18 17:38:20 -0700714 return true;
715
716 } else if (id == R.id.menu_delete) {
Steve McKayef280152015-06-11 10:10:49 -0700717 deleteDocuments(selection);
Jeff Sharkeyb3620442013-09-01 18:41:04 -0700718 mode.finish();
Jeff Sharkey873daa32013-08-18 17:38:20 -0700719 return true;
720
Steve McKay1f199482015-05-20 15:58:42 -0700721 } else if (id == R.id.menu_copy_to) {
Steve McKayef280152015-06-11 10:10:49 -0700722 transferDocuments(selection, CopyService.TRANSFER_MODE_COPY);
Ben Kwacb4461f2015-05-05 11:50:11 -0700723 mode.finish();
724 return true;
725
Steve McKay1f199482015-05-20 15:58:42 -0700726 } else if (id == R.id.menu_move_to) {
Steve McKayef280152015-06-11 10:10:49 -0700727 transferDocuments(selection, CopyService.TRANSFER_MODE_MOVE);
Ben Kwa41b26c12015-03-31 10:11:43 -0700728 mode.finish();
729 return true;
730
Steve McKay1f199482015-05-20 15:58:42 -0700731 } else if (id == R.id.menu_copy_to_clipboard) {
Steve McKayef280152015-06-11 10:10:49 -0700732 copySelectionToClipboard(selection);
Steve McKay1f199482015-05-20 15:58:42 -0700733 mode.finish();
734 return true;
735
Ben Kwa512a6ba2015-03-31 08:15:21 -0700736 } else if (id == R.id.menu_select_all) {
Steve McKay0599a442015-05-05 14:50:00 -0700737 selectAllFiles();
Ben Kwa512a6ba2015-03-31 08:15:21 -0700738 return true;
739
Jeff Sharkeyc317af82013-07-01 16:56:54 -0700740 } else {
741 return false;
742 }
743 }
Steve McKayef280152015-06-11 10:10:49 -0700744 }
Jeff Sharkeyc317af82013-07-01 16:56:54 -0700745
Steve McKayef280152015-06-11 10:10:49 -0700746 private static void cancelThumbnailTask(View view) {
747 final ImageView iconThumb = (ImageView) view.findViewById(R.id.icon_thumb);
748 if (iconThumb != null) {
749 final ThumbnailAsyncTask oldTask = (ThumbnailAsyncTask) iconThumb.getTag();
750 if (oldTask != null) {
751 oldTask.preempt();
752 iconThumb.setTag(null);
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -0700753 }
754 }
Steve McKayef280152015-06-11 10:10:49 -0700755 }
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -0700756
Steve McKayef280152015-06-11 10:10:49 -0700757 private void openDocuments(final Selection selected) {
Steve McKay9276f3b2015-05-27 16:11:42 -0700758 new GetDocumentsTask() {
759 @Override
760 void onDocumentsReady(List<DocumentInfo> docs) {
Steve McKay0fbfc652015-08-20 16:48:49 -0700761 // TODO: Implement support in Files activity for opening multiple docs.
Steve McKay9276f3b2015-05-27 16:11:42 -0700762 BaseActivity.get(DirectoryFragment.this).onDocumentsPicked(docs);
Ben Kwaf527c632015-04-08 15:03:35 -0700763 }
Steve McKay9276f3b2015-05-27 16:11:42 -0700764 }.execute(selected);
Jeff Sharkey873daa32013-08-18 17:38:20 -0700765 }
766
Steve McKayef280152015-06-11 10:10:49 -0700767 private void shareDocuments(final Selection selected) {
Steve McKay9276f3b2015-05-27 16:11:42 -0700768 new GetDocumentsTask() {
769 @Override
770 void onDocumentsReady(List<DocumentInfo> docs) {
771 Intent intent;
772
773 // Filter out directories - those can't be shared.
Steve McKayfefcd702015-08-20 16:19:38 +0000774 List<DocumentInfo> docsForSend = new ArrayList<>();
Steve McKay9276f3b2015-05-27 16:11:42 -0700775 for (DocumentInfo doc: docs) {
776 if (!Document.MIME_TYPE_DIR.equals(doc.mimeType)) {
777 docsForSend.add(doc);
778 }
779 }
780
781 if (docsForSend.size() == 1) {
782 final DocumentInfo doc = docsForSend.get(0);
783
784 intent = new Intent(Intent.ACTION_SEND);
785 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
786 intent.addCategory(Intent.CATEGORY_DEFAULT);
787 intent.setType(doc.mimeType);
788 intent.putExtra(Intent.EXTRA_STREAM, doc.derivedUri);
789
790 } else if (docsForSend.size() > 1) {
791 intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
792 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
793 intent.addCategory(Intent.CATEGORY_DEFAULT);
794
Steve McKayfefcd702015-08-20 16:19:38 +0000795 final ArrayList<String> mimeTypes = new ArrayList<>();
796 final ArrayList<Uri> uris = new ArrayList<>();
Steve McKay9276f3b2015-05-27 16:11:42 -0700797 for (DocumentInfo doc : docsForSend) {
798 mimeTypes.add(doc.mimeType);
799 uris.add(doc.derivedUri);
800 }
801
802 intent.setType(findCommonMimeType(mimeTypes));
803 intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
804
805 } else {
806 return;
807 }
808
809 intent = Intent.createChooser(intent, getActivity().getText(R.string.share_via));
810 startActivity(intent);
811 }
812 }.execute(selected);
813 }
814
Steve McKayef280152015-06-11 10:10:49 -0700815 private void deleteDocuments(final Selection selected) {
Ben Kwa91923182015-08-27 16:06:33 -0700816 Context context = getActivity();
817 ContentResolver resolver = context.getContentResolver();
818 String message = Shared.getQuantityString(context, R.plurals.deleting, selected.size());
Jeff Sharkey873daa32013-08-18 17:38:20 -0700819
Ben Kwa91923182015-08-27 16:06:33 -0700820 mModel.markForDeletion(selected);
Jeff Sharkey873daa32013-08-18 17:38:20 -0700821
Ben Kwa91923182015-08-27 16:06:33 -0700822 Activity activity = getActivity();
823 Snackbar.make(this.getView(), message, Snackbar.LENGTH_LONG)
824 .setAction(
825 R.string.undo,
826 new android.view.View.OnClickListener() {
827 @Override
828 public void onClick(View view) {}
829 })
830 .setCallback(
831 new Snackbar.Callback() {
832 @Override
833 public void onDismissed(Snackbar snackbar, int event) {
834 if (event == Snackbar.Callback.DISMISS_EVENT_ACTION) {
835 mModel.undoDeletion();
836 } else {
Ben Kwa7461a952015-09-01 11:03:01 -0700837 // TODO: Use a listener rather than pushing the view.
838 mModel.finalizeDeletion(DirectoryFragment.this.getView());
Ben Kwa91923182015-08-27 16:06:33 -0700839 }
Ben Kwa91923182015-08-27 16:06:33 -0700840 }
841 })
842 .show();
Jeff Sharkey873daa32013-08-18 17:38:20 -0700843 }
844
Steve McKayef280152015-06-11 10:10:49 -0700845 private void transferDocuments(final Selection selected, final int mode) {
Ben Kwaf5858932015-04-07 15:43:39 -0700846 // Pop up a dialog to pick a destination. This is inadequate but works for now.
847 // TODO: Implement a picker that is to spec.
Daichi Hironocaadd412015-04-10 15:50:38 +0900848 final Intent intent = new Intent(
Daichi Hirono22574ed2015-04-15 13:41:18 +0900849 BaseActivity.DocumentsIntent.ACTION_OPEN_COPY_DESTINATION,
Daichi Hironocaadd412015-04-10 15:50:38 +0900850 Uri.EMPTY,
851 getActivity(),
852 DocumentsActivity.class);
Steve McKay9276f3b2015-05-27 16:11:42 -0700853
854 new GetDocumentsTask() {
855 @Override
856 void onDocumentsReady(List<DocumentInfo> docs) {
857 getDisplayState(DirectoryFragment.this).selectedDocumentsForCopy = docs;
858
859 boolean directoryCopy = false;
860 for (DocumentInfo info : docs) {
861 if (Document.MIME_TYPE_DIR.equals(info.mimeType)) {
862 directoryCopy = true;
863 break;
864 }
865 }
866 intent.putExtra(BaseActivity.DocumentsIntent.EXTRA_DIRECTORY_COPY, directoryCopy);
867 intent.putExtra(CopyService.EXTRA_TRANSFER_MODE, mode);
868 startActivityForResult(intent, REQUEST_COPY_DESTINATION);
Daichi Hironof2a822d2015-04-14 17:12:54 +0900869 }
Steve McKay9276f3b2015-05-27 16:11:42 -0700870 }.execute(selected);
Ben Kwa41b26c12015-03-31 10:11:43 -0700871 }
872
Jeff Sharkeyb3620442013-09-01 18:41:04 -0700873 private static State getDisplayState(Fragment fragment) {
Steve McKayd0a2a2c2015-03-25 14:35:33 -0700874 return ((BaseActivity) fragment.getActivity()).getDisplayState();
Jeff Sharkey09c10bf2013-06-30 20:02:59 -0700875 }
876
Steve McKayef280152015-06-11 10:10:49 -0700877 // Provide a reference to the views for each data item
878 // Complex data items may need more than one view per item, and
879 // you provide access to all the views for a data item in a view holder
880 private static final class DocumentHolder extends RecyclerView.ViewHolder {
881 // each data item is just a string in this case
882 public View view;
883 public String docId; // The stable document id.
884 public DocumentHolder(View view) {
885 super(view);
886 this.view = view;
887 }
888 }
889
Ben Kwa7461a952015-09-01 11:03:01 -0700890 private final class DocumentsAdapter extends RecyclerView.Adapter<DocumentHolder>
891 implements Model.UpdateListener {
Steve McKayef280152015-06-11 10:10:49 -0700892
893 private final Context mContext;
894 private final LayoutInflater mInflater;
Steve McKayef280152015-06-11 10:10:49 -0700895
Steve McKayef280152015-06-11 10:10:49 -0700896 public DocumentsAdapter(Context context) {
897 mContext = context;
898 mInflater = LayoutInflater.from(context);
899 }
Jeff Sharkeyac9e6272013-08-31 21:27:44 -0700900
Ben Kwac42fa402015-09-16 08:04:37 -0700901 @Override
Ben Kwa7461a952015-09-01 11:03:01 -0700902 public void onModelUpdate(Model model) {
Ben Kwac42fa402015-09-16 08:04:37 -0700903 if (model.info != null || model.error != null) {
904 mMessageBar.setInfo(model.info);
905 mMessageBar.setError(model.error);
906 mMessageBar.show();
Jeff Sharkey20b32272013-09-03 15:25:52 -0700907 }
Jeff Sharkeyc6cbdf12013-08-07 16:22:02 -0700908
Ben Kwa7461a952015-09-01 11:03:01 -0700909 if (model.isEmpty()) {
Jeff Sharkeyc6cbdf12013-08-07 16:22:02 -0700910 mEmptyView.setVisibility(View.VISIBLE);
911 } else {
912 mEmptyView.setVisibility(View.GONE);
913 }
914
Jeff Sharkeya5defe32013-08-05 17:56:48 -0700915 notifyDataSetChanged();
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700916 }
917
Ben Kwac42fa402015-09-16 08:04:37 -0700918 @Override
Ben Kwa7461a952015-09-01 11:03:01 -0700919 public void onModelUpdateFailed(Exception e) {
Ben Kwac42fa402015-09-16 08:04:37 -0700920 // TODO: deal with catastrophic update failures
Ben Kwa7461a952015-09-01 11:03:01 -0700921 String error = getString(R.string.query_error);
Ben Kwa7461a952015-09-01 11:03:01 -0700922 notifyDataSetChanged();
923 }
924
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700925 @Override
Steve McKayef280152015-06-11 10:10:49 -0700926 public DocumentHolder onCreateViewHolder(ViewGroup parent, int viewType) {
927 final State state = getDisplayState(DirectoryFragment.this);
928 final LayoutInflater inflater = LayoutInflater.from(getContext());
929 switch (state.derivedMode) {
930 case MODE_GRID:
931 return new DocumentHolder(inflater.inflate(R.layout.item_doc_grid, parent, false));
932 case MODE_LIST:
933 return new DocumentHolder(inflater.inflate(R.layout.item_doc_list, parent, false));
934 case MODE_UNKNOWN:
935 default:
936 throw new IllegalStateException("Unsupported layout mode.");
Jeff Sharkey20b32272013-09-03 15:25:52 -0700937 }
938 }
939
Steve McKayef280152015-06-11 10:10:49 -0700940 @Override
941 public void onBindViewHolder(DocumentHolder holder, int position) {
942
943 final Context context = getContext();
Jeff Sharkeyb3620442013-09-01 18:41:04 -0700944 final State state = getDisplayState(DirectoryFragment.this);
Jeff Sharkey9656a532013-09-13 13:42:19 -0700945 final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC);
Jeff Sharkey873daa32013-08-18 17:38:20 -0700946 final RootsCache roots = DocumentsApplication.getRootsCache(context);
947 final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache(
948 context, mThumbSize);
949
Ben Kwa24be5d32015-08-27 16:04:46 -0700950 final Cursor cursor = mModel.getItem(position);
Steve McKayef280152015-06-11 10:10:49 -0700951 checkNotNull(cursor, "Cursor cannot be null.");
Jeff Sharkeyac9e6272013-08-31 21:27:44 -0700952
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700953 final String docAuthority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY);
954 final String docRootId = getCursorString(cursor, RootCursorWrapper.COLUMN_ROOT_ID);
Jeff Sharkeyac9e6272013-08-31 21:27:44 -0700955 final String docId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
956 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
957 final String docDisplayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
958 final long docLastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED);
959 final int docIcon = getCursorInt(cursor, Document.COLUMN_ICON);
960 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
961 final String docSummary = getCursorString(cursor, Document.COLUMN_SUMMARY);
962 final long docSize = getCursorLong(cursor, Document.COLUMN_SIZE);
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700963
Steve McKayef280152015-06-11 10:10:49 -0700964 holder.docId = docId;
965 final View itemView = holder.view;
Ben Kwa24be5d32015-08-27 16:04:46 -0700966 itemView.setActivated(mModel.isSelected(position));
Jeff Sharkey9656a532013-09-13 13:42:19 -0700967
Steve McKayef280152015-06-11 10:10:49 -0700968 final View line1 = itemView.findViewById(R.id.line1);
969 final View line2 = itemView.findViewById(R.id.line2);
970
971 final ImageView iconMime = (ImageView) itemView.findViewById(R.id.icon_mime);
972 final ImageView iconThumb = (ImageView) itemView.findViewById(R.id.icon_thumb);
973 final TextView title = (TextView) itemView.findViewById(android.R.id.title);
974 final ImageView icon1 = (ImageView) itemView.findViewById(android.R.id.icon1);
975 final ImageView icon2 = (ImageView) itemView.findViewById(android.R.id.icon2);
976 final TextView summary = (TextView) itemView.findViewById(android.R.id.summary);
977 final TextView date = (TextView) itemView.findViewById(R.id.date);
978 final TextView size = (TextView) itemView.findViewById(R.id.size);
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700979
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -0700980 final ThumbnailAsyncTask oldTask = (ThumbnailAsyncTask) iconThumb.getTag();
Jeff Sharkey8a8fb672013-05-07 12:41:33 -0700981 if (oldTask != null) {
Jeff Sharkey753a3ae2013-10-22 17:09:44 -0700982 oldTask.preempt();
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -0700983 iconThumb.setTag(null);
Jeff Sharkey8a8fb672013-05-07 12:41:33 -0700984 }
985
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -0700986 iconMime.animate().cancel();
987 iconThumb.animate().cancel();
988
Jeff Sharkeyaed873d2013-09-09 16:51:06 -0700989 final boolean supportsThumbnail = (docFlags & Document.FLAG_SUPPORTS_THUMBNAIL) != 0;
Jeff Sharkeyd10f0492013-09-09 17:35:46 -0700990 final boolean allowThumbnail = (state.derivedMode == MODE_GRID)
Jeff Sharkey9656a532013-09-13 13:42:19 -0700991 || MimePredicate.mimeMatches(MimePredicate.VISUAL_MIMES, docMimeType);
Jeff Sharkeyf63b7772013-10-01 17:57:41 -0700992 final boolean showThumbnail = supportsThumbnail && allowThumbnail && !mSvelteRecents;
Jeff Sharkeyaed873d2013-09-09 16:51:06 -0700993
Jeff Sharkey7e544612014-08-29 15:38:27 -0700994 final boolean enabled = isDocumentEnabled(docMimeType, docFlags);
995 final float iconAlpha = (state.derivedMode == MODE_LIST && !enabled) ? 0.5f : 1f;
996
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -0700997 boolean cacheHit = false;
Jeff Sharkey9656a532013-09-13 13:42:19 -0700998 if (showThumbnail) {
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700999 final Uri uri = DocumentsContract.buildDocumentUri(docAuthority, docId);
Jeff Sharkeyac9e6272013-08-31 21:27:44 -07001000 final Bitmap cachedResult = thumbs.get(uri);
Jeff Sharkey8a8fb672013-05-07 12:41:33 -07001001 if (cachedResult != null) {
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -07001002 iconThumb.setImageBitmap(cachedResult);
1003 cacheHit = true;
Jeff Sharkey8a8fb672013-05-07 12:41:33 -07001004 } else {
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -07001005 iconThumb.setImageDrawable(null);
Steve McKayef280152015-06-11 10:10:49 -07001006 // TODO: Hang this off DocumentHolder?
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -07001007 final ThumbnailAsyncTask task = new ThumbnailAsyncTask(
Jeff Sharkey7e544612014-08-29 15:38:27 -07001008 uri, iconMime, iconThumb, mThumbSize, iconAlpha);
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -07001009 iconThumb.setTag(task);
Jeff Sharkey753a3ae2013-10-22 17:09:44 -07001010 ProviderExecutor.forAuthority(docAuthority).execute(task);
Jeff Sharkey8a8fb672013-05-07 12:41:33 -07001011 }
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -07001012 }
1013
1014 // Always throw MIME icon into place, even when a thumbnail is being
1015 // loaded in background.
1016 if (cacheHit) {
1017 iconMime.setAlpha(0f);
Jeff Sharkey9dd02622013-09-27 16:44:11 -07001018 iconMime.setImageDrawable(null);
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -07001019 iconThumb.setAlpha(1f);
Jeff Sharkeye22d02e2013-04-26 16:54:55 -07001020 } else {
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -07001021 iconMime.setAlpha(1f);
1022 iconThumb.setAlpha(0f);
Jeff Sharkey9dd02622013-09-27 16:44:11 -07001023 iconThumb.setImageDrawable(null);
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001024 iconMime.setImageDrawable(
Steve McKayef280152015-06-11 10:10:49 -07001025 getDocumentIcon(mContext, docAuthority, docId, docMimeType, docIcon, state));
Jeff Sharkeye22d02e2013-04-26 16:54:55 -07001026 }
1027
Jeff Sharkey9656a532013-09-13 13:42:19 -07001028 boolean hasLine1 = false;
Jeff Sharkey42d26792013-09-06 13:22:09 -07001029 boolean hasLine2 = false;
1030
Jeff Sharkey9656a532013-09-13 13:42:19 -07001031 final boolean hideTitle = (state.derivedMode == MODE_GRID) && mHideGridTitles;
1032 if (!hideTitle) {
1033 title.setText(docDisplayName);
1034 hasLine1 = true;
1035 }
1036
1037 Drawable iconDrawable = null;
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -07001038 if (mType == TYPE_RECENT_OPEN) {
Jeff Sharkey8b997042013-09-19 15:25:56 -07001039 // We've already had to enumerate roots before any results can
1040 // be shown, so this will never block.
1041 final RootInfo root = roots.getRootBlocking(docAuthority, docRootId);
Jeff Sharkey93cdbc22014-07-29 17:33:36 -07001042 if (state.derivedMode == MODE_GRID) {
Steve McKayef280152015-06-11 10:10:49 -07001043 iconDrawable = root.loadGridIcon(mContext);
Jeff Sharkey93cdbc22014-07-29 17:33:36 -07001044 } else {
Steve McKayef280152015-06-11 10:10:49 -07001045 iconDrawable = root.loadIcon(mContext);
Jeff Sharkey93cdbc22014-07-29 17:33:36 -07001046 }
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -07001047
Jeff Sharkey7d58fc62013-09-12 16:25:02 -07001048 if (summary != null) {
1049 final boolean alwaysShowSummary = getResources()
1050 .getBoolean(R.bool.always_show_summary);
1051 if (alwaysShowSummary) {
1052 summary.setText(root.getDirectoryString());
1053 summary.setVisibility(View.VISIBLE);
1054 hasLine2 = true;
1055 } else {
Jeff Sharkey8b997042013-09-19 15:25:56 -07001056 if (iconDrawable != null && roots.isIconUniqueBlocking(root)) {
Jeff Sharkey7d58fc62013-09-12 16:25:02 -07001057 // No summary needed if icon speaks for itself
1058 summary.setVisibility(View.INVISIBLE);
1059 } else {
1060 summary.setText(root.getDirectoryString());
1061 summary.setVisibility(View.VISIBLE);
1062 summary.setTextAlignment(TextView.TEXT_ALIGNMENT_TEXT_END);
1063 hasLine2 = true;
1064 }
1065 }
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -07001066 }
Jeff Sharkeyac9e6272013-08-31 21:27:44 -07001067 } else {
Jeff Sharkey9656a532013-09-13 13:42:19 -07001068 // Directories showing thumbnails in grid mode get a little icon
1069 // hint to remind user they're a directory.
1070 if (Document.MIME_TYPE_DIR.equals(docMimeType) && state.derivedMode == MODE_GRID
1071 && showThumbnail) {
Steve McKayef280152015-06-11 10:10:49 -07001072 iconDrawable = IconUtils.applyTintAttr(mContext, R.drawable.ic_doc_folder,
Jeff Sharkey34c54092014-08-08 13:08:56 -07001073 android.R.attr.textColorPrimaryInverse);
Jeff Sharkey9656a532013-09-13 13:42:19 -07001074 }
1075
Jeff Sharkey7d58fc62013-09-12 16:25:02 -07001076 if (summary != null) {
1077 if (docSummary != null) {
1078 summary.setText(docSummary);
1079 summary.setVisibility(View.VISIBLE);
1080 hasLine2 = true;
1081 } else {
1082 summary.setVisibility(View.INVISIBLE);
1083 }
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -07001084 }
Jeff Sharkey2e694f82013-08-06 16:26:14 -07001085 }
1086
Jeff Sharkey9656a532013-09-13 13:42:19 -07001087 if (icon1 != null) icon1.setVisibility(View.GONE);
1088 if (icon2 != null) icon2.setVisibility(View.GONE);
1089
1090 if (iconDrawable != null) {
1091 if (hasLine1) {
1092 icon1.setVisibility(View.VISIBLE);
1093 icon1.setImageDrawable(iconDrawable);
1094 } else {
1095 icon2.setVisibility(View.VISIBLE);
1096 icon2.setImageDrawable(iconDrawable);
1097 }
1098 }
1099
Jeff Sharkeyac9e6272013-08-31 21:27:44 -07001100 if (docLastModified == -1) {
Jeff Sharkey8a8fb672013-05-07 12:41:33 -07001101 date.setText(null);
1102 } else {
Steve McKayef280152015-06-11 10:10:49 -07001103 date.setText(formatTime(mContext, docLastModified));
Jeff Sharkey42d26792013-09-06 13:22:09 -07001104 hasLine2 = true;
Jeff Sharkey8a8fb672013-05-07 12:41:33 -07001105 }
Jeff Sharkey2e694f82013-08-06 16:26:14 -07001106
1107 if (state.showSize) {
1108 size.setVisibility(View.VISIBLE);
Jeff Sharkeyac9e6272013-08-31 21:27:44 -07001109 if (Document.MIME_TYPE_DIR.equals(docMimeType) || docSize == -1) {
Jeff Sharkey2e694f82013-08-06 16:26:14 -07001110 size.setText(null);
1111 } else {
Steve McKayef280152015-06-11 10:10:49 -07001112 size.setText(Formatter.formatFileSize(mContext, docSize));
Jeff Sharkey42d26792013-09-06 13:22:09 -07001113 hasLine2 = true;
Jeff Sharkey2e694f82013-08-06 16:26:14 -07001114 }
1115 } else {
1116 size.setVisibility(View.GONE);
Jeff Sharkey09c10bf2013-06-30 20:02:59 -07001117 }
Jeff Sharkeya5defe32013-08-05 17:56:48 -07001118
Jeff Sharkey9656a532013-09-13 13:42:19 -07001119 if (line1 != null) {
1120 line1.setVisibility(hasLine1 ? View.VISIBLE : View.GONE);
1121 }
Jeff Sharkey7d58fc62013-09-12 16:25:02 -07001122 if (line2 != null) {
1123 line2.setVisibility(hasLine2 ? View.VISIBLE : View.GONE);
1124 }
Jeff Sharkey42d26792013-09-06 13:22:09 -07001125
Steve McKayef280152015-06-11 10:10:49 -07001126 setEnabledRecursive(itemView, enabled);
Jeff Sharkey7e544612014-08-29 15:38:27 -07001127
1128 iconMime.setAlpha(iconAlpha);
1129 iconThumb.setAlpha(iconAlpha);
1130 if (icon1 != null) icon1.setAlpha(iconAlpha);
1131 if (icon2 != null) icon2.setAlpha(iconAlpha);
Jeff Sharkeyaed873d2013-09-09 16:51:06 -07001132
Steve McKay8e258c62015-05-06 14:27:57 -07001133 if (DEBUG_ENABLE_DND) {
Steve McKayef280152015-06-11 10:10:49 -07001134 setupDragAndDropOnDocumentView(itemView, cursor);
Steve McKay8e258c62015-05-06 14:27:57 -07001135 }
Jeff Sharkeye22d02e2013-04-26 16:54:55 -07001136 }
Jeff Sharkey09c10bf2013-06-30 20:02:59 -07001137
Steve McKay351a7492015-08-04 10:11:01 -07001138 @Override
Steve McKayef280152015-06-11 10:10:49 -07001139 public int getItemCount() {
Ben Kwa24be5d32015-08-27 16:04:46 -07001140 return mModel.getItemCount();
Jeff Sharkeyaed873d2013-09-09 16:51:06 -07001141 }
1142
Jeff Sharkey8a8fb672013-05-07 12:41:33 -07001143 }
1144
1145 private static String formatTime(Context context, long when) {
1146 // TODO: DateUtils should make this easier
1147 Time then = new Time();
1148 then.set(when);
1149 Time now = new Time();
1150 now.setToNow();
1151
1152 int flags = DateUtils.FORMAT_NO_NOON | DateUtils.FORMAT_NO_MIDNIGHT
1153 | DateUtils.FORMAT_ABBREV_ALL;
1154
1155 if (then.year != now.year) {
1156 flags |= DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE;
1157 } else if (then.yearDay != now.yearDay) {
1158 flags |= DateUtils.FORMAT_SHOW_DATE;
1159 } else {
1160 flags |= DateUtils.FORMAT_SHOW_TIME;
1161 }
1162
1163 return DateUtils.formatDateTime(context, when, flags);
1164 }
Jeff Sharkeyb3620442013-09-01 18:41:04 -07001165
1166 private String findCommonMimeType(List<String> mimeTypes) {
1167 String[] commonType = mimeTypes.get(0).split("/");
1168 if (commonType.length != 2) {
1169 return "*/*";
1170 }
1171
1172 for (int i = 1; i < mimeTypes.size(); i++) {
1173 String[] type = mimeTypes.get(i).split("/");
1174 if (type.length != 2) continue;
1175
1176 if (!commonType[1].equals(type[1])) {
1177 commonType[1] = "*";
1178 }
1179
1180 if (!commonType[0].equals(type[0])) {
1181 commonType[0] = "*";
1182 commonType[1] = "*";
1183 break;
1184 }
1185 }
1186
1187 return commonType[0] + "/" + commonType[1];
1188 }
Jeff Sharkeyaed873d2013-09-09 16:51:06 -07001189
1190 private void setEnabledRecursive(View v, boolean enabled) {
Jeff Sharkey9656a532013-09-13 13:42:19 -07001191 if (v == null) return;
Jeff Sharkeyaed873d2013-09-09 16:51:06 -07001192 if (v.isEnabled() == enabled) return;
1193 v.setEnabled(enabled);
1194
1195 if (v instanceof ViewGroup) {
1196 final ViewGroup vg = (ViewGroup) v;
1197 for (int i = vg.getChildCount() - 1; i >= 0; i--) {
1198 setEnabledRecursive(vg.getChildAt(i), enabled);
1199 }
1200 }
1201 }
Jeff Sharkey7cf49032013-09-26 10:54:16 -07001202
1203 private boolean isDocumentEnabled(String docMimeType, int docFlags) {
1204 final State state = getDisplayState(DirectoryFragment.this);
1205
Jeff Sharkey7cf49032013-09-26 10:54:16 -07001206 // Directories are always enabled
1207 if (Document.MIME_TYPE_DIR.equals(docMimeType)) {
1208 return true;
1209 }
1210
Jeff Sharkey783ebc22013-09-26 19:42:52 -07001211 // Read-only files are disabled when creating
1212 if (state.action == ACTION_CREATE && (docFlags & Document.FLAG_SUPPORTS_WRITE) == 0) {
1213 return false;
1214 }
1215
Jeff Sharkey7cf49032013-09-26 10:54:16 -07001216 return MimePredicate.mimeMatches(state.acceptMimes, docMimeType);
1217 }
Vladislav Kaznacheev89b90332015-05-01 13:46:57 -07001218
Steve McKay1f199482015-05-20 15:58:42 -07001219 private void copyFromClipboard() {
1220 new AsyncTask<Void, Void, List<DocumentInfo>>() {
1221
1222 @Override
1223 protected List<DocumentInfo> doInBackground(Void... params) {
1224 return mClipper.getClippedDocuments();
1225 }
1226
1227 @Override
1228 protected void onPostExecute(List<DocumentInfo> docs) {
1229 DocumentInfo destination =
1230 ((BaseActivity) getActivity()).getCurrentDirectory();
1231 copyDocuments(docs, destination);
1232 }
1233 }.execute();
Steve McKay0599a442015-05-05 14:50:00 -07001234 }
1235
Steve McKay1f199482015-05-20 15:58:42 -07001236 private void copyFromClipData(final ClipData clipData, final DocumentInfo destination) {
Steve McKayef280152015-06-11 10:10:49 -07001237 checkNotNull(clipData);
Steve McKay1f199482015-05-20 15:58:42 -07001238 new AsyncTask<Void, Void, List<DocumentInfo>>() {
Vladislav Kaznacheev89b90332015-05-01 13:46:57 -07001239
Steve McKay1f199482015-05-20 15:58:42 -07001240 @Override
1241 protected List<DocumentInfo> doInBackground(Void... params) {
1242 return mClipper.getDocumentsFromClipData(clipData);
1243 }
1244
1245 @Override
1246 protected void onPostExecute(List<DocumentInfo> docs) {
1247 copyDocuments(docs, destination);
1248 }
1249 }.execute();
1250 }
1251
1252 private void copyDocuments(final List<DocumentInfo> docs, final DocumentInfo destination) {
1253 if (!canCopy(docs, destination)) {
1254 Toast.makeText(
1255 getActivity(),
1256 R.string.clipboard_files_cannot_paste, Toast.LENGTH_SHORT).show();
Steve McKay0599a442015-05-05 14:50:00 -07001257 return;
1258 }
1259
Steve McKay1f199482015-05-20 15:58:42 -07001260 if (docs.isEmpty()) {
Vladislav Kaznacheev89b90332015-05-01 13:46:57 -07001261 return;
Vladislav Kaznacheeve16887a2015-05-05 12:09:47 -07001262 }
Vladislav Kaznacheev89b90332015-05-01 13:46:57 -07001263
Steve McKay1f199482015-05-20 15:58:42 -07001264 final DocumentStack curStack = getDisplayState(DirectoryFragment.this).stack;
Vladislav Kaznacheev89b90332015-05-01 13:46:57 -07001265 DocumentStack tmpStack = new DocumentStack();
Steve McKay1f199482015-05-20 15:58:42 -07001266 if (destination != null) {
1267 tmpStack.push(destination);
Vladislav Kaznacheev89b90332015-05-01 13:46:57 -07001268 tmpStack.addAll(curStack);
1269 } else {
1270 tmpStack = curStack;
1271 }
1272
Steve McKay1f199482015-05-20 15:58:42 -07001273 CopyService.start(getActivity(), docs, tmpStack, CopyService.TRANSFER_MODE_COPY);
Vladislav Kaznacheev89b90332015-05-01 13:46:57 -07001274 }
1275
1276 private ClipData getClipDataFromDocuments(List<DocumentInfo> docs) {
1277 Context context = getActivity();
1278 final ContentResolver resolver = context.getContentResolver();
1279 ClipData clipData = null;
1280 for (DocumentInfo doc : docs) {
1281 final Uri uri = DocumentsContract.buildDocumentUri(doc.authority, doc.documentId);
1282 if (clipData == null) {
Vladislav Kaznacheeve16887a2015-05-05 12:09:47 -07001283 // TODO: figure out what this string should be.
1284 // Currently it is not displayed anywhere in the UI, but this might change.
1285 final String label = "";
1286 clipData = ClipData.newUri(resolver, label, uri);
Vladislav Kaznacheev89b90332015-05-01 13:46:57 -07001287 } else {
1288 // TODO: update list of mime types in ClipData.
1289 clipData.addItem(new ClipData.Item(uri));
1290 }
1291 }
1292 return clipData;
1293 }
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001294
Steve McKay1f199482015-05-20 15:58:42 -07001295 void copySelectedToClipboard() {
Ben Kwa24be5d32015-08-27 16:04:46 -07001296 Selection sel = mModel.getSelection(new Selection());
Steve McKayef280152015-06-11 10:10:49 -07001297 copySelectionToClipboard(sel);
Steve McKay9276f3b2015-05-27 16:11:42 -07001298 }
Steve McKay0599a442015-05-05 14:50:00 -07001299
Steve McKayef280152015-06-11 10:10:49 -07001300 void copySelectionToClipboard(Selection items) {
Steve McKay9276f3b2015-05-27 16:11:42 -07001301 new GetDocumentsTask() {
1302 @Override
1303 void onDocumentsReady(List<DocumentInfo> docs) {
1304 mClipper.clipDocuments(docs);
Steve McKay1f199482015-05-20 15:58:42 -07001305 Activity activity = getActivity();
1306 Toast.makeText(activity,
1307 activity.getResources().getQuantityString(
1308 R.plurals.clipboard_files_clipped, docs.size(), docs.size()),
1309 Toast.LENGTH_SHORT).show();
Steve McKay9276f3b2015-05-27 16:11:42 -07001310 }
Steve McKayef280152015-06-11 10:10:49 -07001311 }.execute(items);
Steve McKay0599a442015-05-05 14:50:00 -07001312 }
1313
1314 void pasteFromClipboard() {
Steve McKay1f199482015-05-20 15:58:42 -07001315 copyFromClipboard();
1316 getActivity().invalidateOptionsMenu();
Steve McKay0599a442015-05-05 14:50:00 -07001317 }
1318
Steve McKay0599a442015-05-05 14:50:00 -07001319 /**
1320 * Returns true if the list of files can be copied to destination. Note that this
1321 * is a policy check only. Currently the method does not attempt to verify
1322 * available space or any other environmental aspects possibly resulting in
1323 * failure to copy.
1324 *
1325 * @return true if the list of files can be copied to destination.
1326 */
1327 boolean canCopy(List<DocumentInfo> files, DocumentInfo dest) {
Ben Kwa91923182015-08-27 16:06:33 -07001328 BaseActivity activity = (BaseActivity) getActivity();
Steve McKay0599a442015-05-05 14:50:00 -07001329
1330 final RootInfo root = activity.getCurrentRoot();
1331
1332 // Can't copy folders to Downloads.
1333 if (root.isDownloads()) {
1334 for (DocumentInfo docs : files) {
1335 if (docs.isDirectory()) {
1336 return false;
1337 }
1338 }
1339 }
1340
1341 return dest != null && dest.isDirectory() && dest.isCreateSupported();
1342 }
1343
1344 void selectAllFiles() {
Ben Kwa24be5d32015-08-27 16:04:46 -07001345 boolean changed = mModel.selectAll();
Steve McKay9459a7c2015-07-24 13:14:20 -07001346 if (changed) {
1347 updateDisplayState();
1348 }
Steve McKay0599a442015-05-05 14:50:00 -07001349 }
1350
Steve McKayef280152015-06-11 10:10:49 -07001351 private void setupDragAndDropOnDirectoryView(View view) {
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001352 // Listen for drops on non-directory items and empty space.
1353 view.setOnDragListener(mOnDragListener);
1354 }
1355
1356 private void setupDragAndDropOnDocumentView(View view, Cursor cursor) {
1357 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
1358 if (Document.MIME_TYPE_DIR.equals(docMimeType)) {
1359 // Make a directory item a drop target. Drop on non-directories and empty space
1360 // is handled at the list/grid view level.
1361 view.setOnDragListener(mOnDragListener);
1362 }
1363
1364 // Temporary: attaching the listener to the title only.
1365 // Attaching to the entire item conflicts with the item long click handler responsible
1366 // for item selection.
1367 final View title = view.findViewById(android.R.id.title);
1368 title.setOnLongClickListener(mLongClickListener);
1369 }
1370
1371 private View.OnDragListener mOnDragListener = new View.OnDragListener() {
1372 @Override
1373 public boolean onDrag(View v, DragEvent event) {
1374 switch (event.getAction()) {
1375 case DragEvent.ACTION_DRAG_STARTED:
1376 // TODO: Check if the event contains droppable data.
1377 return true;
1378
1379 // TODO: Highlight potential drop target directory?
1380 // TODO: Expand drop target directory on hover?
1381 case DragEvent.ACTION_DRAG_ENTERED:
1382 case DragEvent.ACTION_DRAG_LOCATION:
1383 case DragEvent.ACTION_DRAG_EXITED:
1384 case DragEvent.ACTION_DRAG_ENDED:
1385 return true;
1386
1387 case DragEvent.ACTION_DROP:
Vladislav Kaznacheev9400b892015-09-04 09:17:37 -07001388 int dstPosition = mRecView.getChildAdapterPosition(getContainingItemView(v));
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001389 DocumentInfo dstDir = null;
1390 if (dstPosition != android.widget.AdapterView.INVALID_POSITION) {
Ben Kwa24be5d32015-08-27 16:04:46 -07001391 Cursor dstCursor = mModel.getItem(dstPosition);
Steve McKayef280152015-06-11 10:10:49 -07001392 checkNotNull(dstCursor, "Cursor cannot be null.");
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001393 dstDir = DocumentInfo.fromDirectoryCursor(dstCursor);
1394 // TODO: Do not drop into the directory where the documents came from.
1395 }
1396 copyFromClipData(event.getClipData(), dstDir);
1397 return true;
1398 }
1399 return false;
1400 }
1401 };
1402
Vladislav Kaznacheev9400b892015-09-04 09:17:37 -07001403 private View getContainingItemView(View view) {
1404 while (true) {
1405 if (view.getLayoutParams() instanceof RecyclerView.LayoutParams) {
1406 return view;
1407 }
1408 ViewParent parent = view.getParent();
1409 if (parent == null || !(parent instanceof View)) {
1410 return null;
1411 }
1412 view = (View) parent;
1413 }
1414 }
1415
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001416 private View.OnLongClickListener mLongClickListener = new View.OnLongClickListener() {
1417 @Override
1418 public boolean onLongClick(View v) {
1419 final List<DocumentInfo> docs = getDraggableDocuments(v);
1420 if (docs.isEmpty()) {
1421 return false;
1422 }
1423 v.startDrag(
1424 getClipDataFromDocuments(docs),
1425 new DrawableShadowBuilder(getDragShadowIcon(docs)),
1426 null,
1427 View.DRAG_FLAG_GLOBAL
1428 );
1429 return true;
1430 }
1431 };
1432
1433 private List<DocumentInfo> getDraggableDocuments(View currentItemView) {
Vladislav Kaznacheev9400b892015-09-04 09:17:37 -07001434 int position = mRecView.getChildAdapterPosition(getContainingItemView(currentItemView));
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001435 if (position == android.widget.AdapterView.INVALID_POSITION) {
1436 return Collections.EMPTY_LIST;
1437 }
1438
Ben Kwa24be5d32015-08-27 16:04:46 -07001439 final List<DocumentInfo> selectedDocs = mModel.getSelectedDocuments();
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001440 if (!selectedDocs.isEmpty()) {
Ben Kwa24be5d32015-08-27 16:04:46 -07001441 if (!mModel.isSelected(position)) {
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001442 // There is a selection that does not include the current item, drag nothing.
1443 return Collections.EMPTY_LIST;
1444 }
1445 return selectedDocs;
1446 }
1447
Ben Kwa24be5d32015-08-27 16:04:46 -07001448 final Cursor cursor = mModel.getItem(position);
Steve McKayef280152015-06-11 10:10:49 -07001449 checkNotNull(cursor, "Cursor cannot be null.");
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001450 final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
Steve McKayfefcd702015-08-20 16:19:38 +00001451
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001452 return Lists.newArrayList(doc);
1453 }
1454
1455 private Drawable getDragShadowIcon(List<DocumentInfo> docs) {
1456 if (docs.size() == 1) {
1457 final DocumentInfo doc = docs.get(0);
1458 return getDocumentIcon(getActivity(), doc.authority, doc.documentId,
1459 doc.mimeType, doc.icon, getDisplayState(this));
1460 }
1461 return getActivity().getDrawable(R.drawable.ic_doc_generic);
1462 }
1463
1464 public static Drawable getDocumentIcon(Context context, String docAuthority, String docId,
1465 String docMimeType, int docIcon, State state) {
1466 if (docIcon != 0) {
1467 return IconUtils.loadPackageIcon(context, docAuthority, docIcon);
1468 } else {
1469 return IconUtils.loadMimeIcon(context, docMimeType, docAuthority, docId,
1470 state.derivedMode);
1471 }
1472 }
1473
Steve McKayef280152015-06-11 10:10:49 -07001474 private static class ThumbnailAsyncTask extends AsyncTask<Uri, Void, Bitmap>
1475 implements Preemptable {
1476 private final Uri mUri;
1477 private final ImageView mIconMime;
1478 private final ImageView mIconThumb;
1479 private final Point mThumbSize;
1480 private final float mTargetAlpha;
1481 private final CancellationSignal mSignal;
1482
1483 public ThumbnailAsyncTask(Uri uri, ImageView iconMime, ImageView iconThumb, Point thumbSize,
1484 float targetAlpha) {
1485 mUri = uri;
1486 mIconMime = iconMime;
1487 mIconThumb = iconThumb;
1488 mThumbSize = thumbSize;
1489 mTargetAlpha = targetAlpha;
1490 mSignal = new CancellationSignal();
1491 }
1492
1493 @Override
1494 public void preempt() {
1495 cancel(false);
1496 mSignal.cancel();
1497 }
1498
1499 @Override
1500 protected Bitmap doInBackground(Uri... params) {
1501 if (isCancelled()) return null;
1502
1503 final Context context = mIconThumb.getContext();
1504 final ContentResolver resolver = context.getContentResolver();
1505
1506 ContentProviderClient client = null;
1507 Bitmap result = null;
1508 try {
1509 client = DocumentsApplication.acquireUnstableProviderOrThrow(
1510 resolver, mUri.getAuthority());
1511 result = DocumentsContract.getDocumentThumbnail(client, mUri, mThumbSize, mSignal);
1512 if (result != null) {
1513 final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache(
1514 context, mThumbSize);
1515 thumbs.put(mUri, result);
1516 }
1517 } catch (Exception e) {
1518 if (!(e instanceof OperationCanceledException)) {
1519 Log.w(TAG, "Failed to load thumbnail for " + mUri + ": " + e);
1520 }
1521 } finally {
1522 ContentProviderClient.releaseQuietly(client);
1523 }
1524 return result;
1525 }
1526
1527 @Override
1528 protected void onPostExecute(Bitmap result) {
1529 if (mIconThumb.getTag() == this && result != null) {
1530 mIconThumb.setTag(null);
1531 mIconThumb.setImageBitmap(result);
1532
1533 mIconMime.setAlpha(mTargetAlpha);
1534 mIconMime.animate().alpha(0f).start();
1535 mIconThumb.setAlpha(0f);
1536 mIconThumb.animate().alpha(mTargetAlpha).start();
1537 }
1538 }
1539 }
1540
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001541 private class DrawableShadowBuilder extends View.DragShadowBuilder {
1542
1543 private final Drawable mShadow;
1544
1545 private final int mShadowDimension;
1546
1547 public DrawableShadowBuilder(Drawable shadow) {
1548 mShadow = shadow;
1549 mShadowDimension = getResources().getDimensionPixelSize(
1550 R.dimen.drag_shadow_size);
1551 mShadow.setBounds(0, 0, mShadowDimension, mShadowDimension);
1552 }
1553
Ben Kwa24be5d32015-08-27 16:04:46 -07001554 @Override
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001555 public void onProvideShadowMetrics(
1556 Point shadowSize, Point shadowTouchPoint) {
1557 shadowSize.set(mShadowDimension, mShadowDimension);
1558 shadowTouchPoint.set(mShadowDimension / 2, mShadowDimension / 2);
1559 }
1560
Ben Kwa24be5d32015-08-27 16:04:46 -07001561 @Override
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001562 public void onDrawShadow(Canvas canvas) {
1563 mShadow.draw(canvas);
1564 }
1565 }
Steve McKay1f199482015-05-20 15:58:42 -07001566
1567 private FragmentTuner pickFragmentTuner(final State state) {
1568 return state.action == ACTION_BROWSE_ALL
Steve McKay0fbfc652015-08-20 16:48:49 -07001569 ? new FilesTuner()
Steve McKay1f199482015-05-20 15:58:42 -07001570 : new DefaultTuner(state);
1571 }
1572
1573 /**
1574 * Interface for specializing the Fragment for the "host" Activity.
1575 * Feel free to expand the role of this class to handle other specializations.
1576 */
1577 private interface FragmentTuner {
Steve McKay4f4232d2015-07-22 12:13:46 -07001578 void updateActionMenu(Menu menu, int dirType, boolean canDelete);
Kyle Horimoto426bd0d2015-07-29 15:33:49 -07001579 void afterActivityCreated(DirectoryFragment fragment);
Steve McKay1f199482015-05-20 15:58:42 -07001580 }
1581
1582 /**
Steve McKay9276f3b2015-05-27 16:11:42 -07001583 * Abstract task providing support for loading documents *off*
1584 * the main thread. And if it isn't obvious, creating a list
1585 * of documents (especially large lists) can be pretty expensive.
1586 */
1587 private abstract class GetDocumentsTask
Steve McKayef280152015-06-11 10:10:49 -07001588 extends AsyncTask<Selection, Void, List<DocumentInfo>> {
Steve McKay9276f3b2015-05-27 16:11:42 -07001589 @Override
Steve McKayef280152015-06-11 10:10:49 -07001590 protected final List<DocumentInfo> doInBackground(Selection... selected) {
Ben Kwa24be5d32015-08-27 16:04:46 -07001591 return mModel.getDocuments(selected[0]);
Steve McKay9276f3b2015-05-27 16:11:42 -07001592 }
1593
1594 @Override
1595 protected final void onPostExecute(List<DocumentInfo> docs) {
1596 onDocumentsReady(docs);
1597 }
1598
1599 abstract void onDocumentsReady(List<DocumentInfo> docs);
1600 }
1601
1602 /**
Steve McKay1f199482015-05-20 15:58:42 -07001603 * Provides support for Platform specific specializations of DirectoryFragment.
1604 */
1605 private static final class DefaultTuner implements FragmentTuner {
1606
1607 private final State mState;
1608
1609 public DefaultTuner(State state) {
1610 mState = state;
1611 }
1612
1613 @Override
Steve McKay4f4232d2015-07-22 12:13:46 -07001614 public void updateActionMenu(Menu menu, int dirType, boolean canDelete) {
Steve McKay1f199482015-05-20 15:58:42 -07001615 Preconditions.checkState(mState.action != ACTION_BROWSE_ALL);
1616
1617 final MenuItem open = menu.findItem(R.id.menu_open);
1618 final MenuItem share = menu.findItem(R.id.menu_share);
1619 final MenuItem delete = menu.findItem(R.id.menu_delete);
1620 final MenuItem copyTo = menu.findItem(R.id.menu_copy_to);
1621 final MenuItem moveTo = menu.findItem(R.id.menu_move_to);
1622 final MenuItem copyToClipboard = menu.findItem(R.id.menu_copy_to_clipboard);
1623
1624 final boolean manageOrBrowse = (mState.action == ACTION_MANAGE
1625 || mState.action == ACTION_BROWSE);
1626
1627 open.setVisible(!manageOrBrowse);
1628 share.setVisible(manageOrBrowse);
Steve McKay4f4232d2015-07-22 12:13:46 -07001629 delete.setVisible(manageOrBrowse && canDelete);
Steve McKay1f199482015-05-20 15:58:42 -07001630 // Disable copying from the Recents view.
1631 copyTo.setVisible(manageOrBrowse && dirType != TYPE_RECENT_OPEN);
1632 moveTo.setVisible(SystemProperties.getBoolean("debug.documentsui.enable_move", false));
1633
Steve McKay0fbfc652015-08-20 16:48:49 -07001634 // Only shown in files mode.
Steve McKay1f199482015-05-20 15:58:42 -07001635 copyToClipboard.setVisible(false);
1636 }
Kyle Horimoto426bd0d2015-07-29 15:33:49 -07001637
1638 @Override
1639 public void afterActivityCreated(DirectoryFragment fragment) {}
Steve McKay1f199482015-05-20 15:58:42 -07001640 }
1641
1642 /**
Steve McKay0fbfc652015-08-20 16:48:49 -07001643 * Provides support for Files activity specific specializations of DirectoryFragment.
Steve McKay1f199482015-05-20 15:58:42 -07001644 */
Steve McKay0fbfc652015-08-20 16:48:49 -07001645 private static final class FilesTuner implements FragmentTuner {
Steve McKay1f199482015-05-20 15:58:42 -07001646 @Override
Steve McKay4f4232d2015-07-22 12:13:46 -07001647 public void updateActionMenu(Menu menu, int dirType, boolean canDelete) {
Steve McKay1f199482015-05-20 15:58:42 -07001648 menu.findItem(R.id.menu_share).setVisible(true);
Steve McKay4f4232d2015-07-22 12:13:46 -07001649 menu.findItem(R.id.menu_delete).setVisible(canDelete);
Steve McKay1f199482015-05-20 15:58:42 -07001650 menu.findItem(R.id.menu_copy_to_clipboard).setVisible(true);
1651
1652 menu.findItem(R.id.menu_open).setVisible(false);
1653 menu.findItem(R.id.menu_copy_to).setVisible(false);
1654 menu.findItem(R.id.menu_move_to).setVisible(false);
1655 }
Kyle Horimoto426bd0d2015-07-29 15:33:49 -07001656
1657 @Override
Kyle Horimoto62a7fd02015-08-18 13:25:29 -07001658 public void afterActivityCreated(DirectoryFragment fragment) {}
Steve McKay1f199482015-05-20 15:58:42 -07001659 }
Ben Kwa24be5d32015-08-27 16:04:46 -07001660
1661 /**
1662 * The data model for the current loaded directory.
1663 */
Ben Kwa7461a952015-09-01 11:03:01 -07001664 @VisibleForTesting
1665 public static final class Model implements DocumentContext {
Ben Kwa24be5d32015-08-27 16:04:46 -07001666 private MultiSelectManager mSelectionManager;
Ben Kwa7461a952015-09-01 11:03:01 -07001667 private Context mContext;
Ben Kwa24be5d32015-08-27 16:04:46 -07001668 private int mCursorCount;
1669 private boolean mIsLoading;
Ben Kwa7461a952015-09-01 11:03:01 -07001670 private SparseBooleanArray mMarkedForDeletion = new SparseBooleanArray();
1671 private UpdateListener mUpdateListener;
Ben Kwa24be5d32015-08-27 16:04:46 -07001672 @Nullable private Cursor mCursor;
1673 @Nullable private String info;
1674 @Nullable private String error;
Ben Kwa7461a952015-09-01 11:03:01 -07001675
1676 Model(Context context, MultiSelectManager selectionManager) {
1677 mContext = context;
1678 mSelectionManager = selectionManager;
1679 }
Ben Kwa24be5d32015-08-27 16:04:46 -07001680
1681 /**
1682 * Sets the selection manager used by the model.
1683 * TODO: the model should instantiate the selection manager. See onActivityCreated.
1684 */
1685 void setSelectionManager(MultiSelectManager mgr) {
1686 mSelectionManager = mgr;
1687 }
1688
1689 /**
1690 * Selects all files in the current directory.
1691 * @return true if the selection state changed for any files.
1692 */
1693 boolean selectAll() {
1694 return mSelectionManager.setItemsSelected(0, mCursorCount, true);
1695 }
1696
1697 /**
1698 * Clones the current selection into the given Selection object.
1699 * @param selection
1700 * @return The selection that was passed in, for convenience.
1701 */
1702 Selection getSelection(Selection selection) {
1703 return mSelectionManager.getSelection(selection);
1704 }
1705
1706 /**
1707 * @return The current selection (the live instance, not a copy).
1708 */
1709 Selection getSelection() {
1710 return mSelectionManager.getSelection();
1711 }
1712
1713 boolean isSelected(int position) {
1714 return mSelectionManager.getSelection().contains(position);
1715 }
1716
1717 void clearSelection() {
1718 mSelectionManager.clearSelection();
1719 }
1720
1721 void update(DirectoryResult result) {
1722 if (DEBUG) Log.i(TAG, "Updating model with new result set.");
1723
1724 if (result == null) {
1725 mCursor = null;
1726 mCursorCount = 0;
1727 info = null;
1728 error = null;
1729 mIsLoading = false;
Ben Kwa7461a952015-09-01 11:03:01 -07001730 if (mUpdateListener != null) mUpdateListener.onModelUpdate(this);
Ben Kwa24be5d32015-08-27 16:04:46 -07001731 return;
1732 }
1733
1734 if (result.exception != null) {
1735 Log.e(TAG, "Error while loading directory contents", result.exception);
Ben Kwa7461a952015-09-01 11:03:01 -07001736 if (mUpdateListener != null) mUpdateListener.onModelUpdateFailed(result.exception);
Ben Kwa24be5d32015-08-27 16:04:46 -07001737 return;
1738 }
1739
1740 mCursor = result.cursor;
1741 mCursorCount = mCursor.getCount();
1742
1743 final Bundle extras = mCursor.getExtras();
1744 if (extras != null) {
1745 info = extras.getString(DocumentsContract.EXTRA_INFO);
1746 error = extras.getString(DocumentsContract.EXTRA_ERROR);
1747 mIsLoading = extras.getBoolean(DocumentsContract.EXTRA_LOADING, false);
1748 }
Ben Kwa7461a952015-09-01 11:03:01 -07001749
1750 if (mUpdateListener != null) mUpdateListener.onModelUpdate(this);
Ben Kwa24be5d32015-08-27 16:04:46 -07001751 }
1752
Ben Kwa7461a952015-09-01 11:03:01 -07001753 int getItemCount() {
Ben Kwa91923182015-08-27 16:06:33 -07001754 return mCursorCount - mMarkedForDeletion.size();
Ben Kwa24be5d32015-08-27 16:04:46 -07001755 }
1756
Ben Kwa7461a952015-09-01 11:03:01 -07001757 Cursor getItem(int position) {
Ben Kwa91923182015-08-27 16:06:33 -07001758 // Items marked for deletion are masked out of the UI. To do this, for every marked
1759 // item whose position is less than the requested item position, advance the requested
1760 // position by 1.
1761 final int originalPos = position;
1762 final int size = mMarkedForDeletion.size();
1763 for (int i = 0; i <= size; ++i) {
1764 // It'd be more concise, but less efficient, to iterate over positions while calling
1765 // mMarkedForDeletion.get. Instead, iterate over deleted entries.
1766 if (mMarkedForDeletion.keyAt(i) <= position && mMarkedForDeletion.valueAt(i)) {
1767 ++position;
1768 }
1769 }
1770
1771 if (DEBUG) {
1772 Log.d(TAG, "Item position adjusted for deletion. Original: " + originalPos
1773 + " Adjusted: " + position);
1774 }
1775
Ben Kwa24be5d32015-08-27 16:04:46 -07001776 if (position >= mCursorCount) {
1777 throw new IndexOutOfBoundsException("Attempt to retrieve " + position + " of " +
1778 mCursorCount + " items");
1779 }
1780
1781 mCursor.moveToPosition(position);
1782 return mCursor;
1783 }
1784
1785 private boolean isEmpty() {
1786 return mCursorCount == 0;
1787 }
1788
1789 private boolean isLoading() {
1790 return mIsLoading;
1791 }
1792
1793 private List<DocumentInfo> getSelectedDocuments() {
1794 Selection sel = getSelection(new Selection());
1795 return getDocuments(sel);
1796 }
1797
Ben Kwa7461a952015-09-01 11:03:01 -07001798 List<DocumentInfo> getDocuments(Selection items) {
Ben Kwa91923182015-08-27 16:06:33 -07001799 final int size = (items != null) ? items.size() : 0;
Ben Kwa24be5d32015-08-27 16:04:46 -07001800
Ben Kwa91923182015-08-27 16:06:33 -07001801 final List<DocumentInfo> docs = new ArrayList<>(size);
Ben Kwa24be5d32015-08-27 16:04:46 -07001802 for (int i = 0; i < size; i++) {
1803 final Cursor cursor = getItem(items.get(i));
1804 checkNotNull(cursor, "Cursor cannot be null.");
1805 final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
1806 docs.add(doc);
1807 }
1808 return docs;
1809 }
1810
1811 @Override
1812 public Cursor getCursor() {
1813 if (Looper.myLooper() != Looper.getMainLooper()) {
1814 throw new IllegalStateException("Can't call getCursor from non-main thread.");
1815 }
1816 return mCursor;
1817 }
Ben Kwa91923182015-08-27 16:06:33 -07001818
Ben Kwa7461a952015-09-01 11:03:01 -07001819 List<DocumentInfo> getDocumentsMarkedForDeletion() {
Ben Kwa91923182015-08-27 16:06:33 -07001820 final int size = mMarkedForDeletion.size();
1821 List<DocumentInfo> docs = new ArrayList<>(size);
1822
1823 for (int i = 0; i < size; ++i) {
1824 final int position = mMarkedForDeletion.keyAt(i);
1825 checkState(position < mCursorCount);
1826 mCursor.moveToPosition(position);
1827 final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(mCursor);
1828 docs.add(doc);
1829 }
1830 return docs;
1831 }
1832
1833 /**
1834 * Marks the given files for deletion. This will remove them from the UI. Clients must then
1835 * call either {@link #undoDeletion()} or {@link #finalizeDeletion()} to cancel or confirm
1836 * the deletion, respectively. Only one deletion operation is allowed at a time.
1837 *
1838 * @param selected A selection representing the files to delete.
1839 */
Ben Kwa7461a952015-09-01 11:03:01 -07001840 void markForDeletion(Selection selected) {
Ben Kwa91923182015-08-27 16:06:33 -07001841 // Only one deletion operation at a time.
1842 checkState(mMarkedForDeletion.size() == 0);
1843 // There should never be more to delete than what exists.
1844 checkState(mCursorCount >= selected.size());
1845
1846 final int size = selected.size();
1847 for (int i = 0; i < size; ++i) {
1848 int position = selected.get(i);
1849 if (DEBUG) Log.d(TAG, "Marked position " + position + " for deletion");
1850 mMarkedForDeletion.append(position, true);
Ben Kwa7461a952015-09-01 11:03:01 -07001851 if (mUpdateListener != null) mUpdateListener.notifyItemRemoved(position);
Ben Kwa91923182015-08-27 16:06:33 -07001852 }
1853 }
1854
1855 /**
1856 * Cancels an ongoing deletion operation. All files currently marked for deletion will be
1857 * unmarked, and restored in the UI. See {@link #markForDeletion(Selection)}.
1858 */
Ben Kwa7461a952015-09-01 11:03:01 -07001859 void undoDeletion() {
Ben Kwa91923182015-08-27 16:06:33 -07001860 // Iterate over deleted items, temporarily marking them false in the deletion list, and
1861 // re-adding them to the UI.
1862 final int size = mMarkedForDeletion.size();
1863 for (int i = 0; i < size; ++i) {
1864 final int position = mMarkedForDeletion.keyAt(i);
1865 mMarkedForDeletion.put(position, false);
Ben Kwa7461a952015-09-01 11:03:01 -07001866 if (mUpdateListener != null) mUpdateListener.notifyItemInserted(position);
Ben Kwa91923182015-08-27 16:06:33 -07001867 }
1868
1869 // Then, clear the deletion list.
1870 mMarkedForDeletion.clear();
1871 }
1872
1873 /**
1874 * Finalizes an ongoing deletion operation. All files currently marked for deletion will be
1875 * deleted. See {@link #markForDeletion(Selection)}.
Ben Kwa7461a952015-09-01 11:03:01 -07001876 *
1877 * @param view The view which will be used to interact with the user (e.g. surfacing
1878 * snackbars) for errors, info, etc.
Ben Kwa91923182015-08-27 16:06:33 -07001879 */
Ben Kwa7461a952015-09-01 11:03:01 -07001880 void finalizeDeletion(final View view) {
1881 final ContentResolver resolver = mContext.getContentResolver();
1882 DeleteFilesTask task = new DeleteFilesTask(
1883 resolver,
1884 new Runnable() {
1885 @Override
1886 public void run() {
1887 Snackbar.make(
1888 view,
1889 R.string.toast_failed_delete,
1890 Snackbar.LENGTH_LONG)
1891 .show();
1892
1893 }
1894 });
1895 task.execute();
Ben Kwa91923182015-08-27 16:06:33 -07001896 }
1897
1898 /**
1899 * A Task which collects the DocumentInfo for documents that have been marked for deletion,
1900 * and actually deletes them.
1901 */
1902 private class DeleteFilesTask extends AsyncTask<Void, Void, List<DocumentInfo>> {
1903 private ContentResolver mResolver;
Ben Kwa7461a952015-09-01 11:03:01 -07001904 private Runnable mErrorCallback;
Ben Kwa91923182015-08-27 16:06:33 -07001905
Ben Kwa7461a952015-09-01 11:03:01 -07001906 /**
1907 * @param resolver A ContentResolver for performing the actual file deletions.
1908 * @param errorCallback A Runnable that is executed in the event that one or more errors
1909 * occured while copying files. Execution will occur on the UI thread.
1910 */
1911 public DeleteFilesTask(ContentResolver resolver, Runnable errorCallback) {
Ben Kwa91923182015-08-27 16:06:33 -07001912 mResolver = resolver;
Ben Kwa7461a952015-09-01 11:03:01 -07001913 mErrorCallback = errorCallback;
Ben Kwa91923182015-08-27 16:06:33 -07001914 }
1915
1916 @Override
1917 protected List<DocumentInfo> doInBackground(Void... params) {
1918 return getDocumentsMarkedForDeletion();
1919 }
1920
1921 @Override
1922 protected void onPostExecute(List<DocumentInfo> docs) {
1923 boolean hadTrouble = false;
1924 for (DocumentInfo doc : docs) {
1925 if (!doc.isDeleteSupported()) {
1926 Log.w(TAG, doc + " could not be deleted. Skipping...");
1927 hadTrouble = true;
1928 continue;
1929 }
1930
1931 ContentProviderClient client = null;
1932 try {
1933 if (DEBUG) Log.d(TAG, "Deleting: " + doc.displayName);
1934 client = DocumentsApplication.acquireUnstableProviderOrThrow(
1935 mResolver, doc.derivedUri.getAuthority());
1936 DocumentsContract.deleteDocument(client, doc.derivedUri);
1937 } catch (Exception e) {
1938 Log.w(TAG, "Failed to delete " + doc);
1939 hadTrouble = true;
1940 } finally {
1941 ContentProviderClient.releaseQuietly(client);
1942 }
1943 }
1944
1945 if (hadTrouble) {
Ben Kwa7461a952015-09-01 11:03:01 -07001946 // TODO show which files failed? b/23720103
1947 mErrorCallback.run();
Ben Kwa91923182015-08-27 16:06:33 -07001948 if (DEBUG) Log.d(TAG, "Deletion task completed. Some deletions failed.");
1949 } else {
1950 if (DEBUG) Log.d(TAG, "Deletion task completed successfully.");
1951 }
1952 mMarkedForDeletion.clear();
1953 }
1954 }
Ben Kwa7461a952015-09-01 11:03:01 -07001955
1956 void addUpdateListener(UpdateListener listener) {
1957 checkState(mUpdateListener == null);
1958 mUpdateListener = listener;
1959 }
1960
1961 interface UpdateListener {
1962 /**
1963 * Called when a successful update has occurred.
1964 */
1965 void onModelUpdate(Model model);
1966
1967 /**
1968 * Called when an update has been attempted but failed.
1969 */
1970 void onModelUpdateFailed(Exception e);
1971
1972 /**
1973 * Called when an item has been removed from the model.
1974 */
1975 void notifyItemRemoved(int position);
1976
1977 /**
1978 * Called when an item has been added to the model.
1979 */
1980 void notifyItemInserted(int position);
1981 }
Ben Kwa24be5d32015-08-27 16:04:46 -07001982 }
Jeff Sharkeye22d02e2013-04-26 16:54:55 -07001983}