blob: 90c2d1b606d585ffbe8e066536d936bb35024fbb [file] [log] [blame]
Jeff Sharkey9e0036e2013-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
Steve McKayf8621552015-11-03 15:23:16 -080017package com.android.documentsui.dirlist;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070018
Steve McKay7a3b88c2015-09-23 17:21:40 -070019import static com.android.documentsui.Shared.DEBUG;
Steve McKay7a3b88c2015-09-23 17:21:40 -070020import static com.android.documentsui.State.ACTION_MANAGE;
21import static com.android.documentsui.State.MODE_GRID;
22import static com.android.documentsui.State.MODE_LIST;
Steve McKay7a3b88c2015-09-23 17:21:40 -070023import static com.android.documentsui.State.SORT_ORDER_UNKNOWN;
Jeff Sharkey5dfb3452013-08-31 21:27:44 -070024import static com.android.documentsui.model.DocumentInfo.getCursorInt;
Jeff Sharkey5dfb3452013-08-31 21:27:44 -070025import static com.android.documentsui.model.DocumentInfo.getCursorString;
Steve McKay4b3a13c2015-06-11 10:10:49 -070026import static com.android.internal.util.Preconditions.checkNotNull;
Steve McKay0ba3a142015-07-23 16:33:41 -070027import static com.android.internal.util.Preconditions.checkState;
Steve McKayd28c5c32015-12-07 16:31:42 -080028import static com.google.common.base.Preconditions.checkArgument;
Steve McKay3da8afc2015-05-05 14:50:00 -070029
Steve McKayaa15dae2016-02-09 16:17:24 -080030import android.annotation.IntDef;
Steve McKay9ed88a42016-01-21 18:46:15 -080031import android.annotation.StringRes;
Ben Kwaef3f2622015-04-07 15:43:39 -070032import android.app.Activity;
Jeff Sharkeyd01571e2013-10-01 17:57:41 -070033import android.app.ActivityManager;
Jeff Sharkey54e55b72013-06-30 20:02:59 -070034import android.app.Fragment;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070035import android.app.FragmentManager;
36import android.app.FragmentTransaction;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070037import android.app.LoaderManager.LoaderCallbacks;
Vladislav Kaznacheev335dba52015-05-01 13:46:57 -070038import android.content.ClipData;
Jeff Sharkey4eb407a2013-08-18 17:38:20 -070039import android.content.ContentResolver;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070040import android.content.Context;
Jeff Sharkey4eb407a2013-08-18 17:38:20 -070041import android.content.Intent;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070042import android.content.Loader;
Jeff Sharkey5dfb3452013-08-31 21:27:44 -070043import android.database.Cursor;
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -070044import android.graphics.Canvas;
Jeff Sharkey9d0843d2013-05-07 12:41:33 -070045import android.graphics.Point;
Jeff Sharkey4ec97392013-09-10 12:04:26 -070046import android.graphics.drawable.Drawable;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070047import android.net.Uri;
Jeff Sharkey9d0843d2013-05-07 12:41:33 -070048import android.os.AsyncTask;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070049import android.os.Bundle;
Jeff Sharkeye20a3ac2013-09-18 16:26:49 -070050import android.os.Parcelable;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070051import android.provider.DocumentsContract;
Jeff Sharkey5dfb3452013-08-31 21:27:44 -070052import android.provider.DocumentsContract.Document;
Ben Kwac64cb252015-08-27 16:04:46 -070053import android.support.annotation.Nullable;
Ben Kwa304895a2015-08-27 16:06:33 -070054import android.support.design.widget.Snackbar;
Ben Kwae3dfebf2016-02-18 16:45:45 -080055import android.support.v13.view.DragStartHelper;
Steve McKay4b3a13c2015-06-11 10:10:49 -070056import android.support.v7.widget.GridLayoutManager;
Steve McKayef16f5f2015-12-22 18:15:31 -080057import android.support.v7.widget.GridLayoutManager.SpanSizeLookup;
Steve McKay4b3a13c2015-06-11 10:10:49 -070058import android.support.v7.widget.RecyclerView;
Steve McKay669ebe72015-10-19 12:04:21 -070059import android.support.v7.widget.RecyclerView.OnItemTouchListener;
Steve McKay4b3a13c2015-06-11 10:10:49 -070060import android.support.v7.widget.RecyclerView.RecyclerListener;
61import android.support.v7.widget.RecyclerView.ViewHolder;
Jeff Sharkeyf491c722015-06-11 09:16:19 -070062import android.text.TextUtils;
Jeff Sharkey9d0843d2013-05-07 12:41:33 -070063import android.util.Log;
Jeff Sharkeye20a3ac2013-09-18 16:26:49 -070064import android.util.SparseArray;
Ben Kwa0f7078f02015-09-08 07:31:19 -070065import android.util.TypedValue;
Jeff Sharkeybe8b12e2013-07-01 16:56:54 -070066import android.view.ActionMode;
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -070067import android.view.DragEvent;
Steve McKay4b3a13c2015-06-11 10:10:49 -070068import android.view.GestureDetector;
Ben Kwa67924892016-01-27 09:58:36 -080069import android.view.KeyEvent;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070070import android.view.LayoutInflater;
Jeff Sharkey54e55b72013-06-30 20:02:59 -070071import android.view.Menu;
Jeff Sharkey54e55b72013-06-30 20:02:59 -070072import android.view.MenuItem;
Steve McKay4b3a13c2015-06-11 10:10:49 -070073import android.view.MotionEvent;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070074import android.view.View;
75import android.view.ViewGroup;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070076import android.widget.ImageView;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070077import android.widget.TextView;
78
Steve McKayf8621552015-11-03 15:23:16 -080079import com.android.documentsui.BaseActivity;
Steve McKayf8621552015-11-03 15:23:16 -080080import com.android.documentsui.DirectoryLoader;
81import com.android.documentsui.DirectoryResult;
82import com.android.documentsui.DocumentClipper;
83import com.android.documentsui.DocumentsActivity;
84import com.android.documentsui.DocumentsApplication;
85import com.android.documentsui.Events;
Ben Kwa0436a752016-01-15 10:43:24 -080086import com.android.documentsui.Events.MotionInputEvent;
Steve McKayf8621552015-11-03 15:23:16 -080087import com.android.documentsui.Menus;
88import com.android.documentsui.MessageBar;
89import com.android.documentsui.MimePredicate;
Steve McKayf8621552015-11-03 15:23:16 -080090import com.android.documentsui.R;
Steve McKaye2af0782016-02-18 09:48:39 -080091import com.android.documentsui.RecentsLoader;
Steve McKayf8621552015-11-03 15:23:16 -080092import com.android.documentsui.RootsCache;
93import com.android.documentsui.Shared;
94import com.android.documentsui.Snackbars;
95import com.android.documentsui.State;
Ben Kwa1e2fa5e2016-02-02 23:00:02 -080096import com.android.documentsui.State.ViewMode;
Steve McKayf8621552015-11-03 15:23:16 -080097import com.android.documentsui.dirlist.MultiSelectManager.Selection;
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -070098import com.android.documentsui.model.DocumentInfo;
Tomasz Mikolajewskif8c3f322015-04-14 16:32:41 +090099import com.android.documentsui.model.DocumentStack;
Jeff Sharkey251097b2013-09-02 15:07:28 -0700100import com.android.documentsui.model.RootInfo;
Steve McKay14e827a2016-01-06 18:32:13 -0800101import com.android.documentsui.services.FileOperationService;
Ben Kwa1e2fa5e2016-02-02 23:00:02 -0800102import com.android.documentsui.services.FileOperationService.OpType;
Steve McKay14e827a2016-01-06 18:32:13 -0800103import com.android.documentsui.services.FileOperations;
Steve McKaya6bbeab2016-02-17 15:02:01 -0800104
Steve McKay58efce32015-08-20 16:19:38 +0000105import com.google.common.collect.Lists;
Jeff Sharkeybe8b12e2013-07-01 16:56:54 -0700106
Steve McKayaa15dae2016-02-09 16:17:24 -0800107import java.lang.annotation.Retention;
108import java.lang.annotation.RetentionPolicy;
Jeff Sharkeybe8b12e2013-07-01 16:56:54 -0700109import java.util.ArrayList;
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -0700110import java.util.Collections;
Jeff Sharkeyef7184a2013-08-05 17:56:48 -0700111import java.util.List;
Jeff Sharkey54e55b72013-06-30 20:02:59 -0700112
113/**
114 * Display the documents inside a single directory.
115 */
Aga Wronska893390b2016-02-17 13:50:42 -0800116public class DirectoryFragment extends Fragment
117 implements DocumentsAdapter.Environment, LoaderCallbacks<DirectoryResult> {
Jeff Sharkey54e55b72013-06-30 20:02:59 -0700118
Steve McKayaa15dae2016-02-09 16:17:24 -0800119 @IntDef(flag = true, value = {
120 TYPE_NORMAL,
Steve McKayaa15dae2016-02-09 16:17:24 -0800121 TYPE_RECENT_OPEN
122 })
123 @Retention(RetentionPolicy.SOURCE)
124 public @interface ResultType {}
Jeff Sharkeyef7184a2013-08-05 17:56:48 -0700125 public static final int TYPE_NORMAL = 1;
Aga Wronska893390b2016-02-17 13:50:42 -0800126 public static final int TYPE_RECENT_OPEN = 2;
Jeff Sharkeydc2963a2013-08-02 15:55:26 -0700127
Aga Wronska893390b2016-02-17 13:50:42 -0800128 @IntDef(flag = true, value = {
129 ANIM_NONE,
130 ANIM_SIDE,
131 ANIM_LEAVE,
132 ANIM_ENTER
133 })
134 @Retention(RetentionPolicy.SOURCE)
135 public @interface AnimationType {}
Jeff Sharkeye20a3ac2013-09-18 16:26:49 -0700136 public static final int ANIM_NONE = 1;
137 public static final int ANIM_SIDE = 2;
Tomasz Mikolajewskie3fe9d72016-02-03 16:53:21 +0900138 public static final int ANIM_LEAVE = 3;
139 public static final int ANIM_ENTER = 4;
Jeff Sharkeye20a3ac2013-09-18 16:26:49 -0700140
Steve McKaya6bbeab2016-02-17 15:02:01 -0800141 @IntDef(flag = true, value = {
142 REQUEST_COPY_DESTINATION
143 })
144 @Retention(RetentionPolicy.SOURCE)
145 public @interface RequestCode {}
Ben Kwaef3f2622015-04-07 15:43:39 -0700146 public static final int REQUEST_COPY_DESTINATION = 1;
147
Steve McKayef16f5f2015-12-22 18:15:31 -0800148 static final boolean DEBUG_ENABLE_DND = true;
Steve McKayb46383b2015-05-06 14:27:57 -0700149
Steve McKay35645432016-01-20 15:09:35 -0800150 private static final String TAG = "DirectoryFragment";
151 private static final int LOADER_ID = 42;
152 private static final int DELETE_UNDO_TIMEOUT = 5000;
153 private static final int DELETE_JOB_DELAY = 5500;
Steve McKay9ed88a42016-01-21 18:46:15 -0800154 private static final int EMPTY_REVEAL_DURATION = 250;
Steve McKay35645432016-01-20 15:09:35 -0800155
Ben Kwa18fce3c2015-09-01 11:03:01 -0700156 private Model mModel;
Ben Kwa8250db42015-10-07 14:15:12 -0700157 private MultiSelectManager mSelectionManager;
Ben Kwa91caed82015-09-21 10:49:52 -0700158 private Model.UpdateListener mModelUpdateListener = new ModelUpdateListener();
Ben Kwa0436a752016-01-15 10:43:24 -0800159 private ItemEventListener mItemEventListener = new ItemEventListener();
Ben Kwab0bfe2d2016-02-09 11:27:45 -0800160 private FocusManager mFocusManager;
Ben Kwac64cb252015-08-27 16:04:46 -0700161
Ben Kwad8391492015-12-17 10:37:00 -0800162 private IconHelper mIconHelper;
163
Steve McKay4b3a13c2015-06-11 10:10:49 -0700164 private View mEmptyView;
165 private RecyclerView mRecView;
Steve McKay41585d52016-01-21 15:10:39 -0800166 private ListeningGestureDetector mGestureDetector;
Steve McKay4b3a13c2015-06-11 10:10:49 -0700167
Steve McKay4b3a13c2015-06-11 10:10:49 -0700168 private String mStateKey;
169
Steve McKay4b3a13c2015-06-11 10:10:49 -0700170 private int mLastSortOrder = SORT_ORDER_UNKNOWN;
Steve McKay4b3a13c2015-06-11 10:10:49 -0700171 private DocumentsAdapter mAdapter;
Steve McKayf69502c2015-11-09 17:38:35 +0900172 private FragmentTuner mTuner;
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700173 private DocumentClipper mClipper;
Ben Kwa1e2fa5e2016-02-02 23:00:02 -0800174 private GridLayoutManager mLayout;
Steve McKay0ba3a142015-07-23 16:33:41 -0700175 private int mColumnCount = 1; // This will get updated when layout changes.
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700176
Ben Kwa3b19e312015-09-16 08:04:37 -0700177 private MessageBar mMessageBar;
Ben Kwa91caed82015-09-21 10:49:52 -0700178 private View mProgressBar;
Ben Kwa3b19e312015-09-16 08:04:37 -0700179
Aga Wronska893390b2016-02-17 13:50:42 -0800180 // Directory fragment state is defined by: root, document, query, type, selection
181 private @ResultType int mType = TYPE_NORMAL;
182 private RootInfo mRoot;
183 private DocumentInfo mDocument;
184 private String mQuery = null;
185 private Selection mSelection = null;
186 private boolean mSearchMode = false;
187
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700188 @Override
189 public View onCreateView(
190 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
Jeff Sharkey54e55b72013-06-30 20:02:59 -0700191 final View view = inflater.inflate(R.layout.fragment_directory, container, false);
192
Ben Kwa3b19e312015-09-16 08:04:37 -0700193 mMessageBar = MessageBar.create(getChildFragmentManager());
Ben Kwa91caed82015-09-21 10:49:52 -0700194 mProgressBar = view.findViewById(R.id.progressbar);
Ben Kwa3b19e312015-09-16 08:04:37 -0700195
Jeff Sharkey9fb567b2013-08-07 16:22:02 -0700196 mEmptyView = view.findViewById(android.R.id.empty);
197
Ben Kwa2036dad2016-02-10 07:46:35 -0800198 mRecView = (RecyclerView) view.findViewById(R.id.dir_list);
Steve McKay4b3a13c2015-06-11 10:10:49 -0700199 mRecView.setRecyclerListener(
200 new RecyclerListener() {
201 @Override
202 public void onViewRecycled(ViewHolder holder) {
203 cancelThumbnailTask(holder.itemView);
204 }
205 });
Steve McKayb46383b2015-05-06 14:27:57 -0700206
Ben Kwae48e4ca2015-10-20 15:02:33 -0700207 mRecView.setItemAnimator(new DirectoryItemAnimator(getActivity()));
Ben Kwabd964562015-10-14 08:00:27 -0700208
Steve McKay0ba3a142015-07-23 16:33:41 -0700209 // TODO: Add a divider between views (which might use RecyclerView.ItemDecoration).
Steve McKayb46383b2015-05-06 14:27:57 -0700210 if (DEBUG_ENABLE_DND) {
Steve McKay4b3a13c2015-06-11 10:10:49 -0700211 setupDragAndDropOnDirectoryView(mRecView);
Steve McKayb46383b2015-05-06 14:27:57 -0700212 }
Jeff Sharkey54e55b72013-06-30 20:02:59 -0700213
Jeff Sharkey5dfb3452013-08-31 21:27:44 -0700214 return view;
215 }
Jeff Sharkey54e55b72013-06-30 20:02:59 -0700216
Jeff Sharkey5dfb3452013-08-31 21:27:44 -0700217 @Override
Jeff Sharkeyd01571e2013-10-01 17:57:41 -0700218 public void onDestroyView() {
219 super.onDestroyView();
220
221 // Cancel any outstanding thumbnail requests
Steve McKay4b3a13c2015-06-11 10:10:49 -0700222 final int count = mRecView.getChildCount();
Jeff Sharkeyd01571e2013-10-01 17:57:41 -0700223 for (int i = 0; i < count; i++) {
Steve McKay4b3a13c2015-06-11 10:10:49 -0700224 final View view = mRecView.getChildAt(i);
225 cancelThumbnailTask(view);
Jeff Sharkeyd01571e2013-10-01 17:57:41 -0700226 }
227 }
228
229 @Override
Jeff Sharkey5dfb3452013-08-31 21:27:44 -0700230 public void onActivityCreated(Bundle savedInstanceState) {
231 super.onActivityCreated(savedInstanceState);
232
233 final Context context = getActivity();
Steve McKayf69502c2015-11-09 17:38:35 +0900234 final State state = getDisplayState();
Jeff Sharkey5dfb3452013-08-31 21:27:44 -0700235
Aga Wronska893390b2016-02-17 13:50:42 -0800236 // Read arguments when object created for the first time.
237 // Restore state if fragment recreated.
238 Bundle args = savedInstanceState == null ? getArguments() : savedInstanceState;
239 mRoot = args.getParcelable(Shared.EXTRA_ROOT);
240 mDocument = args.getParcelable(Shared.EXTRA_DOC);
241 mStateKey = buildStateKey(mRoot, mDocument);
242 mQuery = args.getString(Shared.EXTRA_QUERY);
243 mType = args.getInt(Shared.EXTRA_TYPE);
244 mSelection = args.getParcelable(Shared.EXTRA_SELECTION);
245 mSearchMode = args.getBoolean(Shared.EXTRA_SEARCH_MODE);
Jeff Sharkeyf6db1542013-09-13 13:42:19 -0700246
Steve McKay3eb2d072016-01-25 19:00:22 -0800247 mIconHelper = new IconHelper(context, MODE_GRID);
Steve McKayef16f5f2015-12-22 18:15:31 -0800248
249 mAdapter = new SectionBreakDocumentsAdapterWrapper(
250 this, new ModelBackedDocumentsAdapter(this, mIconHelper));
251
Steve McKay4b3a13c2015-06-11 10:10:49 -0700252 mRecView.setAdapter(mAdapter);
253
Ben Kwa1e2fa5e2016-02-02 23:00:02 -0800254 mLayout = new GridLayoutManager(getContext(), mColumnCount);
255 SpanSizeLookup lookup = mAdapter.createSpanSizeLookup();
256 if (lookup != null) {
257 mLayout.setSpanSizeLookup(lookup);
258 }
259 mRecView.setLayoutManager(mLayout);
260
Ben Kwae3dfebf2016-02-18 16:45:45 -0800261 mGestureDetector =
262 new ListeningGestureDetector(this.getContext(), mDragHelper, new GestureListener());
Steve McKay669ebe72015-10-19 12:04:21 -0700263
Steve McKay41585d52016-01-21 15:10:39 -0800264 mRecView.addOnItemTouchListener(mGestureDetector);
Steve McKay669ebe72015-10-19 12:04:21 -0700265
Ben Kwac64cb252015-08-27 16:04:46 -0700266 // TODO: instead of inserting the view into the constructor, extract listener-creation code
267 // and set the listener on the view after the fact. Then the view doesn't need to be passed
Ben Kwa8250db42015-10-07 14:15:12 -0700268 // into the selection manager.
269 mSelectionManager = new MultiSelectManager(
Steve McKaydbec47a2015-08-12 14:48:34 -0700270 mRecView,
Steve McKay44408262016-01-05 15:27:17 -0800271 mAdapter,
Steve McKaydbec47a2015-08-12 14:48:34 -0700272 state.allowMultiple
273 ? MultiSelectManager.MODE_MULTIPLE
Steve McKaye852d932016-02-08 19:09:42 -0800274 : MultiSelectManager.MODE_SINGLE,
Aga Wronska893390b2016-02-17 13:50:42 -0800275 null);
Steve McKaye852d932016-02-08 19:09:42 -0800276
Aga Wronska893390b2016-02-17 13:50:42 -0800277 mSelectionManager.addCallback(new SelectionModeListener());
Ben Kwa18fce3c2015-09-01 11:03:01 -0700278
Steve McKay35645432016-01-20 15:09:35 -0800279 mModel = new Model();
Ben Kwa743c7c22015-12-01 19:56:57 -0800280 mModel.addUpdateListener(mAdapter);
Ben Kwa91caed82015-09-21 10:49:52 -0700281 mModel.addUpdateListener(mModelUpdateListener);
Steve McKay4b3a13c2015-06-11 10:10:49 -0700282
Ben Kwaa4acc902016-02-10 15:48:25 -0800283 // Make sure this is done after the RecyclerView is set up.
284 mFocusManager = new FocusManager(context, mRecView, mModel);
285
Steve McKayaa15dae2016-02-09 16:17:24 -0800286 mTuner = FragmentTuner.pick(getContext(), state);
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700287 mClipper = new DocumentClipper(context);
288
Ben Kwad8391492015-12-17 10:37:00 -0800289 boolean hideGridTitles;
Jeff Sharkeyf6db1542013-09-13 13:42:19 -0700290 if (mType == TYPE_RECENT_OPEN) {
291 // Hide titles when showing recents for picking images/videos
Ben Kwad8391492015-12-17 10:37:00 -0800292 hideGridTitles = MimePredicate.mimeMatches(
Jeff Sharkeyf6db1542013-09-13 13:42:19 -0700293 MimePredicate.VISUAL_MIMES, state.acceptMimes);
294 } else {
Aga Wronska893390b2016-02-17 13:50:42 -0800295 hideGridTitles = (mDocument != null) && mDocument.isGridTitlesHidden();
Jeff Sharkeyf6db1542013-09-13 13:42:19 -0700296 }
Ben Kwad8391492015-12-17 10:37:00 -0800297 GridDocumentHolder.setHideTitles(hideGridTitles);
Jeff Sharkeyf6db1542013-09-13 13:42:19 -0700298
Jeff Sharkeyd01571e2013-10-01 17:57:41 -0700299 final ActivityManager am = (ActivityManager) context.getSystemService(
300 Context.ACTIVITY_SERVICE);
Ben Kwad8391492015-12-17 10:37:00 -0800301 boolean svelte = am.isLowRamDevice() && (mType == TYPE_RECENT_OPEN);
302 mIconHelper.setThumbnailsEnabled(!svelte);
Jeff Sharkeyd01571e2013-10-01 17:57:41 -0700303
Jeff Sharkeyd182bb62013-09-07 14:45:03 -0700304 // Kick off loader at least once
Aga Wronska893390b2016-02-17 13:50:42 -0800305 getLoaderManager().restartLoader(LOADER_ID, null, this);
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700306 }
307
Jeff Sharkey28c05ee2013-09-06 13:22:09 -0700308 @Override
Steve McKaye852d932016-02-08 19:09:42 -0800309 public void onSaveInstanceState(Bundle outState) {
Aga Wronska893390b2016-02-17 13:50:42 -0800310 super.onSaveInstanceState(outState);
311
312 outState.putInt(Shared.EXTRA_TYPE, mType);
313 outState.putParcelable(Shared.EXTRA_ROOT, mRoot);
314 outState.putParcelable(Shared.EXTRA_DOC, mDocument);
315 outState.putString(Shared.EXTRA_QUERY, mQuery);
316 outState.putParcelable(Shared.EXTRA_SELECTION, mSelectionManager.getSelection());
317 outState.putBoolean(Shared.EXTRA_SEARCH_MODE, mSearchMode);
Steve McKaye852d932016-02-08 19:09:42 -0800318 }
319
320 @Override
Steve McKaya6bbeab2016-02-17 15:02:01 -0800321 public void onActivityResult(@RequestCode int requestCode, int resultCode, Intent data) {
322 switch(requestCode) {
323 case REQUEST_COPY_DESTINATION:
324 handleCopyResult(resultCode, data);
325 break;
326 default:
327 throw new UnsupportedOperationException("Unknown request code: " + requestCode);
Ben Kwaef3f2622015-04-07 15:43:39 -0700328 }
Steve McKaya6bbeab2016-02-17 15:02:01 -0800329 }
330
331 private void handleCopyResult(int resultCode, Intent data) {
Ben Kwaef3f2622015-04-07 15:43:39 -0700332 if (resultCode == Activity.RESULT_CANCELED || data == null) {
333 // User pressed the back button or otherwise cancelled the destination pick. Don't
334 // proceed with the copy.
335 return;
336 }
337
Steve McKaya6bbeab2016-02-17 15:02:01 -0800338 @OpType int operationType = data.getIntExtra(
Steve McKay14e827a2016-01-06 18:32:13 -0800339 FileOperationService.EXTRA_OPERATION,
340 FileOperationService.OPERATION_COPY);
341
342 FileOperations.start(
343 getActivity(),
344 getDisplayState().selectedDocumentsForCopy,
Tomasz Mikolajewskib8436af2016-01-25 16:20:15 +0900345 getDisplayState().stack.peek(),
Steve McKay323ee3e2015-09-25 16:02:56 -0700346 (DocumentStack) data.getParcelableExtra(Shared.EXTRA_STACK),
Steve McKay14e827a2016-01-06 18:32:13 -0800347 operationType);
Ben Kwaef3f2622015-04-07 15:43:39 -0700348 }
349
Steve McKay5353a1e2015-07-30 12:27:44 -0700350 protected boolean onDoubleTap(MotionEvent e) {
351 if (Events.isMouseEvent(e)) {
Ben Kwa003a9992015-11-30 23:00:02 -0800352 String id = getModelId(e);
353 if (id != null) {
354 return handleViewItem(id);
Steve McKay5353a1e2015-07-30 12:27:44 -0700355 }
356 }
357 return false;
358 }
359
Ben Kwa003a9992015-11-30 23:00:02 -0800360 private boolean handleViewItem(String id) {
361 final Cursor cursor = mModel.getItem(id);
Steve McKay5353a1e2015-07-30 12:27:44 -0700362 checkNotNull(cursor, "Cursor cannot be null.");
363 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
364 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
Steve McKayf69502c2015-11-09 17:38:35 +0900365 if (mTuner.isDocumentEnabled(docMimeType, docFlags)) {
Steve McKay5353a1e2015-07-30 12:27:44 -0700366 final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
Ben Kwac64cb252015-08-27 16:04:46 -0700367 ((BaseActivity) getActivity()).onDocumentPicked(doc, mModel);
Ben Kwa8250db42015-10-07 14:15:12 -0700368 mSelectionManager.clearSelection();
Steve McKay5353a1e2015-07-30 12:27:44 -0700369 return true;
370 }
Steve McKay4b3a13c2015-06-11 10:10:49 -0700371 return false;
372 }
373
Ben Kwaef3f2622015-04-07 15:43:39 -0700374 @Override
Jeff Sharkeye20a3ac2013-09-18 16:26:49 -0700375 public void onStop() {
376 super.onStop();
377
378 // Remember last scroll location
379 final SparseArray<Parcelable> container = new SparseArray<Parcelable>();
380 getView().saveHierarchyState(container);
Steve McKayf69502c2015-11-09 17:38:35 +0900381 final State state = getDisplayState();
Jeff Sharkeye20a3ac2013-09-18 16:26:49 -0700382 state.dirState.put(mStateKey, container);
383 }
384
Jeff Sharkey669f8e72014-08-08 15:10:03 -0700385 public void onDisplayStateChanged() {
386 updateDisplayState();
387 }
388
Steve McKay3eb2d072016-01-25 19:00:22 -0800389 public void onSortOrderChanged() {
390 // Sort order is implemented as a sorting wrapper around directory
391 // results. So when sort order changes, we force a reload of the directory.
Aga Wronska893390b2016-02-17 13:50:42 -0800392 getLoaderManager().restartLoader(LOADER_ID, null, this);
Jeff Sharkeyd182bb62013-09-07 14:45:03 -0700393 }
394
Steve McKay3eb2d072016-01-25 19:00:22 -0800395 public void onViewModeChanged() {
396 // Mode change is just visual change; no need to kick loader.
Jeff Sharkeyd182bb62013-09-07 14:45:03 -0700397 updateDisplayState();
398 }
399
400 private void updateDisplayState() {
Steve McKay3eb2d072016-01-25 19:00:22 -0800401 State state = getDisplayState();
Steve McKay4b3a13c2015-06-11 10:10:49 -0700402 updateLayout(state.derivedMode);
Steve McKay4b3a13c2015-06-11 10:10:49 -0700403 mRecView.setAdapter(mAdapter);
404 }
405
406 /**
Ben Kwa1e2fa5e2016-02-02 23:00:02 -0800407 * Updates the layout after the view mode switches.
408 * @param mode The new view mode.
Steve McKay4b3a13c2015-06-11 10:10:49 -0700409 */
Ben Kwa1e2fa5e2016-02-02 23:00:02 -0800410 private void updateLayout(@ViewMode int mode) {
411 mColumnCount = calculateColumnCount(mode);
412 if (mLayout != null) {
413 mLayout.setSpanCount(mColumnCount);
Jeff Sharkey54e55b72013-06-30 20:02:59 -0700414 }
Jeff Sharkey9d0843d2013-05-07 12:41:33 -0700415
Steve McKay76be6202016-01-12 11:14:33 -0800416 int pad = getDirectoryPadding(mode);
417 mRecView.setPadding(pad, pad, pad, pad);
Ben Kwa1e2fa5e2016-02-02 23:00:02 -0800418 mRecView.requestLayout();
Steve McKay76be6202016-01-12 11:14:33 -0800419 mSelectionManager.handleLayoutChanged(); // RecyclerView doesn't do this for us
Steve McKay3eb2d072016-01-25 19:00:22 -0800420 mIconHelper.setViewMode(mode);
Jeff Sharkey54e55b72013-06-30 20:02:59 -0700421 }
422
Ben Kwa1e2fa5e2016-02-02 23:00:02 -0800423 private int calculateColumnCount(@ViewMode int mode) {
424 if (mode == MODE_LIST) {
425 // List mode is a "grid" with 1 column.
426 return 1;
427 }
428
Steve McKay58efce32015-08-20 16:19:38 +0000429 int cellWidth = getResources().getDimensionPixelSize(R.dimen.grid_width);
430 int cellMargin = 2 * getResources().getDimensionPixelSize(R.dimen.grid_item_margin);
Steve McKay0ba3a142015-07-23 16:33:41 -0700431 int viewPadding = mRecView.getPaddingLeft() + mRecView.getPaddingRight();
Steve McKay58efce32015-08-20 16:19:38 +0000432
Steve McKay0ba3a142015-07-23 16:33:41 -0700433 checkState(mRecView.getWidth() > 0);
434 int columnCount = Math.max(1,
Steve McKay58efce32015-08-20 16:19:38 +0000435 (mRecView.getWidth() - viewPadding) / (cellWidth + cellMargin));
436
Steve McKay0ba3a142015-07-23 16:33:41 -0700437 return columnCount;
438 }
439
Ben Kwa1e2fa5e2016-02-02 23:00:02 -0800440 private int getDirectoryPadding(@ViewMode int mode) {
Steve McKay76be6202016-01-12 11:14:33 -0800441 switch (mode) {
442 case MODE_GRID:
Ben Kwa1e2fa5e2016-02-02 23:00:02 -0800443 return getResources().getDimensionPixelSize(R.dimen.grid_container_padding);
Steve McKay76be6202016-01-12 11:14:33 -0800444 case MODE_LIST:
Ben Kwa1e2fa5e2016-02-02 23:00:02 -0800445 return getResources().getDimensionPixelSize(R.dimen.list_container_padding);
Steve McKay76be6202016-01-12 11:14:33 -0800446 default:
447 throw new IllegalArgumentException("Unsupported layout mode: " + mode);
448 }
449 }
450
Steve McKayef16f5f2015-12-22 18:15:31 -0800451 @Override
452 public int getColumnCount() {
453 return mColumnCount;
454 }
455
Steve McKay4b3a13c2015-06-11 10:10:49 -0700456 /**
457 * Manages the integration between our ActionMode and MultiSelectManager, initiating
458 * ActionMode when there is a selection, canceling it when there is no selection,
459 * and clearing selection when action mode is explicitly exited by the user.
460 */
461 private final class SelectionModeListener
462 implements MultiSelectManager.Callback, ActionMode.Callback {
463
464 private Selection mSelected = new Selection();
465 private ActionMode mActionMode;
Steve McKay503648d2015-07-22 12:13:46 -0700466 private int mNoDeleteCount = 0;
Aga Wronska3b327ef2016-01-20 16:32:33 -0800467 private int mNoRenameCount = -1;
Steve McKay503648d2015-07-22 12:13:46 -0700468 private Menu mMenu;
Steve McKay4b3a13c2015-06-11 10:10:49 -0700469
Jeff Sharkey54e55b72013-06-30 20:02:59 -0700470 @Override
Ben Kwa743c7c22015-12-01 19:56:57 -0800471 public boolean onBeforeItemStateChange(String modelId, boolean selected) {
Steve McKay4b3a13c2015-06-11 10:10:49 -0700472 if (selected) {
Ben Kwa743c7c22015-12-01 19:56:57 -0800473 final Cursor cursor = mModel.getItem(modelId);
Steve McKay4b3a13c2015-06-11 10:10:49 -0700474 checkNotNull(cursor, "Cursor cannot be null.");
Jeff Sharkey2a030b02013-09-26 10:54:16 -0700475 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
476 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
Tomasz Mikolajewskia8057a92015-11-16 11:41:28 +0900477 return mTuner.canSelectType(docMimeType, docFlags);
Steve McKay4b3a13c2015-06-11 10:10:49 -0700478 }
479 return true;
480 }
481
482 @Override
Ben Kwa743c7c22015-12-01 19:56:57 -0800483 public void onItemStateChanged(String modelId, boolean selected) {
484 final Cursor cursor = mModel.getItem(modelId);
Steve McKay30551a22016-02-16 13:08:10 -0800485 if (cursor == null) {
486 Log.e(TAG, "Model returned null cursor for document: " + modelId
487 + ". Ignoring state changed event.");
488 return;
489 }
Steve McKay503648d2015-07-22 12:13:46 -0700490
Ben Kwa67924892016-01-27 09:58:36 -0800491 // TODO: Should this be happening in onSelectionChanged? Technically this callback is
492 // triggered on "silent" selection updates (i.e. we might be reacting to unfinalized
493 // selection changes here)
Steve McKay503648d2015-07-22 12:13:46 -0700494 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
Steve McKay27d20a32016-02-22 18:38:09 -0800495 if ((docFlags & Document.FLAG_SUPPORTS_DELETE) == 0
496 && (docFlags & Document.FLAG_SUPPORTS_DELETE) == 0) {
Steve McKay503648d2015-07-22 12:13:46 -0700497 mNoDeleteCount += selected ? 1 : -1;
498 }
Aga Wronska3b327ef2016-01-20 16:32:33 -0800499 if ((docFlags & Document.FLAG_SUPPORTS_RENAME) != 0) {
500 mNoRenameCount += selected ? 1 : -1;
501 }
Steve McKaydbec47a2015-08-12 14:48:34 -0700502 }
Steve McKay503648d2015-07-22 12:13:46 -0700503
Steve McKaydbec47a2015-08-12 14:48:34 -0700504 @Override
505 public void onSelectionChanged() {
Ben Kwa8250db42015-10-07 14:15:12 -0700506 mSelectionManager.getSelection(mSelected);
Ben Kwa7cc80012015-09-11 15:40:18 -0700507 TypedValue color = new TypedValue();
Steve McKay503648d2015-07-22 12:13:46 -0700508 if (mSelected.size() > 0) {
Steve McKay4b3a13c2015-06-11 10:10:49 -0700509 if (DEBUG) Log.d(TAG, "Maybe starting action mode.");
510 if (mActionMode == null) {
511 if (DEBUG) Log.d(TAG, "Yeah. Starting action mode.");
512 mActionMode = getActivity().startActionMode(this);
Jeff Sharkey3f4c2052013-09-09 16:51:06 -0700513 }
Ben Kwa8250db42015-10-07 14:15:12 -0700514 getActivity().getTheme().resolveAttribute(R.attr.colorActionMode, color, true);
Steve McKay503648d2015-07-22 12:13:46 -0700515 updateActionMenu();
516 } else {
517 if (DEBUG) Log.d(TAG, "Finishing action mode.");
518 if (mActionMode != null) {
519 mActionMode.finish();
520 }
Ben Kwa0f7078f02015-09-08 07:31:19 -0700521 getActivity().getTheme().resolveAttribute(
522 android.R.attr.colorPrimaryDark, color, true);
Jeff Sharkeya5599ef2013-08-15 16:17:41 -0700523 }
Ben Kwa7cc80012015-09-11 15:40:18 -0700524 getActivity().getWindow().setStatusBarColor(color.data);
Jeff Sharkey54e55b72013-06-30 20:02:59 -0700525
Steve McKay4b3a13c2015-06-11 10:10:49 -0700526 if (mActionMode != null) {
Steve McKay5aacf1f2015-10-27 14:15:58 -0700527 mActionMode.setTitle(String.valueOf(mSelected.size()));
Steve McKay4b3a13c2015-06-11 10:10:49 -0700528 }
529 }
530
531 // Called when the user exits the action mode
532 @Override
533 public void onDestroyActionMode(ActionMode mode) {
534 if (DEBUG) Log.d(TAG, "Handling action mode destroyed.");
535 mActionMode = null;
536 // clear selection
Ben Kwa8250db42015-10-07 14:15:12 -0700537 mSelectionManager.clearSelection();
Steve McKay503648d2015-07-22 12:13:46 -0700538 mSelected.clear();
539 mNoDeleteCount = 0;
Aga Wronska3b327ef2016-01-20 16:32:33 -0800540 mNoRenameCount = -1;
Steve McKay4b3a13c2015-06-11 10:10:49 -0700541 }
542
Jeff Sharkeybe8b12e2013-07-01 16:56:54 -0700543 @Override
544 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
Ben Kwa8250db42015-10-07 14:15:12 -0700545 int size = mSelectionManager.getSelection().size();
Jeff Sharkeybe8b12e2013-07-01 16:56:54 -0700546 mode.getMenuInflater().inflate(R.menu.mode_directory, menu);
Ben Kwa8250db42015-10-07 14:15:12 -0700547 mode.setTitle(TextUtils.formatSelectedCount(size));
548 return (size > 0);
Jeff Sharkeybe8b12e2013-07-01 16:56:54 -0700549 }
550
551 @Override
552 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
Steve McKay503648d2015-07-22 12:13:46 -0700553 mMenu = menu;
554 updateActionMenu();
Jeff Sharkeybe8b12e2013-07-01 16:56:54 -0700555 return true;
556 }
557
Aga Wronska3b327ef2016-01-20 16:32:33 -0800558 boolean canRenameSelection() {
559 return mNoRenameCount == 0 && mSelectionManager.getSelection().size() == 1;
560 }
561
562 boolean canDeleteSelection() {
563 return mNoDeleteCount == 0;
564 }
565
Steve McKay503648d2015-07-22 12:13:46 -0700566 private void updateActionMenu() {
567 checkNotNull(mMenu);
Aga Wronska3b327ef2016-01-20 16:32:33 -0800568
Steve McKay503648d2015-07-22 12:13:46 -0700569 // Delegate update logic to our owning action, since specialized logic is desired.
Aga Wronska3b327ef2016-01-20 16:32:33 -0800570 mTuner.updateActionMenu(mMenu, mType, canDeleteSelection(), canRenameSelection());
Steve McKay5bbae102015-10-01 11:39:24 -0700571 Menus.disableHiddenItems(mMenu);
Steve McKay503648d2015-07-22 12:13:46 -0700572 }
573
Jeff Sharkeybe8b12e2013-07-01 16:56:54 -0700574 @Override
Steve McKay4b3a13c2015-06-11 10:10:49 -0700575 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700576
Ben Kwa8250db42015-10-07 14:15:12 -0700577 Selection selection = mSelectionManager.getSelection(new Selection());
Jeff Sharkeybe8b12e2013-07-01 16:56:54 -0700578
Steve McKayc2b4b3d2015-12-01 17:02:42 -0800579 switch (item.getItemId()) {
580 case R.id.menu_open:
581 openDocuments(selection);
582 mode.finish();
583 return true;
Jeff Sharkey4eb407a2013-08-18 17:38:20 -0700584
Steve McKayc2b4b3d2015-12-01 17:02:42 -0800585 case R.id.menu_share:
586 shareDocuments(selection);
587 mode.finish();
588 return true;
Jeff Sharkey4eb407a2013-08-18 17:38:20 -0700589
Steve McKayc2b4b3d2015-12-01 17:02:42 -0800590 case R.id.menu_delete:
591 // Exit selection mode first, so we avoid deselecting deleted documents.
592 mode.finish();
593 deleteDocuments(selection);
594 return true;
Jeff Sharkey4eb407a2013-08-18 17:38:20 -0700595
Steve McKayc2b4b3d2015-12-01 17:02:42 -0800596 case R.id.menu_copy_to:
Steve McKay14e827a2016-01-06 18:32:13 -0800597 transferDocuments(selection, FileOperationService.OPERATION_COPY);
Steve McKayc2b4b3d2015-12-01 17:02:42 -0800598 mode.finish();
599 return true;
Ben Kwa0b4a3c42015-05-05 11:50:11 -0700600
Steve McKayc2b4b3d2015-12-01 17:02:42 -0800601 case R.id.menu_move_to:
602 // Exit selection mode first, so we avoid deselecting deleted documents.
603 mode.finish();
Steve McKay14e827a2016-01-06 18:32:13 -0800604 transferDocuments(selection, FileOperationService.OPERATION_MOVE);
Steve McKayc2b4b3d2015-12-01 17:02:42 -0800605 return true;
Ben Kwad99109f2015-03-31 10:11:43 -0700606
Steve McKayc2b4b3d2015-12-01 17:02:42 -0800607 case R.id.menu_copy_to_clipboard:
Steve McKaycbee5442016-01-28 15:30:10 -0800608 copySelectedToClipboard();
Steve McKayc2b4b3d2015-12-01 17:02:42 -0800609 return true;
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700610
Steve McKayc2b4b3d2015-12-01 17:02:42 -0800611 case R.id.menu_select_all:
612 selectAllFiles();
613 return true;
Ben Kwa3bcc94882015-03-31 08:15:21 -0700614
Aga Wronska3b327ef2016-01-20 16:32:33 -0800615 case R.id.menu_rename:
616 renameDocuments(selection);
617 mode.finish();
618 return true;
619
Steve McKayc2b4b3d2015-12-01 17:02:42 -0800620 default:
621 if (DEBUG) Log.d(TAG, "Unhandled menu item selected: " + item);
622 return false;
Jeff Sharkeybe8b12e2013-07-01 16:56:54 -0700623 }
624 }
Steve McKay4b3a13c2015-06-11 10:10:49 -0700625 }
Jeff Sharkeybe8b12e2013-07-01 16:56:54 -0700626
Steve McKaycbee5442016-01-28 15:30:10 -0800627 public final boolean onBackPressed() {
628 if (mSelectionManager.hasSelection()) {
629 if (DEBUG) Log.d(TAG, "Clearing selection on back pressed.");
630 mSelectionManager.clearSelection();
631 return true;
632 }
633 return false;
634 }
635
Ben Kwad8391492015-12-17 10:37:00 -0800636 private void cancelThumbnailTask(View view) {
Steve McKay4b3a13c2015-06-11 10:10:49 -0700637 final ImageView iconThumb = (ImageView) view.findViewById(R.id.icon_thumb);
638 if (iconThumb != null) {
Ben Kwad8391492015-12-17 10:37:00 -0800639 mIconHelper.stopLoading(iconThumb);
Jeff Sharkey4ec97392013-09-10 12:04:26 -0700640 }
Steve McKay4b3a13c2015-06-11 10:10:49 -0700641 }
Jeff Sharkey4ec97392013-09-10 12:04:26 -0700642
Steve McKay4b3a13c2015-06-11 10:10:49 -0700643 private void openDocuments(final Selection selected) {
Steve McKayab7865c2015-05-27 16:11:42 -0700644 new GetDocumentsTask() {
645 @Override
646 void onDocumentsReady(List<DocumentInfo> docs) {
Steve McKay12055472015-08-20 16:48:49 -0700647 // TODO: Implement support in Files activity for opening multiple docs.
Steve McKayab7865c2015-05-27 16:11:42 -0700648 BaseActivity.get(DirectoryFragment.this).onDocumentsPicked(docs);
Ben Kwa726cf702015-04-08 15:03:35 -0700649 }
Steve McKayab7865c2015-05-27 16:11:42 -0700650 }.execute(selected);
Jeff Sharkey4eb407a2013-08-18 17:38:20 -0700651 }
652
Steve McKay4b3a13c2015-06-11 10:10:49 -0700653 private void shareDocuments(final Selection selected) {
Steve McKayab7865c2015-05-27 16:11:42 -0700654 new GetDocumentsTask() {
655 @Override
656 void onDocumentsReady(List<DocumentInfo> docs) {
657 Intent intent;
658
659 // Filter out directories - those can't be shared.
Steve McKay58efce32015-08-20 16:19:38 +0000660 List<DocumentInfo> docsForSend = new ArrayList<>();
Steve McKayab7865c2015-05-27 16:11:42 -0700661 for (DocumentInfo doc: docs) {
662 if (!Document.MIME_TYPE_DIR.equals(doc.mimeType)) {
663 docsForSend.add(doc);
664 }
665 }
666
667 if (docsForSend.size() == 1) {
668 final DocumentInfo doc = docsForSend.get(0);
669
670 intent = new Intent(Intent.ACTION_SEND);
671 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
672 intent.addCategory(Intent.CATEGORY_DEFAULT);
673 intent.setType(doc.mimeType);
674 intent.putExtra(Intent.EXTRA_STREAM, doc.derivedUri);
675
676 } else if (docsForSend.size() > 1) {
677 intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
678 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
679 intent.addCategory(Intent.CATEGORY_DEFAULT);
680
Steve McKay58efce32015-08-20 16:19:38 +0000681 final ArrayList<String> mimeTypes = new ArrayList<>();
682 final ArrayList<Uri> uris = new ArrayList<>();
Steve McKayab7865c2015-05-27 16:11:42 -0700683 for (DocumentInfo doc : docsForSend) {
684 mimeTypes.add(doc.mimeType);
685 uris.add(doc.derivedUri);
686 }
687
688 intent.setType(findCommonMimeType(mimeTypes));
689 intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
690
691 } else {
692 return;
693 }
694
695 intent = Intent.createChooser(intent, getActivity().getText(R.string.share_via));
696 startActivity(intent);
697 }
698 }.execute(selected);
699 }
700
Steve McKay4b3a13c2015-06-11 10:10:49 -0700701 private void deleteDocuments(final Selection selected) {
Jeff Sharkey4eb407a2013-08-18 17:38:20 -0700702
Tomasz Mikolajewskib8436af2016-01-25 16:20:15 +0900703 checkArgument(!selected.isEmpty());
704 final DocumentInfo srcParent = getDisplayState().stack.peek();
705 new GetDocumentsTask() {
706 @Override
707 void onDocumentsReady(List<DocumentInfo> docs) {
708 // Hide the files in the UI.
709 final SparseArray<String> hidden = mAdapter.hide(selected.getAll());
Steve McKay35645432016-01-20 15:09:35 -0800710
Tomasz Mikolajewskib8436af2016-01-25 16:20:15 +0900711 checkState(DELETE_JOB_DELAY > DELETE_UNDO_TIMEOUT);
712 String operationId = FileOperations.delete(
713 getActivity(), docs, srcParent, getDisplayState().stack,
714 DELETE_JOB_DELAY);
715 showDeleteSnackbar(hidden, operationId);
716 }
717 }.execute(selected);
Steve McKay35645432016-01-20 15:09:35 -0800718 }
719
720 private void showDeleteSnackbar(final SparseArray<String> hidden, final String jobId) {
721
722 Context context = getActivity();
723 String message = Shared.getQuantityString(context, R.plurals.deleting, hidden.size());
Jeff Sharkey4eb407a2013-08-18 17:38:20 -0700724
Ben Kwadb65cd52015-12-09 14:33:49 -0800725 // Show a snackbar informing the user that files will be deleted, and give them an option to
726 // cancel.
Ben Kwa94b486d2015-09-30 10:00:10 -0700727 final Activity activity = getActivity();
Steve McKay35645432016-01-20 15:09:35 -0800728 Snackbars.makeSnackbar(activity, message, DELETE_UNDO_TIMEOUT)
Ben Kwa304895a2015-08-27 16:06:33 -0700729 .setAction(
730 R.string.undo,
Ben Kwabd964562015-10-14 08:00:27 -0700731 new View.OnClickListener() {
Ben Kwa304895a2015-08-27 16:06:33 -0700732 @Override
733 public void onClick(View view) {}
734 })
735 .setCallback(
736 new Snackbar.Callback() {
737 @Override
738 public void onDismissed(Snackbar snackbar, int event) {
739 if (event == Snackbar.Callback.DISMISS_EVENT_ACTION) {
Ben Kwadb65cd52015-12-09 14:33:49 -0800740 // If the delete was cancelled, just unhide the files.
Steve McKay35645432016-01-20 15:09:35 -0800741 FileOperations.cancel(activity, jobId);
742 mAdapter.unhide(hidden);
Ben Kwa304895a2015-08-27 16:06:33 -0700743 }
Ben Kwa304895a2015-08-27 16:06:33 -0700744 }
745 })
746 .show();
Jeff Sharkey4eb407a2013-08-18 17:38:20 -0700747 }
748
Ben Kwa1e2fa5e2016-02-02 23:00:02 -0800749 private void transferDocuments(final Selection selected, final @OpType int mode) {
Ben Kwaef3f2622015-04-07 15:43:39 -0700750 // Pop up a dialog to pick a destination. This is inadequate but works for now.
751 // TODO: Implement a picker that is to spec.
Daichi Hironobbe22922015-04-10 15:50:38 +0900752 final Intent intent = new Intent(
Ben Kwa84cebbe2015-09-25 14:48:29 -0700753 Shared.ACTION_PICK_COPY_DESTINATION,
Daichi Hironobbe22922015-04-10 15:50:38 +0900754 Uri.EMPTY,
755 getActivity(),
756 DocumentsActivity.class);
Steve McKayab7865c2015-05-27 16:11:42 -0700757
Steve McKaya6bbeab2016-02-17 15:02:01 -0800758 // Set an appropriate title on the drawer when it is shown in the picker.
759 // Coupled with the fact that we auto-open the drawer for copy/move operations
760 // it should basically be the thing people see first.
761 int drawerTitleId = mode == FileOperationService.OPERATION_MOVE
762 ? R.string.menu_move : R.string.menu_copy;
763 intent.putExtra(DocumentsContract.EXTRA_PROMPT, getResources().getString(drawerTitleId));
764
Steve McKayab7865c2015-05-27 16:11:42 -0700765 new GetDocumentsTask() {
766 @Override
767 void onDocumentsReady(List<DocumentInfo> docs) {
Steve McKaya6bbeab2016-02-17 15:02:01 -0800768 // TODO: Can this move to Fragment bundle state?
Steve McKayf69502c2015-11-09 17:38:35 +0900769 getDisplayState().selectedDocumentsForCopy = docs;
Steve McKayab7865c2015-05-27 16:11:42 -0700770
Steve McKaya6bbeab2016-02-17 15:02:01 -0800771 // Determine if there is a directory in the set of documents
772 // to be copied? Why? Directory creation isn't supported by some roots
773 // (like Downloads). This informs DocumentsActivity (the "picker")
774 // to restrict available roots to just those with support.
775 intent.putExtra(Shared.EXTRA_DIRECTORY_COPY, hasDirectory(docs));
Steve McKay14e827a2016-01-06 18:32:13 -0800776 intent.putExtra(FileOperationService.EXTRA_OPERATION, mode);
Steve McKaya6bbeab2016-02-17 15:02:01 -0800777
778 // This just identifies the type of request...we'll check it
779 // when we reveive a response.
Steve McKayab7865c2015-05-27 16:11:42 -0700780 startActivityForResult(intent, REQUEST_COPY_DESTINATION);
Daichi Hirono9be34292015-04-14 17:12:54 +0900781 }
Steve McKaya6bbeab2016-02-17 15:02:01 -0800782
Steve McKayab7865c2015-05-27 16:11:42 -0700783 }.execute(selected);
Ben Kwad99109f2015-03-31 10:11:43 -0700784 }
785
Steve McKaya6bbeab2016-02-17 15:02:01 -0800786 private static boolean hasDirectory(List<DocumentInfo> docs) {
787 for (DocumentInfo info : docs) {
788 if (Document.MIME_TYPE_DIR.equals(info.mimeType)) {
789 return true;
790 }
791 }
792 return false;
793 }
794
Aga Wronska3b327ef2016-01-20 16:32:33 -0800795 private void renameDocuments(Selection selected) {
796 // Batch renaming not supported
797 // Rename option is only available in menu when 1 document selected
798 checkArgument(selected.size() == 1);
799
800 new GetDocumentsTask() {
801 @Override
802 void onDocumentsReady(List<DocumentInfo> docs) {
803 RenameDocumentFragment.show(getFragmentManager(), docs.get(0));
804 }
805 }.execute(selected);
806 }
807
Steve McKayef16f5f2015-12-22 18:15:31 -0800808 @Override
809 public void initDocumentHolder(DocumentHolder holder) {
Ben Kwa0436a752016-01-15 10:43:24 -0800810 holder.addEventListener(mItemEventListener);
Ben Kwa2036dad2016-02-10 07:46:35 -0800811 holder.itemView.setOnFocusChangeListener(mFocusManager);
Steve McKayef16f5f2015-12-22 18:15:31 -0800812 }
813
814 @Override
815 public void onBindDocumentHolder(DocumentHolder holder, Cursor cursor) {
816 if (DEBUG_ENABLE_DND) {
817 setupDragAndDropOnDocumentView(holder.itemView, cursor);
818 }
819 }
820
821 @Override
822 public State getDisplayState() {
Steve McKayf69502c2015-11-09 17:38:35 +0900823 return ((BaseActivity) getActivity()).getDisplayState();
Jeff Sharkey54e55b72013-06-30 20:02:59 -0700824 }
825
Steve McKayef16f5f2015-12-22 18:15:31 -0800826 @Override
827 public Model getModel() {
828 return mModel;
829 }
830
831 @Override
832 public boolean isDocumentEnabled(String docMimeType, int docFlags) {
833 return mTuner.isDocumentEnabled(docMimeType, docFlags);
834 }
835
Steve McKay9ed88a42016-01-21 18:46:15 -0800836 private void showEmptyDirectory() {
Ben Kwa0e9aae42016-02-04 16:35:27 -0800837 showEmptyView(R.string.empty, R.drawable.cabinet);
Ben Kwa91bec532015-09-16 13:15:38 -0700838 }
839
Steve McKay9ed88a42016-01-21 18:46:15 -0800840 private void showNoResults(RootInfo root) {
841 CharSequence msg = getContext().getResources().getText(R.string.no_results);
Ben Kwa0e9aae42016-02-04 16:35:27 -0800842 showEmptyView(String.format(String.valueOf(msg), root.title), R.drawable.cabinet);
Ben Kwa91bec532015-09-16 13:15:38 -0700843 }
844
Steve McKay9ed88a42016-01-21 18:46:15 -0800845 private void showQueryError() {
Ben Kwa0e9aae42016-02-04 16:35:27 -0800846 showEmptyView(R.string.query_error, R.drawable.hourglass);
Steve McKay9ed88a42016-01-21 18:46:15 -0800847 }
848
Ben Kwa0e9aae42016-02-04 16:35:27 -0800849 private void showEmptyView(@StringRes int id, int drawable) {
850 showEmptyView(getContext().getResources().getText(id), drawable);
Steve McKay9ed88a42016-01-21 18:46:15 -0800851 }
852
Ben Kwa0e9aae42016-02-04 16:35:27 -0800853 private void showEmptyView(CharSequence msg, int drawable) {
Steve McKay9ed88a42016-01-21 18:46:15 -0800854 View content = mEmptyView.findViewById(R.id.content);
855 TextView msgView = (TextView) mEmptyView.findViewById(R.id.message);
Ben Kwa0e9aae42016-02-04 16:35:27 -0800856 ImageView imageView = (ImageView) mEmptyView.findViewById(R.id.artwork);
Steve McKay9ed88a42016-01-21 18:46:15 -0800857 msgView.setText(msg);
Ben Kwa0e9aae42016-02-04 16:35:27 -0800858 imageView.setImageResource(drawable);
Steve McKay9ed88a42016-01-21 18:46:15 -0800859
Steve McKay9ed88a42016-01-21 18:46:15 -0800860 mEmptyView.setVisibility(View.VISIBLE);
Aga Wronskad4b17532016-02-12 09:15:32 -0800861 mEmptyView.requestFocus();
Steve McKay9ed88a42016-01-21 18:46:15 -0800862 mRecView.setVisibility(View.GONE);
Steve McKay9ed88a42016-01-21 18:46:15 -0800863 }
864
865 private void showDirectory() {
Ben Kwa91bec532015-09-16 13:15:38 -0700866 mEmptyView.setVisibility(View.GONE);
867 mRecView.setVisibility(View.VISIBLE);
Aga Wronskad4b17532016-02-12 09:15:32 -0800868 mRecView.requestFocus();
Ben Kwa91bec532015-09-16 13:15:38 -0700869 }
870
Jeff Sharkeyb5133112013-09-01 18:41:04 -0700871 private String findCommonMimeType(List<String> mimeTypes) {
872 String[] commonType = mimeTypes.get(0).split("/");
873 if (commonType.length != 2) {
874 return "*/*";
875 }
876
877 for (int i = 1; i < mimeTypes.size(); i++) {
878 String[] type = mimeTypes.get(i).split("/");
879 if (type.length != 2) continue;
880
881 if (!commonType[1].equals(type[1])) {
882 commonType[1] = "*";
883 }
884
885 if (!commonType[0].equals(type[0])) {
886 commonType[0] = "*";
887 commonType[1] = "*";
888 break;
889 }
890 }
891
892 return commonType[0] + "/" + commonType[1];
893 }
Jeff Sharkey3f4c2052013-09-09 16:51:06 -0700894
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700895 private void copyFromClipboard() {
896 new AsyncTask<Void, Void, List<DocumentInfo>>() {
897
898 @Override
899 protected List<DocumentInfo> doInBackground(Void... params) {
900 return mClipper.getClippedDocuments();
901 }
902
903 @Override
904 protected void onPostExecute(List<DocumentInfo> docs) {
905 DocumentInfo destination =
906 ((BaseActivity) getActivity()).getCurrentDirectory();
907 copyDocuments(docs, destination);
908 }
909 }.execute();
Steve McKay3da8afc2015-05-05 14:50:00 -0700910 }
911
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700912 private void copyFromClipData(final ClipData clipData, final DocumentInfo destination) {
Steve McKay4b3a13c2015-06-11 10:10:49 -0700913 checkNotNull(clipData);
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700914 new AsyncTask<Void, Void, List<DocumentInfo>>() {
Vladislav Kaznacheev335dba52015-05-01 13:46:57 -0700915
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700916 @Override
917 protected List<DocumentInfo> doInBackground(Void... params) {
918 return mClipper.getDocumentsFromClipData(clipData);
919 }
920
921 @Override
922 protected void onPostExecute(List<DocumentInfo> docs) {
923 copyDocuments(docs, destination);
924 }
925 }.execute();
926 }
927
928 private void copyDocuments(final List<DocumentInfo> docs, final DocumentInfo destination) {
Steve McKay4a1ca862016-02-17 18:25:47 -0800929 BaseActivity activity = (BaseActivity) getActivity();
930 if (!canCopy(docs, activity.getCurrentRoot(), destination)) {
Steve McKay5bbae102015-10-01 11:39:24 -0700931 Snackbars.makeSnackbar(
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700932 getActivity(),
Ben Kwa94b486d2015-09-30 10:00:10 -0700933 R.string.clipboard_files_cannot_paste,
934 Snackbar.LENGTH_SHORT)
935 .show();
Steve McKay3da8afc2015-05-05 14:50:00 -0700936 return;
937 }
938
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700939 if (docs.isEmpty()) {
Vladislav Kaznacheev335dba52015-05-01 13:46:57 -0700940 return;
Vladislav Kaznacheevd69d3c42015-05-05 12:09:47 -0700941 }
Vladislav Kaznacheev335dba52015-05-01 13:46:57 -0700942
Steve McKayf69502c2015-11-09 17:38:35 +0900943 final DocumentStack curStack = getDisplayState().stack;
Vladislav Kaznacheev335dba52015-05-01 13:46:57 -0700944 DocumentStack tmpStack = new DocumentStack();
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700945 if (destination != null) {
946 tmpStack.push(destination);
Vladislav Kaznacheev335dba52015-05-01 13:46:57 -0700947 tmpStack.addAll(curStack);
948 } else {
949 tmpStack = curStack;
950 }
951
Steve McKay14e827a2016-01-06 18:32:13 -0800952 FileOperations.copy(getActivity(), docs, tmpStack);
Vladislav Kaznacheev335dba52015-05-01 13:46:57 -0700953 }
954
955 private ClipData getClipDataFromDocuments(List<DocumentInfo> docs) {
956 Context context = getActivity();
957 final ContentResolver resolver = context.getContentResolver();
958 ClipData clipData = null;
959 for (DocumentInfo doc : docs) {
960 final Uri uri = DocumentsContract.buildDocumentUri(doc.authority, doc.documentId);
961 if (clipData == null) {
Vladislav Kaznacheevd69d3c42015-05-05 12:09:47 -0700962 // TODO: figure out what this string should be.
963 // Currently it is not displayed anywhere in the UI, but this might change.
964 final String label = "";
965 clipData = ClipData.newUri(resolver, label, uri);
Vladislav Kaznacheev335dba52015-05-01 13:46:57 -0700966 } else {
967 // TODO: update list of mime types in ClipData.
968 clipData.addItem(new ClipData.Item(uri));
969 }
970 }
971 return clipData;
972 }
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -0700973
Steve McKayf8621552015-11-03 15:23:16 -0800974 public void copySelectedToClipboard() {
Steve McKayd28c5c32015-12-07 16:31:42 -0800975 Selection selection = mSelectionManager.getSelection(new Selection());
976 if (!selection.isEmpty()) {
977 copySelectionToClipboard(selection);
Steve McKaycbee5442016-01-28 15:30:10 -0800978 mSelectionManager.clearSelection();
Steve McKayd28c5c32015-12-07 16:31:42 -0800979 }
Steve McKayab7865c2015-05-27 16:11:42 -0700980 }
Steve McKay3da8afc2015-05-05 14:50:00 -0700981
Steve McKayd28c5c32015-12-07 16:31:42 -0800982 void copySelectionToClipboard(Selection selection) {
983 checkArgument(!selection.isEmpty());
Steve McKayab7865c2015-05-27 16:11:42 -0700984 new GetDocumentsTask() {
985 @Override
986 void onDocumentsReady(List<DocumentInfo> docs) {
987 mClipper.clipDocuments(docs);
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700988 Activity activity = getActivity();
Steve McKay5bbae102015-10-01 11:39:24 -0700989 Snackbars.makeSnackbar(activity,
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700990 activity.getResources().getQuantityString(
991 R.plurals.clipboard_files_clipped, docs.size(), docs.size()),
Ben Kwa862b96412015-12-07 13:25:27 -0800992 Snackbar.LENGTH_SHORT).show();
Steve McKayab7865c2015-05-27 16:11:42 -0700993 }
Steve McKayd28c5c32015-12-07 16:31:42 -0800994 }.execute(selection);
Steve McKay3da8afc2015-05-05 14:50:00 -0700995 }
996
Steve McKayf8621552015-11-03 15:23:16 -0800997 public void pasteFromClipboard() {
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700998 copyFromClipboard();
999 getActivity().invalidateOptionsMenu();
Steve McKay3da8afc2015-05-05 14:50:00 -07001000 }
1001
Steve McKay3da8afc2015-05-05 14:50:00 -07001002 /**
1003 * Returns true if the list of files can be copied to destination. Note that this
1004 * is a policy check only. Currently the method does not attempt to verify
1005 * available space or any other environmental aspects possibly resulting in
1006 * failure to copy.
1007 *
1008 * @return true if the list of files can be copied to destination.
1009 */
Steve McKay4a1ca862016-02-17 18:25:47 -08001010 private boolean canCopy(List<DocumentInfo> files, RootInfo root, DocumentInfo dest) {
1011 if (dest == null || !dest.isDirectory() || !dest.isCreateSupported()) {
1012 return false;
1013 }
Steve McKay3da8afc2015-05-05 14:50:00 -07001014
Steve McKay008e9482016-02-18 15:32:16 -08001015 // Can't copy folders to downloads, because we don't show folders there.
1016 if (!root.isDownloads()) {
Steve McKay3da8afc2015-05-05 14:50:00 -07001017 for (DocumentInfo docs : files) {
1018 if (docs.isDirectory()) {
1019 return false;
1020 }
1021 }
1022 }
1023
Steve McKay4a1ca862016-02-17 18:25:47 -08001024 return true;
Steve McKay3da8afc2015-05-05 14:50:00 -07001025 }
1026
Steve McKayf8621552015-11-03 15:23:16 -08001027 public void selectAllFiles() {
Ben Kwa862b96412015-12-07 13:25:27 -08001028 // Only select things currently visible in the adapter.
1029 boolean changed = mSelectionManager.setItemsSelected(mAdapter.getModelIds(), true);
Steve McKayb04b1642015-07-24 13:14:20 -07001030 if (changed) {
1031 updateDisplayState();
1032 }
Steve McKay3da8afc2015-05-05 14:50:00 -07001033 }
1034
Ben Kwa2036dad2016-02-10 07:46:35 -08001035 /**
1036 * Attempts to restore focus on the directory listing.
1037 */
1038 public void requestFocus() {
1039 mFocusManager.restoreLastFocus();
1040 }
1041
Steve McKay4b3a13c2015-06-11 10:10:49 -07001042 private void setupDragAndDropOnDirectoryView(View view) {
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001043 // Listen for drops on non-directory items and empty space.
1044 view.setOnDragListener(mOnDragListener);
1045 }
1046
1047 private void setupDragAndDropOnDocumentView(View view, Cursor cursor) {
1048 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
1049 if (Document.MIME_TYPE_DIR.equals(docMimeType)) {
1050 // Make a directory item a drop target. Drop on non-directories and empty space
1051 // is handled at the list/grid view level.
1052 view.setOnDragListener(mOnDragListener);
1053 }
1054
Ben Kwae3dfebf2016-02-18 16:45:45 -08001055 view.setOnLongClickListener(mDragHelper);
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001056 }
1057
1058 private View.OnDragListener mOnDragListener = new View.OnDragListener() {
1059 @Override
1060 public boolean onDrag(View v, DragEvent event) {
1061 switch (event.getAction()) {
1062 case DragEvent.ACTION_DRAG_STARTED:
1063 // TODO: Check if the event contains droppable data.
1064 return true;
1065
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001066 // TODO: Expand drop target directory on hover?
1067 case DragEvent.ACTION_DRAG_ENTERED:
Ben Kwa1b685e52016-02-24 10:05:53 -08001068 setDropTargetHighlight(v, true);
Ben Kwae3dfebf2016-02-18 16:45:45 -08001069 return true;
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001070 case DragEvent.ACTION_DRAG_EXITED:
Ben Kwa1b685e52016-02-24 10:05:53 -08001071 setDropTargetHighlight(v, false);
Ben Kwae3dfebf2016-02-18 16:45:45 -08001072 return true;
1073
1074 case DragEvent.ACTION_DRAG_LOCATION:
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001075 case DragEvent.ACTION_DRAG_ENDED:
1076 return true;
1077
1078 case DragEvent.ACTION_DROP:
Ben Kwa003a9992015-11-30 23:00:02 -08001079 String dstId = getModelId(v);
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001080 DocumentInfo dstDir = null;
Ben Kwa003a9992015-11-30 23:00:02 -08001081 if (dstId != null) {
1082 Cursor dstCursor = mModel.getItem(dstId);
Steve McKay4b3a13c2015-06-11 10:10:49 -07001083 checkNotNull(dstCursor, "Cursor cannot be null.");
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001084 dstDir = DocumentInfo.fromDirectoryCursor(dstCursor);
1085 // TODO: Do not drop into the directory where the documents came from.
1086 }
1087 copyFromClipData(event.getClipData(), dstDir);
Ben Kwa1b685e52016-02-24 10:05:53 -08001088 // Clean up the UI.
1089 setDropTargetHighlight(v, false);
1090 mSelectionManager.clearSelection();
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001091 return true;
1092 }
1093 return false;
1094 }
Ben Kwae3dfebf2016-02-18 16:45:45 -08001095
Ben Kwa1b685e52016-02-24 10:05:53 -08001096 private void setDropTargetHighlight(View v, boolean highlight) {
Ben Kwae3dfebf2016-02-18 16:45:45 -08001097 // Note: use exact comparison - this code is searching for views which are children of
1098 // the RecyclerView instance in the UI.
1099 if (v.getParent() == mRecView) {
1100 RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(v);
1101 if (vh instanceof DocumentHolder) {
1102 ((DocumentHolder) vh).setHighlighted(highlight);
1103 }
1104 }
1105 }
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001106 };
1107
Ben Kwa003a9992015-11-30 23:00:02 -08001108 /**
1109 * Gets the model ID for a given motion event (using the event position)
1110 */
1111 private String getModelId(MotionEvent e) {
1112 View view = mRecView.findChildViewUnder(e.getX(), e.getY());
1113 if (view == null) {
1114 return null;
1115 }
1116 RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(view);
1117 if (vh instanceof DocumentHolder) {
1118 return ((DocumentHolder) vh).modelId;
1119 } else {
1120 return null;
1121 }
1122 }
1123
1124 /**
1125 * Gets the model ID for a given RecyclerView item.
1126 * @param view A View that is a document item view, or a child of a document item view.
1127 * @return The Model ID for the given document, or null if the given view is not associated with
1128 * a document item view.
1129 */
1130 private String getModelId(View view) {
Ben Kwae3dfebf2016-02-18 16:45:45 -08001131 View itemView = mRecView.findContainingItemView(view);
1132 if (itemView != null) {
1133 RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(itemView);
1134 if (vh instanceof DocumentHolder) {
1135 return ((DocumentHolder) vh).modelId;
Vladislav Kaznacheevb5999ef2015-09-04 09:17:37 -07001136 }
Vladislav Kaznacheevb5999ef2015-09-04 09:17:37 -07001137 }
Ben Kwae3dfebf2016-02-18 16:45:45 -08001138 return null;
Vladislav Kaznacheevb5999ef2015-09-04 09:17:37 -07001139 }
1140
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001141 private List<DocumentInfo> getDraggableDocuments(View currentItemView) {
Ben Kwa003a9992015-11-30 23:00:02 -08001142 String modelId = getModelId(currentItemView);
1143 if (modelId == null) {
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001144 return Collections.EMPTY_LIST;
1145 }
1146
Ben Kwa8250db42015-10-07 14:15:12 -07001147 final List<DocumentInfo> selectedDocs =
1148 mModel.getDocuments(mSelectionManager.getSelection());
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001149 if (!selectedDocs.isEmpty()) {
Ben Kwa003a9992015-11-30 23:00:02 -08001150 if (!isSelected(modelId)) {
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001151 // There is a selection that does not include the current item, drag nothing.
1152 return Collections.EMPTY_LIST;
1153 }
1154 return selectedDocs;
1155 }
1156
Ben Kwa003a9992015-11-30 23:00:02 -08001157 final Cursor cursor = mModel.getItem(modelId);
Steve McKay4b3a13c2015-06-11 10:10:49 -07001158 checkNotNull(cursor, "Cursor cannot be null.");
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001159 final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
Steve McKay58efce32015-08-20 16:19:38 +00001160
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001161 return Lists.newArrayList(doc);
1162 }
1163
1164 private Drawable getDragShadowIcon(List<DocumentInfo> docs) {
1165 if (docs.size() == 1) {
1166 final DocumentInfo doc = docs.get(0);
Ben Kwad8391492015-12-17 10:37:00 -08001167 return mIconHelper.getDocumentIcon(getActivity(), doc.authority, doc.documentId,
1168 doc.mimeType, doc.icon);
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001169 }
1170 return getActivity().getDrawable(R.drawable.ic_doc_generic);
1171 }
1172
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001173 private class DrawableShadowBuilder extends View.DragShadowBuilder {
1174
1175 private final Drawable mShadow;
1176
1177 private final int mShadowDimension;
1178
1179 public DrawableShadowBuilder(Drawable shadow) {
1180 mShadow = shadow;
1181 mShadowDimension = getResources().getDimensionPixelSize(
1182 R.dimen.drag_shadow_size);
1183 mShadow.setBounds(0, 0, mShadowDimension, mShadowDimension);
1184 }
1185
Ben Kwac64cb252015-08-27 16:04:46 -07001186 @Override
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001187 public void onProvideShadowMetrics(
1188 Point shadowSize, Point shadowTouchPoint) {
1189 shadowSize.set(mShadowDimension, mShadowDimension);
1190 shadowTouchPoint.set(mShadowDimension / 2, mShadowDimension / 2);
1191 }
1192
Ben Kwac64cb252015-08-27 16:04:46 -07001193 @Override
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001194 public void onDrawShadow(Canvas canvas) {
1195 mShadow.draw(canvas);
1196 }
1197 }
Steve McKaybdbd0ff2015-05-20 15:58:42 -07001198
Steve McKaybdbd0ff2015-05-20 15:58:42 -07001199 /**
Steve McKayab7865c2015-05-27 16:11:42 -07001200 * Abstract task providing support for loading documents *off*
1201 * the main thread. And if it isn't obvious, creating a list
1202 * of documents (especially large lists) can be pretty expensive.
1203 */
1204 private abstract class GetDocumentsTask
Steve McKay4b3a13c2015-06-11 10:10:49 -07001205 extends AsyncTask<Selection, Void, List<DocumentInfo>> {
Steve McKayab7865c2015-05-27 16:11:42 -07001206 @Override
Steve McKay4b3a13c2015-06-11 10:10:49 -07001207 protected final List<DocumentInfo> doInBackground(Selection... selected) {
Ben Kwac64cb252015-08-27 16:04:46 -07001208 return mModel.getDocuments(selected[0]);
Steve McKayab7865c2015-05-27 16:11:42 -07001209 }
1210
1211 @Override
1212 protected final void onPostExecute(List<DocumentInfo> docs) {
1213 onDocumentsReady(docs);
1214 }
1215
1216 abstract void onDocumentsReady(List<DocumentInfo> docs);
1217 }
1218
Steve McKayef16f5f2015-12-22 18:15:31 -08001219 @Override
1220 public boolean isSelected(String modelId) {
Ben Kwa743c7c22015-12-01 19:56:57 -08001221 return mSelectionManager.getSelection().contains(modelId);
Ben Kwa91caed82015-09-21 10:49:52 -07001222 }
Ben Kwa18fce3c2015-09-01 11:03:01 -07001223
Ben Kwa0436a752016-01-15 10:43:24 -08001224 private class ItemEventListener implements DocumentHolder.EventListener {
Ben Kwabd964562015-10-14 08:00:27 -07001225 @Override
Ben Kwa0436a752016-01-15 10:43:24 -08001226 public boolean onActivate(DocumentHolder doc) {
Ben Kwa67924892016-01-27 09:58:36 -08001227 // Toggle selection if we're in selection mode, othewise, view item.
1228 if (mSelectionManager.hasSelection()) {
1229 mSelectionManager.toggleSelection(doc.modelId);
1230 } else {
1231 handleViewItem(doc.modelId);
1232 }
Ben Kwa0436a752016-01-15 10:43:24 -08001233 return true;
1234 }
1235
1236 @Override
1237 public boolean onSelect(DocumentHolder doc) {
1238 mSelectionManager.toggleSelection(doc.modelId);
1239 mSelectionManager.setSelectionRangeBegin(doc.getAdapterPosition());
1240 return true;
Ben Kwabd964562015-10-14 08:00:27 -07001241 }
Ben Kwa67924892016-01-27 09:58:36 -08001242
1243 @Override
1244 public boolean onKey(DocumentHolder doc, int keyCode, KeyEvent event) {
1245 // Only handle key-down events. This is simpler, consistent with most other UIs, and
1246 // enables the handling of repeated key events from holding down a key.
1247 if (event.getAction() != KeyEvent.ACTION_DOWN) {
1248 return false;
1249 }
1250
Ben Kwa22937c92016-02-23 23:00:01 -08001251 // Ignore tab key events. Those should be handled by the top-level key handler.
1252 if (keyCode == KeyEvent.KEYCODE_TAB) {
1253 return false;
1254 }
1255
Ben Kwab0bfe2d2016-02-09 11:27:45 -08001256 if (mFocusManager.handleKey(doc, keyCode, event)) {
Ben Kwa83df50f2016-02-10 14:01:19 -08001257 // Handle range selection adjustments. Extending the selection will adjust the
1258 // bounds of the in-progress range selection. Each time an unshifted navigation
1259 // event is received, the range selection is restarted.
1260 if (shouldExtendSelection(event)) {
1261 if (!mSelectionManager.isRangeSelectionActive()) {
1262 // Start a range selection if one isn't active
1263 mSelectionManager.startRangeSelection(doc.getAdapterPosition());
1264 }
1265 mSelectionManager.snapRangeSelection(mFocusManager.getFocusPosition());
1266 } else {
1267 mSelectionManager.endRangeSelection();
1268 }
Ben Kwab0bfe2d2016-02-09 11:27:45 -08001269 return true;
Ben Kwa67924892016-01-27 09:58:36 -08001270 }
1271
Ben Kwab0bfe2d2016-02-09 11:27:45 -08001272 // Handle enter key events
1273 if (keyCode == KeyEvent.KEYCODE_ENTER) {
Ben Kwa09eb77b2016-02-10 08:57:25 -08001274 if (event.isShiftPressed()) {
1275 return onSelect(doc);
1276 } else {
1277 return onActivate(doc);
1278 }
Ben Kwab0bfe2d2016-02-09 11:27:45 -08001279 }
1280
1281 return false;
Ben Kwa67924892016-01-27 09:58:36 -08001282 }
Ben Kwa83df50f2016-02-10 14:01:19 -08001283
1284 private boolean shouldExtendSelection(KeyEvent event) {
1285 return Events.isNavigationKeyCode(event.getKeyCode()) &&
1286 event.isShiftPressed();
1287 }
Ben Kwabd964562015-10-14 08:00:27 -07001288 }
1289
Steve McKayef16f5f2015-12-22 18:15:31 -08001290 private final class ModelUpdateListener implements Model.UpdateListener {
Ben Kwa91caed82015-09-21 10:49:52 -07001291 @Override
1292 public void onModelUpdate(Model model) {
1293 if (model.info != null || model.error != null) {
1294 mMessageBar.setInfo(model.info);
1295 mMessageBar.setError(model.error);
1296 mMessageBar.show();
1297 }
Ben Kwa18fce3c2015-09-01 11:03:01 -07001298
Ben Kwa91caed82015-09-21 10:49:52 -07001299 mProgressBar.setVisibility(model.isLoading() ? View.VISIBLE : View.GONE);
1300
1301 if (model.isEmpty()) {
Aga Wronska893390b2016-02-17 13:50:42 -08001302 if (mSearchMode) {
Steve McKay9ed88a42016-01-21 18:46:15 -08001303 showNoResults(getDisplayState().stack.root);
1304 } else {
1305 showEmptyDirectory();
1306 }
Ben Kwa91caed82015-09-21 10:49:52 -07001307 } else {
Steve McKay9ed88a42016-01-21 18:46:15 -08001308 showDirectory();
Ben Kwa91bec532015-09-16 13:15:38 -07001309 mAdapter.notifyDataSetChanged();
Ben Kwa91caed82015-09-21 10:49:52 -07001310 }
Ben Kwa91caed82015-09-21 10:49:52 -07001311 }
1312
1313 @Override
1314 public void onModelUpdateFailed(Exception e) {
Steve McKay9ed88a42016-01-21 18:46:15 -08001315 showQueryError();
Ben Kwa18fce3c2015-09-01 11:03:01 -07001316 }
Ben Kwac64cb252015-08-27 16:04:46 -07001317 }
Ben Kwa0436a752016-01-15 10:43:24 -08001318
Ben Kwae3dfebf2016-02-18 16:45:45 -08001319 private DragStartHelper mDragHelper = new DragStartHelper(null) {
Steve McKay41585d52016-01-21 15:10:39 -08001320 @Override
Ben Kwae3dfebf2016-02-18 16:45:45 -08001321 protected boolean onDragStart(View v) {
1322 if (isSelected(getModelId(v))) {
Steve McKay41585d52016-01-21 15:10:39 -08001323 List<DocumentInfo> docs = getDraggableDocuments(v);
1324 if (docs.isEmpty()) {
1325 return false;
1326 }
1327 v.startDrag(
1328 getClipDataFromDocuments(docs),
1329 new DrawableShadowBuilder(getDragShadowIcon(docs)),
1330 null,
1331 View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ |
1332 View.DRAG_FLAG_GLOBAL_URI_WRITE
1333 );
1334 return true;
1335 }
1336
1337 return false;
1338 }
1339 };
1340
1341 // Previously we listened to events with one class, only to bounce them forward
1342 // to GestureDetector. We're still doing that here, but with a single class
1343 // that reduces overall complexity in our glue code.
1344 private static final class ListeningGestureDetector extends GestureDetector
1345 implements OnItemTouchListener {
1346
1347 private int mLastTool = -1;
Ben Kwae3dfebf2016-02-18 16:45:45 -08001348 private DragStartHelper mDragHelper;
Steve McKay41585d52016-01-21 15:10:39 -08001349
Ben Kwae3dfebf2016-02-18 16:45:45 -08001350 public ListeningGestureDetector(
1351 Context context, DragStartHelper dragHelper, GestureListener listener) {
Steve McKay41585d52016-01-21 15:10:39 -08001352 super(context, listener);
Ben Kwae3dfebf2016-02-18 16:45:45 -08001353 mDragHelper = dragHelper;
Steve McKay41585d52016-01-21 15:10:39 -08001354 setOnDoubleTapListener(listener);
1355 }
1356
1357 boolean mouseSpawnedLastEvent() {
1358 return Events.isMouseType(mLastTool);
1359 }
1360
1361 boolean touchSpawnedLastEvent() {
1362 return Events.isTouchType(mLastTool);
1363 }
1364
1365 @Override
1366 public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
1367 mLastTool = e.getToolType(0);
Ben Kwae3dfebf2016-02-18 16:45:45 -08001368
1369 // Detect drag events. When a drag is detected, intercept the rest of the gesture.
1370 View itemView = rv.findChildViewUnder(e.getX(), e.getY());
1371 if (itemView != null && mDragHelper.handleTouch(itemView, e)) {
1372 return true;
1373 }
1374 // Forward unhandled events to the GestureDetector.
1375 onTouchEvent(e);
1376
Steve McKay41585d52016-01-21 15:10:39 -08001377 return false;
1378 }
1379
1380 @Override
Ben Kwae3dfebf2016-02-18 16:45:45 -08001381 public void onTouchEvent(RecyclerView rv, MotionEvent e) {
1382 View itemView = rv.findChildViewUnder(e.getX(), e.getY());
1383 mDragHelper.handleTouch(itemView, e);
1384 // Note: even though this event is being handled as part of a drag gesture, continue
1385 // forwarding to the GestureDetector. The detector needs to see the entire cluster of
1386 // events in order to properly interpret gestures.
1387 onTouchEvent(e);
1388 }
Steve McKay41585d52016-01-21 15:10:39 -08001389
1390 @Override
1391 public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {}
1392 }
1393
Ben Kwa0436a752016-01-15 10:43:24 -08001394 /**
1395 * The gesture listener for items in the list/grid view. Interprets gestures and sends the
1396 * events to the target DocumentHolder, whence they are routed to the appropriate listener.
1397 */
1398 private class GestureListener extends GestureDetector.SimpleOnGestureListener {
1399 @Override
1400 public boolean onSingleTapUp(MotionEvent e) {
1401 // Single tap logic:
1402 // If the selection manager is active, it gets first whack at handling tap
1403 // events. Otherwise, tap events are routed to the target DocumentHolder.
1404 boolean handled = mSelectionManager.onSingleTapUp(
1405 new MotionInputEvent(e, mRecView));
1406
1407 if (handled) {
1408 return handled;
1409 }
1410
1411 // Give the DocumentHolder a crack at the event.
1412 DocumentHolder holder = getTarget(e);
1413 if (holder != null) {
1414 handled = holder.onSingleTapUp(e);
1415 }
1416
1417 return handled;
1418 }
1419
1420 @Override
1421 public void onLongPress(MotionEvent e) {
1422 // Long-press events get routed directly to the selection manager. They can be
1423 // changed to route through the DocumentHolder if necessary.
1424 mSelectionManager.onLongPress(new MotionInputEvent(e, mRecView));
1425 }
1426
1427 @Override
1428 public boolean onDoubleTap(MotionEvent e) {
1429 // Double-tap events are handled directly by the DirectoryFragment. They can be changed
1430 // to route through the DocumentHolder if necessary.
1431 return DirectoryFragment.this.onDoubleTap(e);
1432 }
1433
1434 private @Nullable DocumentHolder getTarget(MotionEvent e) {
1435 View childView = mRecView.findChildViewUnder(e.getX(), e.getY());
1436 if (childView != null) {
1437 return (DocumentHolder) mRecView.getChildViewHolder(childView);
1438 } else {
1439 return null;
1440 }
1441 }
1442 }
Steve McKay9ed88a42016-01-21 18:46:15 -08001443
1444 public static void showDirectory(
1445 FragmentManager fm, RootInfo root, DocumentInfo doc, int anim) {
Aga Wronska893390b2016-02-17 13:50:42 -08001446 create(fm, TYPE_NORMAL, root, doc, null, anim);
Steve McKay9ed88a42016-01-21 18:46:15 -08001447 }
1448
1449 public static void showRecentsOpen(FragmentManager fm, int anim) {
Aga Wronska893390b2016-02-17 13:50:42 -08001450 create(fm, TYPE_RECENT_OPEN, null, null, null, anim);
Steve McKay9ed88a42016-01-21 18:46:15 -08001451 }
1452
Aga Wronska893390b2016-02-17 13:50:42 -08001453 public static void reloadSearch(FragmentManager fm, RootInfo root, DocumentInfo doc,
1454 String query) {
1455 DirectoryFragment df = get(fm);
1456
1457 df.mQuery = query;
1458 df.mRoot = root;
1459 df.mDocument = doc;
1460 df.mSearchMode = query != null;
1461 df.getLoaderManager().restartLoader(LOADER_ID, null, df);
1462 }
1463
1464 public static void reload(FragmentManager fm, int type, RootInfo root, DocumentInfo doc,
1465 String query) {
1466 DirectoryFragment df = get(fm);
1467 df.mType = type;
1468 df.mQuery = query;
1469 df.mRoot = root;
1470 df.mDocument = doc;
1471 df.mSearchMode = query != null;
1472 df.getLoaderManager().restartLoader(LOADER_ID, null, df);
1473 }
1474
1475 public static void create(FragmentManager fm, int type, RootInfo root, DocumentInfo doc,
Steve McKay9ed88a42016-01-21 18:46:15 -08001476 String query, int anim) {
1477 final Bundle args = new Bundle();
Aga Wronska893390b2016-02-17 13:50:42 -08001478 args.putInt(Shared.EXTRA_TYPE, type);
1479 args.putParcelable(Shared.EXTRA_ROOT, root);
1480 args.putParcelable(Shared.EXTRA_DOC, doc);
1481 args.putString(Shared.EXTRA_QUERY, query);
Steve McKay9ed88a42016-01-21 18:46:15 -08001482
1483 final FragmentTransaction ft = fm.beginTransaction();
1484 switch (anim) {
1485 case ANIM_SIDE:
Aga Wronska893390b2016-02-17 13:50:42 -08001486 args.putBoolean(Shared.EXTRA_IGNORE_STATE, true);
Steve McKay9ed88a42016-01-21 18:46:15 -08001487 break;
Tomasz Mikolajewskie3fe9d72016-02-03 16:53:21 +09001488 case ANIM_ENTER:
Aga Wronska893390b2016-02-17 13:50:42 -08001489 args.putBoolean(Shared.EXTRA_IGNORE_STATE, true);
Tomasz Mikolajewskie3fe9d72016-02-03 16:53:21 +09001490 ft.setCustomAnimations(R.animator.dir_enter, R.animator.dir_frozen);
Steve McKay9ed88a42016-01-21 18:46:15 -08001491 break;
Tomasz Mikolajewskie3fe9d72016-02-03 16:53:21 +09001492 case ANIM_LEAVE:
1493 ft.setCustomAnimations(R.animator.dir_frozen, R.animator.dir_leave);
Steve McKay9ed88a42016-01-21 18:46:15 -08001494 break;
1495 }
1496
1497 final DirectoryFragment fragment = new DirectoryFragment();
1498 fragment.setArguments(args);
1499
Aga Wronska893390b2016-02-17 13:50:42 -08001500 ft.replace(getFragmentId(), fragment);
Steve McKay9ed88a42016-01-21 18:46:15 -08001501 ft.commitAllowingStateLoss();
1502 }
1503
1504 private static String buildStateKey(RootInfo root, DocumentInfo doc) {
1505 final StringBuilder builder = new StringBuilder();
1506 builder.append(root != null ? root.authority : "null").append(';');
1507 builder.append(root != null ? root.rootId : "null").append(';');
1508 builder.append(doc != null ? doc.documentId : "null");
1509 return builder.toString();
1510 }
1511
1512 public static @Nullable DirectoryFragment get(FragmentManager fm) {
1513 // TODO: deal with multiple directories shown at once
Aga Wronska893390b2016-02-17 13:50:42 -08001514 Fragment fragment = fm.findFragmentById(getFragmentId());
Steve McKay9ed88a42016-01-21 18:46:15 -08001515 return fragment instanceof DirectoryFragment
1516 ? (DirectoryFragment) fragment
1517 : null;
1518 }
Aga Wronska893390b2016-02-17 13:50:42 -08001519
1520 private static int getFragmentId() {
1521 return R.id.container_directory;
1522 }
1523
1524 @Override
1525 public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) {
1526 Context context = getActivity();
1527 State state = getDisplayState();
1528
1529 Uri contentsUri;
1530 switch (mType) {
1531 case TYPE_NORMAL:
1532 contentsUri = mSearchMode ? DocumentsContract.buildSearchDocumentsUri(
1533 mRoot.authority, mRoot.rootId, mQuery)
1534 : DocumentsContract.buildChildDocumentsUri(
1535 mDocument.authority, mDocument.documentId);
1536 if (state.action == ACTION_MANAGE) {
1537 contentsUri = DocumentsContract.setManageMode(contentsUri);
1538 }
1539 return new DirectoryLoader(
Steve McKay27d20a32016-02-22 18:38:09 -08001540 context, mType, mRoot, mDocument, contentsUri, state.userSortOrder,
1541 mSearchMode);
Aga Wronska893390b2016-02-17 13:50:42 -08001542 case TYPE_RECENT_OPEN:
1543 final RootsCache roots = DocumentsApplication.getRootsCache(context);
1544 return new RecentsLoader(context, roots, state);
1545 default:
1546 throw new IllegalStateException("Unknown type " + mType);
1547 }
1548 }
1549
1550 @Override
1551 public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
1552 if (!isAdded()) return;
1553
1554 State state = getDisplayState();
1555
1556 mAdapter.notifyDataSetChanged();
1557 mModel.update(result);
1558
1559 state.derivedSortOrder = result.sortOrder;
1560
1561 updateLayout(state.derivedMode);
1562
1563 if (mSelection != null) {
1564 mSelectionManager.setItemsSelected(mSelection.toList(), true);
1565 }
1566
1567 // Restore any previous instance state
1568 final SparseArray<Parcelable> container = state.dirState.remove(mStateKey);
1569 if (container != null && !getArguments().getBoolean(Shared.EXTRA_IGNORE_STATE, false)) {
1570 getView().restoreHierarchyState(container);
1571 } else if (mLastSortOrder != state.derivedSortOrder) {
1572 // The derived sort order takes the user sort order into account, but applies
1573 // directory-specific defaults when the user doesn't explicitly set the sort
1574 // order. Scroll to the top if the sort order actually changed.
1575 mRecView.smoothScrollToPosition(0);
1576 }
1577
1578 mLastSortOrder = state.derivedSortOrder;
1579
1580 mTuner.onModelLoaded(mModel, mType, mSearchMode);
1581
1582 }
1583
1584 @Override
1585 public void onLoaderReset(Loader<DirectoryResult> loader) {
1586 mModel.update(null);
1587 }
1588 }