blob: 20316fff31b4d6fd5b89bffa0f0ded2e4bdf883c [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.MODE_GRID;
21import static com.android.documentsui.State.MODE_LIST;
Steve McKay7a3b88c2015-09-23 17:21:40 -070022import static com.android.documentsui.State.SORT_ORDER_UNKNOWN;
Jeff Sharkey5dfb3452013-08-31 21:27:44 -070023import static com.android.documentsui.model.DocumentInfo.getCursorInt;
Jeff Sharkey5dfb3452013-08-31 21:27:44 -070024import static com.android.documentsui.model.DocumentInfo.getCursorString;
Steve McKay3da8afc2015-05-05 14:50:00 -070025
Steve McKayaa15dae2016-02-09 16:17:24 -080026import android.annotation.IntDef;
Steve McKay9ed88a42016-01-21 18:46:15 -080027import android.annotation.StringRes;
Ben Kwaef3f2622015-04-07 15:43:39 -070028import android.app.Activity;
Jeff Sharkeyd01571e2013-10-01 17:57:41 -070029import android.app.ActivityManager;
Steve McKay7a3b8112016-02-23 10:06:50 -080030import android.app.AlertDialog;
Jeff Sharkey54e55b72013-06-30 20:02:59 -070031import android.app.Fragment;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070032import android.app.FragmentManager;
33import android.app.FragmentTransaction;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070034import android.app.LoaderManager.LoaderCallbacks;
Vladislav Kaznacheev335dba52015-05-01 13:46:57 -070035import android.content.ClipData;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070036import android.content.Context;
Steve McKay7a3b8112016-02-23 10:06:50 -080037import android.content.DialogInterface;
Jeff Sharkey4eb407a2013-08-18 17:38:20 -070038import android.content.Intent;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070039import android.content.Loader;
Jeff Sharkey5dfb3452013-08-31 21:27:44 -070040import android.database.Cursor;
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -070041import android.graphics.Canvas;
Jeff Sharkey9d0843d2013-05-07 12:41:33 -070042import android.graphics.Point;
Jeff Sharkey4ec97392013-09-10 12:04:26 -070043import android.graphics.drawable.Drawable;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070044import android.net.Uri;
Jeff Sharkey9d0843d2013-05-07 12:41:33 -070045import android.os.AsyncTask;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070046import android.os.Bundle;
Jeff Sharkeye20a3ac2013-09-18 16:26:49 -070047import android.os.Parcelable;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070048import android.provider.DocumentsContract;
Jeff Sharkey5dfb3452013-08-31 21:27:44 -070049import android.provider.DocumentsContract.Document;
Ben Kwac64cb252015-08-27 16:04:46 -070050import android.support.annotation.Nullable;
Ben Kwa304895a2015-08-27 16:06:33 -070051import android.support.design.widget.Snackbar;
Ben Kwae3dfebf2016-02-18 16:45:45 -080052import android.support.v13.view.DragStartHelper;
Steve McKay4b3a13c2015-06-11 10:10:49 -070053import android.support.v7.widget.GridLayoutManager;
Steve McKayef16f5f2015-12-22 18:15:31 -080054import android.support.v7.widget.GridLayoutManager.SpanSizeLookup;
Steve McKay4b3a13c2015-06-11 10:10:49 -070055import android.support.v7.widget.RecyclerView;
Steve McKay669ebe72015-10-19 12:04:21 -070056import android.support.v7.widget.RecyclerView.OnItemTouchListener;
Steve McKay4b3a13c2015-06-11 10:10:49 -070057import android.support.v7.widget.RecyclerView.RecyclerListener;
58import android.support.v7.widget.RecyclerView.ViewHolder;
Jeff Sharkeyf491c722015-06-11 09:16:19 -070059import android.text.TextUtils;
Jeff Sharkey9d0843d2013-05-07 12:41:33 -070060import android.util.Log;
Jeff Sharkeye20a3ac2013-09-18 16:26:49 -070061import android.util.SparseArray;
Ben Kwa0f7078f02015-09-08 07:31:19 -070062import android.util.TypedValue;
Jeff Sharkeybe8b12e2013-07-01 16:56:54 -070063import android.view.ActionMode;
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -070064import android.view.DragEvent;
Steve McKay4b3a13c2015-06-11 10:10:49 -070065import android.view.GestureDetector;
Aga Wronska7d5a2472016-03-08 17:56:28 -080066import android.view.HapticFeedbackConstants;
Ben Kwa67924892016-01-27 09:58:36 -080067import android.view.KeyEvent;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070068import android.view.LayoutInflater;
Jeff Sharkey54e55b72013-06-30 20:02:59 -070069import android.view.Menu;
Jeff Sharkey54e55b72013-06-30 20:02:59 -070070import android.view.MenuItem;
Steve McKay4b3a13c2015-06-11 10:10:49 -070071import android.view.MotionEvent;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070072import android.view.View;
73import android.view.ViewGroup;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070074import android.widget.ImageView;
Tomasz Mikolajewski1e479ab2016-03-22 13:30:59 +090075import android.widget.Toolbar;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070076import android.widget.TextView;
77
Steve McKayf8621552015-11-03 15:23:16 -080078import com.android.documentsui.BaseActivity;
Steve McKayf8621552015-11-03 15:23:16 -080079import com.android.documentsui.DirectoryLoader;
80import com.android.documentsui.DirectoryResult;
81import com.android.documentsui.DocumentClipper;
82import com.android.documentsui.DocumentsActivity;
83import com.android.documentsui.DocumentsApplication;
84import com.android.documentsui.Events;
Ben Kwa0436a752016-01-15 10:43:24 -080085import com.android.documentsui.Events.MotionInputEvent;
Steve McKayf8621552015-11-03 15:23:16 -080086import com.android.documentsui.Menus;
87import com.android.documentsui.MessageBar;
Aga Wronskac573bec2016-03-31 16:37:40 -070088import com.android.documentsui.Metrics;
Steve McKayf8621552015-11-03 15:23:16 -080089import com.android.documentsui.R;
Steve McKaye2af0782016-02-18 09:48:39 -080090import com.android.documentsui.RecentsLoader;
Steve McKayf8621552015-11-03 15:23:16 -080091import com.android.documentsui.RootsCache;
92import com.android.documentsui.Shared;
93import com.android.documentsui.Snackbars;
94import com.android.documentsui.State;
Ben Kwa1e2fa5e2016-02-02 23:00:02 -080095import com.android.documentsui.State.ViewMode;
Steve McKayf8621552015-11-03 15:23:16 -080096import com.android.documentsui.dirlist.MultiSelectManager.Selection;
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -070097import com.android.documentsui.model.DocumentInfo;
Tomasz Mikolajewskif8c3f322015-04-14 16:32:41 +090098import com.android.documentsui.model.DocumentStack;
Jeff Sharkey251097b2013-09-02 15:07:28 -070099import com.android.documentsui.model.RootInfo;
Steve McKay14e827a2016-01-06 18:32:13 -0800100import com.android.documentsui.services.FileOperationService;
Ben Kwa1e2fa5e2016-02-02 23:00:02 -0800101import com.android.documentsui.services.FileOperationService.OpType;
Steve McKay14e827a2016-01-06 18:32:13 -0800102import com.android.documentsui.services.FileOperations;
Steve McKay58efce32015-08-20 16:19:38 +0000103import com.google.common.collect.Lists;
Jeff Sharkeybe8b12e2013-07-01 16:56:54 -0700104
Steve McKayaa15dae2016-02-09 16:17:24 -0800105import java.lang.annotation.Retention;
106import java.lang.annotation.RetentionPolicy;
Jeff Sharkeybe8b12e2013-07-01 16:56:54 -0700107import java.util.ArrayList;
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -0700108import java.util.Collections;
Jeff Sharkeyef7184a2013-08-05 17:56:48 -0700109import java.util.List;
Ben Kwa990489d2016-02-25 14:10:40 -0800110import java.util.Objects;
Jeff Sharkey54e55b72013-06-30 20:02:59 -0700111
112/**
113 * Display the documents inside a single directory.
114 */
Aga Wronska893390b2016-02-17 13:50:42 -0800115public class DirectoryFragment extends Fragment
116 implements DocumentsAdapter.Environment, LoaderCallbacks<DirectoryResult> {
Jeff Sharkey54e55b72013-06-30 20:02:59 -0700117
Steve McKayaa15dae2016-02-09 16:17:24 -0800118 @IntDef(flag = true, value = {
119 TYPE_NORMAL,
Steve McKayaa15dae2016-02-09 16:17:24 -0800120 TYPE_RECENT_OPEN
121 })
122 @Retention(RetentionPolicy.SOURCE)
123 public @interface ResultType {}
Jeff Sharkeyef7184a2013-08-05 17:56:48 -0700124 public static final int TYPE_NORMAL = 1;
Aga Wronska893390b2016-02-17 13:50:42 -0800125 public static final int TYPE_RECENT_OPEN = 2;
Jeff Sharkeydc2963a2013-08-02 15:55:26 -0700126
Aga Wronska893390b2016-02-17 13:50:42 -0800127 @IntDef(flag = true, value = {
Steve McKaya6bbeab2016-02-17 15:02:01 -0800128 REQUEST_COPY_DESTINATION
129 })
130 @Retention(RetentionPolicy.SOURCE)
131 public @interface RequestCode {}
Ben Kwaef3f2622015-04-07 15:43:39 -0700132 public static final int REQUEST_COPY_DESTINATION = 1;
133
Steve McKay35645432016-01-20 15:09:35 -0800134 private static final String TAG = "DirectoryFragment";
135 private static final int LOADER_ID = 42;
Steve McKay35645432016-01-20 15:09:35 -0800136
Ben Kwa18fce3c2015-09-01 11:03:01 -0700137 private Model mModel;
Ben Kwa8250db42015-10-07 14:15:12 -0700138 private MultiSelectManager mSelectionManager;
Ben Kwa91caed82015-09-21 10:49:52 -0700139 private Model.UpdateListener mModelUpdateListener = new ModelUpdateListener();
Ben Kwa0436a752016-01-15 10:43:24 -0800140 private ItemEventListener mItemEventListener = new ItemEventListener();
Ben Kwab0bfe2d2016-02-09 11:27:45 -0800141 private FocusManager mFocusManager;
Ben Kwac64cb252015-08-27 16:04:46 -0700142
Ben Kwad8391492015-12-17 10:37:00 -0800143 private IconHelper mIconHelper;
144
Steve McKay4b3a13c2015-06-11 10:10:49 -0700145 private View mEmptyView;
146 private RecyclerView mRecView;
Steve McKay41585d52016-01-21 15:10:39 -0800147 private ListeningGestureDetector mGestureDetector;
Steve McKay4b3a13c2015-06-11 10:10:49 -0700148
Steve McKay4b3a13c2015-06-11 10:10:49 -0700149 private String mStateKey;
150
Steve McKay4b3a13c2015-06-11 10:10:49 -0700151 private int mLastSortOrder = SORT_ORDER_UNKNOWN;
Steve McKay4b3a13c2015-06-11 10:10:49 -0700152 private DocumentsAdapter mAdapter;
Steve McKayf69502c2015-11-09 17:38:35 +0900153 private FragmentTuner mTuner;
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700154 private DocumentClipper mClipper;
Ben Kwa1e2fa5e2016-02-02 23:00:02 -0800155 private GridLayoutManager mLayout;
Steve McKay0ba3a142015-07-23 16:33:41 -0700156 private int mColumnCount = 1; // This will get updated when layout changes.
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700157
Steve McKayc8d4e222016-03-01 08:28:02 -0800158 private LayoutInflater mInflater;
Ben Kwa3b19e312015-09-16 08:04:37 -0700159 private MessageBar mMessageBar;
Ben Kwa91caed82015-09-21 10:49:52 -0700160 private View mProgressBar;
Ben Kwa3b19e312015-09-16 08:04:37 -0700161
Aga Wronska893390b2016-02-17 13:50:42 -0800162 // Directory fragment state is defined by: root, document, query, type, selection
163 private @ResultType int mType = TYPE_NORMAL;
164 private RootInfo mRoot;
165 private DocumentInfo mDocument;
166 private String mQuery = null;
167 private Selection mSelection = null;
168 private boolean mSearchMode = false;
Ben Kwaeb61aac2016-03-14 15:12:12 -0700169 private @Nullable ActionMode mActionMode;
Aga Wronska893390b2016-02-17 13:50:42 -0800170
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700171 @Override
172 public View onCreateView(
173 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
Steve McKayc8d4e222016-03-01 08:28:02 -0800174 mInflater = inflater;
Jeff Sharkey54e55b72013-06-30 20:02:59 -0700175 final View view = inflater.inflate(R.layout.fragment_directory, container, false);
176
Ben Kwa3b19e312015-09-16 08:04:37 -0700177 mMessageBar = MessageBar.create(getChildFragmentManager());
Ben Kwa91caed82015-09-21 10:49:52 -0700178 mProgressBar = view.findViewById(R.id.progressbar);
Jeff Sharkey9fb567b2013-08-07 16:22:02 -0700179 mEmptyView = view.findViewById(android.R.id.empty);
Ben Kwa2036dad2016-02-10 07:46:35 -0800180 mRecView = (RecyclerView) view.findViewById(R.id.dir_list);
Steve McKay4b3a13c2015-06-11 10:10:49 -0700181 mRecView.setRecyclerListener(
182 new RecyclerListener() {
183 @Override
184 public void onViewRecycled(ViewHolder holder) {
185 cancelThumbnailTask(holder.itemView);
186 }
187 });
Steve McKayb46383b2015-05-06 14:27:57 -0700188
Ben Kwae48e4ca2015-10-20 15:02:33 -0700189 mRecView.setItemAnimator(new DirectoryItemAnimator(getActivity()));
Ben Kwabd964562015-10-14 08:00:27 -0700190
Ben Kwa990489d2016-02-25 14:10:40 -0800191 // Make the recycler and the empty views responsive to drop events.
192 mRecView.setOnDragListener(mOnDragListener);
193 mEmptyView.setOnDragListener(mOnDragListener);
Jeff Sharkey54e55b72013-06-30 20:02:59 -0700194
Jeff Sharkey5dfb3452013-08-31 21:27:44 -0700195 return view;
196 }
Jeff Sharkey54e55b72013-06-30 20:02:59 -0700197
Jeff Sharkey5dfb3452013-08-31 21:27:44 -0700198 @Override
Jeff Sharkeyd01571e2013-10-01 17:57:41 -0700199 public void onDestroyView() {
Aga Wronskad5597432016-02-26 11:36:07 -0800200 mSelectionManager.clearSelection();
Jeff Sharkeyd01571e2013-10-01 17:57:41 -0700201
202 // Cancel any outstanding thumbnail requests
Steve McKay4b3a13c2015-06-11 10:10:49 -0700203 final int count = mRecView.getChildCount();
Jeff Sharkeyd01571e2013-10-01 17:57:41 -0700204 for (int i = 0; i < count; i++) {
Steve McKay4b3a13c2015-06-11 10:10:49 -0700205 final View view = mRecView.getChildAt(i);
206 cancelThumbnailTask(view);
Jeff Sharkeyd01571e2013-10-01 17:57:41 -0700207 }
Aga Wronskad5597432016-02-26 11:36:07 -0800208
209 super.onDestroyView();
Jeff Sharkeyd01571e2013-10-01 17:57:41 -0700210 }
211
212 @Override
Jeff Sharkey5dfb3452013-08-31 21:27:44 -0700213 public void onActivityCreated(Bundle savedInstanceState) {
214 super.onActivityCreated(savedInstanceState);
215
216 final Context context = getActivity();
Steve McKayf69502c2015-11-09 17:38:35 +0900217 final State state = getDisplayState();
Jeff Sharkey5dfb3452013-08-31 21:27:44 -0700218
Aga Wronska893390b2016-02-17 13:50:42 -0800219 // Read arguments when object created for the first time.
220 // Restore state if fragment recreated.
221 Bundle args = savedInstanceState == null ? getArguments() : savedInstanceState;
222 mRoot = args.getParcelable(Shared.EXTRA_ROOT);
223 mDocument = args.getParcelable(Shared.EXTRA_DOC);
224 mStateKey = buildStateKey(mRoot, mDocument);
225 mQuery = args.getString(Shared.EXTRA_QUERY);
226 mType = args.getInt(Shared.EXTRA_TYPE);
227 mSelection = args.getParcelable(Shared.EXTRA_SELECTION);
228 mSearchMode = args.getBoolean(Shared.EXTRA_SEARCH_MODE);
Jeff Sharkeyf6db1542013-09-13 13:42:19 -0700229
Steve McKay3eb2d072016-01-25 19:00:22 -0800230 mIconHelper = new IconHelper(context, MODE_GRID);
Steve McKayef16f5f2015-12-22 18:15:31 -0800231
232 mAdapter = new SectionBreakDocumentsAdapterWrapper(
233 this, new ModelBackedDocumentsAdapter(this, mIconHelper));
234
Steve McKay4b3a13c2015-06-11 10:10:49 -0700235 mRecView.setAdapter(mAdapter);
236
Ben Kwa1e2fa5e2016-02-02 23:00:02 -0800237 mLayout = new GridLayoutManager(getContext(), mColumnCount);
238 SpanSizeLookup lookup = mAdapter.createSpanSizeLookup();
239 if (lookup != null) {
240 mLayout.setSpanSizeLookup(lookup);
241 }
242 mRecView.setLayoutManager(mLayout);
243
Ben Kwae3dfebf2016-02-18 16:45:45 -0800244 mGestureDetector =
245 new ListeningGestureDetector(this.getContext(), mDragHelper, new GestureListener());
Steve McKay669ebe72015-10-19 12:04:21 -0700246
Steve McKay41585d52016-01-21 15:10:39 -0800247 mRecView.addOnItemTouchListener(mGestureDetector);
Steve McKay669ebe72015-10-19 12:04:21 -0700248
Ben Kwac64cb252015-08-27 16:04:46 -0700249 // TODO: instead of inserting the view into the constructor, extract listener-creation code
250 // 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 -0700251 // into the selection manager.
252 mSelectionManager = new MultiSelectManager(
Steve McKaydbec47a2015-08-12 14:48:34 -0700253 mRecView,
Steve McKay44408262016-01-05 15:27:17 -0800254 mAdapter,
Steve McKaydbec47a2015-08-12 14:48:34 -0700255 state.allowMultiple
256 ? MultiSelectManager.MODE_MULTIPLE
Steve McKaye852d932016-02-08 19:09:42 -0800257 : MultiSelectManager.MODE_SINGLE,
Aga Wronska893390b2016-02-17 13:50:42 -0800258 null);
Steve McKaye852d932016-02-08 19:09:42 -0800259
Aga Wronska893390b2016-02-17 13:50:42 -0800260 mSelectionManager.addCallback(new SelectionModeListener());
Ben Kwa18fce3c2015-09-01 11:03:01 -0700261
Steve McKay35645432016-01-20 15:09:35 -0800262 mModel = new Model();
Ben Kwa743c7c22015-12-01 19:56:57 -0800263 mModel.addUpdateListener(mAdapter);
Ben Kwa91caed82015-09-21 10:49:52 -0700264 mModel.addUpdateListener(mModelUpdateListener);
Steve McKay4b3a13c2015-06-11 10:10:49 -0700265
Ben Kwaa4acc902016-02-10 15:48:25 -0800266 // Make sure this is done after the RecyclerView is set up.
267 mFocusManager = new FocusManager(context, mRecView, mModel);
268
Steve McKayaa15dae2016-02-09 16:17:24 -0800269 mTuner = FragmentTuner.pick(getContext(), state);
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700270 mClipper = new DocumentClipper(context);
271
Jeff Sharkeyd01571e2013-10-01 17:57:41 -0700272 final ActivityManager am = (ActivityManager) context.getSystemService(
273 Context.ACTIVITY_SERVICE);
Ben Kwad8391492015-12-17 10:37:00 -0800274 boolean svelte = am.isLowRamDevice() && (mType == TYPE_RECENT_OPEN);
275 mIconHelper.setThumbnailsEnabled(!svelte);
Jeff Sharkeyd01571e2013-10-01 17:57:41 -0700276
Jeff Sharkeyd182bb62013-09-07 14:45:03 -0700277 // Kick off loader at least once
Aga Wronska893390b2016-02-17 13:50:42 -0800278 getLoaderManager().restartLoader(LOADER_ID, null, this);
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700279 }
280
Jeff Sharkey28c05ee2013-09-06 13:22:09 -0700281 @Override
Steve McKaye852d932016-02-08 19:09:42 -0800282 public void onSaveInstanceState(Bundle outState) {
Aga Wronska893390b2016-02-17 13:50:42 -0800283 super.onSaveInstanceState(outState);
284
Aga Wronskad5597432016-02-26 11:36:07 -0800285 mSelectionManager.getSelection(mSelection);
286
Aga Wronska893390b2016-02-17 13:50:42 -0800287 outState.putInt(Shared.EXTRA_TYPE, mType);
288 outState.putParcelable(Shared.EXTRA_ROOT, mRoot);
289 outState.putParcelable(Shared.EXTRA_DOC, mDocument);
290 outState.putString(Shared.EXTRA_QUERY, mQuery);
Aga Wronskad5597432016-02-26 11:36:07 -0800291 outState.putParcelable(Shared.EXTRA_SELECTION, mSelection);
Aga Wronska893390b2016-02-17 13:50:42 -0800292 outState.putBoolean(Shared.EXTRA_SEARCH_MODE, mSearchMode);
Aga Wronskad5597432016-02-26 11:36:07 -0800293
Steve McKaye852d932016-02-08 19:09:42 -0800294 }
295
296 @Override
Steve McKaya6bbeab2016-02-17 15:02:01 -0800297 public void onActivityResult(@RequestCode int requestCode, int resultCode, Intent data) {
298 switch(requestCode) {
299 case REQUEST_COPY_DESTINATION:
300 handleCopyResult(resultCode, data);
301 break;
302 default:
303 throw new UnsupportedOperationException("Unknown request code: " + requestCode);
Ben Kwaef3f2622015-04-07 15:43:39 -0700304 }
Steve McKaya6bbeab2016-02-17 15:02:01 -0800305 }
306
307 private void handleCopyResult(int resultCode, Intent data) {
Ben Kwaef3f2622015-04-07 15:43:39 -0700308 if (resultCode == Activity.RESULT_CANCELED || data == null) {
309 // User pressed the back button or otherwise cancelled the destination pick. Don't
310 // proceed with the copy.
311 return;
312 }
313
Steve McKaya6bbeab2016-02-17 15:02:01 -0800314 @OpType int operationType = data.getIntExtra(
Steve McKay14e827a2016-01-06 18:32:13 -0800315 FileOperationService.EXTRA_OPERATION,
316 FileOperationService.OPERATION_COPY);
317
318 FileOperations.start(
319 getActivity(),
320 getDisplayState().selectedDocumentsForCopy,
Tomasz Mikolajewskib8436af2016-01-25 16:20:15 +0900321 getDisplayState().stack.peek(),
Steve McKay323ee3e2015-09-25 16:02:56 -0700322 (DocumentStack) data.getParcelableExtra(Shared.EXTRA_STACK),
Steve McKay14e827a2016-01-06 18:32:13 -0800323 operationType);
Ben Kwaef3f2622015-04-07 15:43:39 -0700324 }
325
Steve McKay5353a1e2015-07-30 12:27:44 -0700326 protected boolean onDoubleTap(MotionEvent e) {
327 if (Events.isMouseEvent(e)) {
Ben Kwa003a9992015-11-30 23:00:02 -0800328 String id = getModelId(e);
329 if (id != null) {
330 return handleViewItem(id);
Steve McKay5353a1e2015-07-30 12:27:44 -0700331 }
332 }
333 return false;
334 }
335
Ben Kwa003a9992015-11-30 23:00:02 -0800336 private boolean handleViewItem(String id) {
337 final Cursor cursor = mModel.getItem(id);
Steve McKaya1f76802016-02-25 13:34:03 -0800338
339 assert(cursor != null);
340
Steve McKay5353a1e2015-07-30 12:27:44 -0700341 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
342 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
Steve McKayf69502c2015-11-09 17:38:35 +0900343 if (mTuner.isDocumentEnabled(docMimeType, docFlags)) {
Steve McKay5353a1e2015-07-30 12:27:44 -0700344 final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
Ben Kwac64cb252015-08-27 16:04:46 -0700345 ((BaseActivity) getActivity()).onDocumentPicked(doc, mModel);
Ben Kwa8250db42015-10-07 14:15:12 -0700346 mSelectionManager.clearSelection();
Steve McKay5353a1e2015-07-30 12:27:44 -0700347 return true;
348 }
Steve McKay4b3a13c2015-06-11 10:10:49 -0700349 return false;
350 }
351
Ben Kwaef3f2622015-04-07 15:43:39 -0700352 @Override
Jeff Sharkeye20a3ac2013-09-18 16:26:49 -0700353 public void onStop() {
354 super.onStop();
355
356 // Remember last scroll location
357 final SparseArray<Parcelable> container = new SparseArray<Parcelable>();
358 getView().saveHierarchyState(container);
Steve McKayf69502c2015-11-09 17:38:35 +0900359 final State state = getDisplayState();
Jeff Sharkeye20a3ac2013-09-18 16:26:49 -0700360 state.dirState.put(mStateKey, container);
361 }
362
Jeff Sharkey669f8e72014-08-08 15:10:03 -0700363 public void onDisplayStateChanged() {
364 updateDisplayState();
365 }
366
Steve McKay3eb2d072016-01-25 19:00:22 -0800367 public void onSortOrderChanged() {
368 // Sort order is implemented as a sorting wrapper around directory
369 // results. So when sort order changes, we force a reload of the directory.
Aga Wronska893390b2016-02-17 13:50:42 -0800370 getLoaderManager().restartLoader(LOADER_ID, null, this);
Jeff Sharkeyd182bb62013-09-07 14:45:03 -0700371 }
372
Steve McKay3eb2d072016-01-25 19:00:22 -0800373 public void onViewModeChanged() {
374 // Mode change is just visual change; no need to kick loader.
Jeff Sharkeyd182bb62013-09-07 14:45:03 -0700375 updateDisplayState();
376 }
377
378 private void updateDisplayState() {
Steve McKay3eb2d072016-01-25 19:00:22 -0800379 State state = getDisplayState();
Steve McKay4b3a13c2015-06-11 10:10:49 -0700380 updateLayout(state.derivedMode);
Steve McKay4b3a13c2015-06-11 10:10:49 -0700381 mRecView.setAdapter(mAdapter);
382 }
383
384 /**
Ben Kwa1e2fa5e2016-02-02 23:00:02 -0800385 * Updates the layout after the view mode switches.
386 * @param mode The new view mode.
Steve McKay4b3a13c2015-06-11 10:10:49 -0700387 */
Ben Kwa1e2fa5e2016-02-02 23:00:02 -0800388 private void updateLayout(@ViewMode int mode) {
389 mColumnCount = calculateColumnCount(mode);
390 if (mLayout != null) {
391 mLayout.setSpanCount(mColumnCount);
Jeff Sharkey54e55b72013-06-30 20:02:59 -0700392 }
Jeff Sharkey9d0843d2013-05-07 12:41:33 -0700393
Steve McKay76be6202016-01-12 11:14:33 -0800394 int pad = getDirectoryPadding(mode);
395 mRecView.setPadding(pad, pad, pad, pad);
Ben Kwa1e2fa5e2016-02-02 23:00:02 -0800396 mRecView.requestLayout();
Steve McKay76be6202016-01-12 11:14:33 -0800397 mSelectionManager.handleLayoutChanged(); // RecyclerView doesn't do this for us
Steve McKay3eb2d072016-01-25 19:00:22 -0800398 mIconHelper.setViewMode(mode);
Jeff Sharkey54e55b72013-06-30 20:02:59 -0700399 }
400
Ben Kwa1e2fa5e2016-02-02 23:00:02 -0800401 private int calculateColumnCount(@ViewMode int mode) {
402 if (mode == MODE_LIST) {
403 // List mode is a "grid" with 1 column.
404 return 1;
405 }
406
Steve McKay58efce32015-08-20 16:19:38 +0000407 int cellWidth = getResources().getDimensionPixelSize(R.dimen.grid_width);
408 int cellMargin = 2 * getResources().getDimensionPixelSize(R.dimen.grid_item_margin);
Steve McKay0ba3a142015-07-23 16:33:41 -0700409 int viewPadding = mRecView.getPaddingLeft() + mRecView.getPaddingRight();
Steve McKay58efce32015-08-20 16:19:38 +0000410
Ben Kwa1a63c612016-03-16 13:01:20 -0700411 // RecyclerView sometimes gets a width of 0 (see b/27150284). Clamp so that we always lay
412 // out the grid with at least 2 columns.
413 int columnCount = Math.max(2,
Steve McKay58efce32015-08-20 16:19:38 +0000414 (mRecView.getWidth() - viewPadding) / (cellWidth + cellMargin));
415
Steve McKay0ba3a142015-07-23 16:33:41 -0700416 return columnCount;
417 }
418
Ben Kwa1e2fa5e2016-02-02 23:00:02 -0800419 private int getDirectoryPadding(@ViewMode int mode) {
Steve McKay76be6202016-01-12 11:14:33 -0800420 switch (mode) {
421 case MODE_GRID:
Ben Kwa1e2fa5e2016-02-02 23:00:02 -0800422 return getResources().getDimensionPixelSize(R.dimen.grid_container_padding);
Steve McKay76be6202016-01-12 11:14:33 -0800423 case MODE_LIST:
Ben Kwa1e2fa5e2016-02-02 23:00:02 -0800424 return getResources().getDimensionPixelSize(R.dimen.list_container_padding);
Steve McKay76be6202016-01-12 11:14:33 -0800425 default:
426 throw new IllegalArgumentException("Unsupported layout mode: " + mode);
427 }
428 }
429
Steve McKayef16f5f2015-12-22 18:15:31 -0800430 @Override
431 public int getColumnCount() {
432 return mColumnCount;
433 }
434
Steve McKay4b3a13c2015-06-11 10:10:49 -0700435 /**
436 * Manages the integration between our ActionMode and MultiSelectManager, initiating
437 * ActionMode when there is a selection, canceling it when there is no selection,
438 * and clearing selection when action mode is explicitly exited by the user.
439 */
440 private final class SelectionModeListener
441 implements MultiSelectManager.Callback, ActionMode.Callback {
442
443 private Selection mSelected = new Selection();
Steve McKay168e4642016-03-14 13:02:56 -0700444 private int mNoCopyCount = 0;
Steve McKay503648d2015-07-22 12:13:46 -0700445 private int mNoDeleteCount = 0;
Aga Wronska3b327ef2016-01-20 16:32:33 -0800446 private int mNoRenameCount = -1;
Steve McKay503648d2015-07-22 12:13:46 -0700447 private Menu mMenu;
Steve McKay4b3a13c2015-06-11 10:10:49 -0700448
Jeff Sharkey54e55b72013-06-30 20:02:59 -0700449 @Override
Ben Kwa743c7c22015-12-01 19:56:57 -0800450 public boolean onBeforeItemStateChange(String modelId, boolean selected) {
Steve McKay4b3a13c2015-06-11 10:10:49 -0700451 if (selected) {
Ben Kwa743c7c22015-12-01 19:56:57 -0800452 final Cursor cursor = mModel.getItem(modelId);
Steve McKaya1f76802016-02-25 13:34:03 -0800453
454 assert(cursor != null);
455
Jeff Sharkey2a030b02013-09-26 10:54:16 -0700456 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
457 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
Tomasz Mikolajewskia8057a92015-11-16 11:41:28 +0900458 return mTuner.canSelectType(docMimeType, docFlags);
Steve McKay4b3a13c2015-06-11 10:10:49 -0700459 }
460 return true;
461 }
462
463 @Override
Ben Kwa743c7c22015-12-01 19:56:57 -0800464 public void onItemStateChanged(String modelId, boolean selected) {
465 final Cursor cursor = mModel.getItem(modelId);
Steve McKay30551a22016-02-16 13:08:10 -0800466 if (cursor == null) {
467 Log.e(TAG, "Model returned null cursor for document: " + modelId
468 + ". Ignoring state changed event.");
469 return;
470 }
Steve McKay503648d2015-07-22 12:13:46 -0700471
Ben Kwa67924892016-01-27 09:58:36 -0800472 // TODO: Should this be happening in onSelectionChanged? Technically this callback is
473 // triggered on "silent" selection updates (i.e. we might be reacting to unfinalized
474 // selection changes here)
Steve McKay503648d2015-07-22 12:13:46 -0700475 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
Steve McKay168e4642016-03-14 13:02:56 -0700476 if ((docFlags & Document.FLAG_PARTIAL) != 0) {
477 mNoCopyCount += selected ? 1 : -1;
478 }
Steve McKay27d20a32016-02-22 18:38:09 -0800479 if ((docFlags & Document.FLAG_SUPPORTS_DELETE) == 0
480 && (docFlags & Document.FLAG_SUPPORTS_DELETE) == 0) {
Steve McKay503648d2015-07-22 12:13:46 -0700481 mNoDeleteCount += selected ? 1 : -1;
482 }
Aga Wronska3b327ef2016-01-20 16:32:33 -0800483 if ((docFlags & Document.FLAG_SUPPORTS_RENAME) != 0) {
484 mNoRenameCount += selected ? 1 : -1;
485 }
Steve McKaydbec47a2015-08-12 14:48:34 -0700486 }
Steve McKay503648d2015-07-22 12:13:46 -0700487
Steve McKaydbec47a2015-08-12 14:48:34 -0700488 @Override
489 public void onSelectionChanged() {
Ben Kwa8250db42015-10-07 14:15:12 -0700490 mSelectionManager.getSelection(mSelected);
Ben Kwa7cc80012015-09-11 15:40:18 -0700491 TypedValue color = new TypedValue();
Steve McKay503648d2015-07-22 12:13:46 -0700492 if (mSelected.size() > 0) {
Steve McKay4b3a13c2015-06-11 10:10:49 -0700493 if (DEBUG) Log.d(TAG, "Maybe starting action mode.");
494 if (mActionMode == null) {
495 if (DEBUG) Log.d(TAG, "Yeah. Starting action mode.");
496 mActionMode = getActivity().startActionMode(this);
Jeff Sharkey3f4c2052013-09-09 16:51:06 -0700497 }
Ben Kwa8250db42015-10-07 14:15:12 -0700498 getActivity().getTheme().resolveAttribute(R.attr.colorActionMode, color, true);
Steve McKay503648d2015-07-22 12:13:46 -0700499 updateActionMenu();
500 } else {
501 if (DEBUG) Log.d(TAG, "Finishing action mode.");
502 if (mActionMode != null) {
503 mActionMode.finish();
504 }
Ben Kwa0f7078f02015-09-08 07:31:19 -0700505 getActivity().getTheme().resolveAttribute(
506 android.R.attr.colorPrimaryDark, color, true);
Jeff Sharkeya5599ef2013-08-15 16:17:41 -0700507 }
Ben Kwa7cc80012015-09-11 15:40:18 -0700508 getActivity().getWindow().setStatusBarColor(color.data);
Jeff Sharkey54e55b72013-06-30 20:02:59 -0700509
Steve McKay4b3a13c2015-06-11 10:10:49 -0700510 if (mActionMode != null) {
Tomasz Mikolajewski52137ac2016-03-22 16:43:59 +0900511 final String title = Shared.getQuantityString(getActivity(),
512 R.plurals.elements_selected, mSelected.size());
513 mActionMode.setTitle(title);
514 mRecView.announceForAccessibility(title);
Steve McKay4b3a13c2015-06-11 10:10:49 -0700515 }
516 }
517
518 // Called when the user exits the action mode
519 @Override
520 public void onDestroyActionMode(ActionMode mode) {
521 if (DEBUG) Log.d(TAG, "Handling action mode destroyed.");
522 mActionMode = null;
523 // clear selection
Ben Kwa8250db42015-10-07 14:15:12 -0700524 mSelectionManager.clearSelection();
Steve McKay503648d2015-07-22 12:13:46 -0700525 mSelected.clear();
526 mNoDeleteCount = 0;
Aga Wronska3b327ef2016-01-20 16:32:33 -0800527 mNoRenameCount = -1;
Tomasz Mikolajewski1e479ab2016-03-22 13:30:59 +0900528
529 // Re-enable TalkBack for the toolbars, as they are no longer covered by action mode.
530 final Toolbar toolbar = (Toolbar) getActivity().findViewById(R.id.toolbar);
Tomasz Mikolajewski1e479ab2016-03-22 13:30:59 +0900531 toolbar.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
Aga Wronskad8f74442016-03-23 15:11:25 -0700532
533 // This toolbar is not present in the fixed_layout
534 final Toolbar rootsToolbar = (Toolbar) getActivity().findViewById(R.id.roots_toolbar);
535 if (rootsToolbar != null) {
536 rootsToolbar.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
537 }
Steve McKay4b3a13c2015-06-11 10:10:49 -0700538 }
539
Jeff Sharkeybe8b12e2013-07-01 16:56:54 -0700540 @Override
541 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
Aga Wronska7d5a2472016-03-08 17:56:28 -0800542 mRecView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
543
Ben Kwa8250db42015-10-07 14:15:12 -0700544 int size = mSelectionManager.getSelection().size();
Jeff Sharkeybe8b12e2013-07-01 16:56:54 -0700545 mode.getMenuInflater().inflate(R.menu.mode_directory, menu);
Ben Kwa8250db42015-10-07 14:15:12 -0700546 mode.setTitle(TextUtils.formatSelectedCount(size));
Tomasz Mikolajewski1e479ab2016-03-22 13:30:59 +0900547
548 if (size > 0) {
549 // Hide the toolbars if action mode is enabled, so TalkBack doesn't navigate to
550 // these controls when using linear navigation.
551 final Toolbar toolbar = (Toolbar) getActivity().findViewById(R.id.toolbar);
Tomasz Mikolajewski1e479ab2016-03-22 13:30:59 +0900552 toolbar.setImportantForAccessibility(
553 View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
Tomasz Mikolajewski1e479ab2016-03-22 13:30:59 +0900554
Aga Wronskad8f74442016-03-23 15:11:25 -0700555 // This toolbar is not present in the fixed_layout
556 final Toolbar rootsToolbar = (Toolbar) getActivity().findViewById(
557 R.id.roots_toolbar);
558 if (rootsToolbar != null) {
559 rootsToolbar.setImportantForAccessibility(
560 View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
561 }
Tomasz Mikolajewski1e479ab2016-03-22 13:30:59 +0900562 return true;
563 }
564
565 return false;
Jeff Sharkeybe8b12e2013-07-01 16:56:54 -0700566 }
567
568 @Override
569 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
Steve McKay503648d2015-07-22 12:13:46 -0700570 mMenu = menu;
571 updateActionMenu();
Jeff Sharkeybe8b12e2013-07-01 16:56:54 -0700572 return true;
573 }
574
Steve McKay168e4642016-03-14 13:02:56 -0700575 boolean canCopySelection() {
576 return mNoCopyCount == 0;
Aga Wronska3b327ef2016-01-20 16:32:33 -0800577 }
578
579 boolean canDeleteSelection() {
580 return mNoDeleteCount == 0;
581 }
582
Steve McKay168e4642016-03-14 13:02:56 -0700583 boolean canRenameSelection() {
584 return mNoRenameCount == 0 && mSelectionManager.getSelection().size() == 1;
585 }
586
Steve McKay503648d2015-07-22 12:13:46 -0700587 private void updateActionMenu() {
Steve McKaya1f76802016-02-25 13:34:03 -0800588 assert(mMenu != null);
Aga Wronska3b327ef2016-01-20 16:32:33 -0800589
Steve McKay503648d2015-07-22 12:13:46 -0700590 // Delegate update logic to our owning action, since specialized logic is desired.
Steve McKay168e4642016-03-14 13:02:56 -0700591 mTuner.updateActionMenu(
592 mMenu, mType, canCopySelection(), canDeleteSelection(), canRenameSelection());
Steve McKay5bbae102015-10-01 11:39:24 -0700593 Menus.disableHiddenItems(mMenu);
Steve McKay503648d2015-07-22 12:13:46 -0700594 }
595
Jeff Sharkeybe8b12e2013-07-01 16:56:54 -0700596 @Override
Steve McKay4b3a13c2015-06-11 10:10:49 -0700597 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
Aga Wronskacf966ae2016-03-30 13:55:19 -0700598 Metrics.logMenuAction(getContext(), item.getItemId());
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700599
Ben Kwa8250db42015-10-07 14:15:12 -0700600 Selection selection = mSelectionManager.getSelection(new Selection());
Jeff Sharkeybe8b12e2013-07-01 16:56:54 -0700601
Steve McKayc2b4b3d2015-12-01 17:02:42 -0800602 switch (item.getItemId()) {
603 case R.id.menu_open:
604 openDocuments(selection);
605 mode.finish();
606 return true;
Jeff Sharkey4eb407a2013-08-18 17:38:20 -0700607
Steve McKayc2b4b3d2015-12-01 17:02:42 -0800608 case R.id.menu_share:
609 shareDocuments(selection);
Steve McKayc2b4b3d2015-12-01 17:02:42 -0800610 return true;
Jeff Sharkey4eb407a2013-08-18 17:38:20 -0700611
Steve McKayc2b4b3d2015-12-01 17:02:42 -0800612 case R.id.menu_delete:
Ben Kwaeb61aac2016-03-14 15:12:12 -0700613 // deleteDocuments will end action mode if the documents are deleted.
Steve McKayda4b8aa2016-03-08 14:49:38 -0800614 // It won't end action mode if user cancels the delete.
Ben Kwaeb61aac2016-03-14 15:12:12 -0700615 deleteDocuments(selection);
Steve McKayc2b4b3d2015-12-01 17:02:42 -0800616 return true;
Jeff Sharkey4eb407a2013-08-18 17:38:20 -0700617
Steve McKayc2b4b3d2015-12-01 17:02:42 -0800618 case R.id.menu_copy_to:
Steve McKay14e827a2016-01-06 18:32:13 -0800619 transferDocuments(selection, FileOperationService.OPERATION_COPY);
Steve McKayc2b4b3d2015-12-01 17:02:42 -0800620 return true;
Ben Kwa0b4a3c42015-05-05 11:50:11 -0700621
Steve McKayc2b4b3d2015-12-01 17:02:42 -0800622 case R.id.menu_move_to:
623 // Exit selection mode first, so we avoid deselecting deleted documents.
624 mode.finish();
Steve McKay14e827a2016-01-06 18:32:13 -0800625 transferDocuments(selection, FileOperationService.OPERATION_MOVE);
Steve McKayc2b4b3d2015-12-01 17:02:42 -0800626 return true;
Ben Kwad99109f2015-03-31 10:11:43 -0700627
Steve McKayc2b4b3d2015-12-01 17:02:42 -0800628 case R.id.menu_copy_to_clipboard:
Steve McKaycbee5442016-01-28 15:30:10 -0800629 copySelectedToClipboard();
Steve McKayc2b4b3d2015-12-01 17:02:42 -0800630 return true;
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700631
Steve McKayc2b4b3d2015-12-01 17:02:42 -0800632 case R.id.menu_select_all:
633 selectAllFiles();
634 return true;
Ben Kwa3bcc94882015-03-31 08:15:21 -0700635
Aga Wronska3b327ef2016-01-20 16:32:33 -0800636 case R.id.menu_rename:
Steve McKayda4b8aa2016-03-08 14:49:38 -0800637 // Exit selection mode first, so we avoid deselecting deleted
638 // (renamed) documents.
Aga Wronska3b327ef2016-01-20 16:32:33 -0800639 mode.finish();
Steve McKayda4b8aa2016-03-08 14:49:38 -0800640 renameDocuments(selection);
Aga Wronska3b327ef2016-01-20 16:32:33 -0800641 return true;
642
Steve McKayc2b4b3d2015-12-01 17:02:42 -0800643 default:
644 if (DEBUG) Log.d(TAG, "Unhandled menu item selected: " + item);
645 return false;
Jeff Sharkeybe8b12e2013-07-01 16:56:54 -0700646 }
647 }
Steve McKay4b3a13c2015-06-11 10:10:49 -0700648 }
Jeff Sharkeybe8b12e2013-07-01 16:56:54 -0700649
Steve McKaycbee5442016-01-28 15:30:10 -0800650 public final boolean onBackPressed() {
651 if (mSelectionManager.hasSelection()) {
652 if (DEBUG) Log.d(TAG, "Clearing selection on back pressed.");
653 mSelectionManager.clearSelection();
654 return true;
655 }
656 return false;
657 }
658
Ben Kwad8391492015-12-17 10:37:00 -0800659 private void cancelThumbnailTask(View view) {
Steve McKay4b3a13c2015-06-11 10:10:49 -0700660 final ImageView iconThumb = (ImageView) view.findViewById(R.id.icon_thumb);
661 if (iconThumb != null) {
Ben Kwad8391492015-12-17 10:37:00 -0800662 mIconHelper.stopLoading(iconThumb);
Jeff Sharkey4ec97392013-09-10 12:04:26 -0700663 }
Steve McKay4b3a13c2015-06-11 10:10:49 -0700664 }
Jeff Sharkey4ec97392013-09-10 12:04:26 -0700665
Steve McKay4b3a13c2015-06-11 10:10:49 -0700666 private void openDocuments(final Selection selected) {
Steve McKayab7865c2015-05-27 16:11:42 -0700667 new GetDocumentsTask() {
668 @Override
669 void onDocumentsReady(List<DocumentInfo> docs) {
Steve McKay12055472015-08-20 16:48:49 -0700670 // TODO: Implement support in Files activity for opening multiple docs.
Steve McKayab7865c2015-05-27 16:11:42 -0700671 BaseActivity.get(DirectoryFragment.this).onDocumentsPicked(docs);
Ben Kwa726cf702015-04-08 15:03:35 -0700672 }
Steve McKayab7865c2015-05-27 16:11:42 -0700673 }.execute(selected);
Jeff Sharkey4eb407a2013-08-18 17:38:20 -0700674 }
675
Steve McKay4b3a13c2015-06-11 10:10:49 -0700676 private void shareDocuments(final Selection selected) {
Steve McKayab7865c2015-05-27 16:11:42 -0700677 new GetDocumentsTask() {
678 @Override
679 void onDocumentsReady(List<DocumentInfo> docs) {
680 Intent intent;
681
682 // Filter out directories - those can't be shared.
Steve McKay58efce32015-08-20 16:19:38 +0000683 List<DocumentInfo> docsForSend = new ArrayList<>();
Steve McKayab7865c2015-05-27 16:11:42 -0700684 for (DocumentInfo doc: docs) {
685 if (!Document.MIME_TYPE_DIR.equals(doc.mimeType)) {
686 docsForSend.add(doc);
687 }
688 }
689
690 if (docsForSend.size() == 1) {
691 final DocumentInfo doc = docsForSend.get(0);
692
693 intent = new Intent(Intent.ACTION_SEND);
694 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
695 intent.addCategory(Intent.CATEGORY_DEFAULT);
696 intent.setType(doc.mimeType);
697 intent.putExtra(Intent.EXTRA_STREAM, doc.derivedUri);
698
699 } else if (docsForSend.size() > 1) {
700 intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
701 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
702 intent.addCategory(Intent.CATEGORY_DEFAULT);
703
Steve McKay58efce32015-08-20 16:19:38 +0000704 final ArrayList<String> mimeTypes = new ArrayList<>();
705 final ArrayList<Uri> uris = new ArrayList<>();
Steve McKayab7865c2015-05-27 16:11:42 -0700706 for (DocumentInfo doc : docsForSend) {
707 mimeTypes.add(doc.mimeType);
708 uris.add(doc.derivedUri);
709 }
710
711 intent.setType(findCommonMimeType(mimeTypes));
712 intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
713
714 } else {
715 return;
716 }
717
718 intent = Intent.createChooser(intent, getActivity().getText(R.string.share_via));
719 startActivity(intent);
720 }
721 }.execute(selected);
722 }
723
Aga Wronska824ac172016-03-17 13:45:32 -0700724 private String generateDeleteMessage(final List<DocumentInfo> docs) {
725 String message;
726 int dirsCount = 0;
727
728 for (DocumentInfo doc : docs) {
729 if (doc.isDirectory()) {
730 ++dirsCount;
731 }
732 }
733
734 if (docs.size() == 1) {
735 // Deleteing 1 file xor 1 folder in cwd
736 message = dirsCount == 0
737 ? getActivity().getString(R.string.delete_filename_confirmation_message,
738 docs.get(0).displayName)
739 : getActivity().getString(R.string.delete_foldername_confirmation_message,
740 docs.get(0).displayName);
741 } else if (dirsCount == 0) {
742 // Deleting only files in cwd
743 message = Shared.getQuantityString(getActivity(),
744 R.plurals.delete_files_confirmation_message, docs.size());
745 } else if (dirsCount == docs.size()) {
746 // Deleting only folders in cwd
747 message = Shared.getQuantityString(getActivity(),
748 R.plurals.delete_folders_confirmation_message, docs.size());
749 } else {
750 // Deleting mixed items (files and folders) in cwd
751 message = Shared.getQuantityString(getActivity(),
752 R.plurals.delete_items_confirmation_message, docs.size());
753 }
754 return message;
755 }
756
Ben Kwaeb61aac2016-03-14 15:12:12 -0700757 private void deleteDocuments(final Selection selected) {
Steve McKaya1f76802016-02-25 13:34:03 -0800758 assert(!selected.isEmpty());
Jeff Sharkey4eb407a2013-08-18 17:38:20 -0700759
Tomasz Mikolajewskib8436af2016-01-25 16:20:15 +0900760 final DocumentInfo srcParent = getDisplayState().stack.peek();
761 new GetDocumentsTask() {
762 @Override
Steve McKay7a3b8112016-02-23 10:06:50 -0800763 void onDocumentsReady(final List<DocumentInfo> docs) {
Steve McKayc8d4e222016-03-01 08:28:02 -0800764
765 TextView message =
766 (TextView) mInflater.inflate(R.layout.dialog_delete_confirmation, null);
Aga Wronska824ac172016-03-17 13:45:32 -0700767 message.setText(generateDeleteMessage(docs));
Steve McKayc8d4e222016-03-01 08:28:02 -0800768
769 // This "insta-hides" files that are being deleted, because
770 // the delete operation may be not execute immediately (it
771 // may be queued up on the FileOperationService.)
772 // To hide the files locally, we call the hide method on the adapter
773 // ...which a live object...cannot be parceled.
774 // For that reason, for now, we implement this dialog NOT
775 // as a fragment (which can survive rotation and have its own state),
776 // but as a simple runtime dialog. So rotating a device with an
777 // active delete dialog...results in that dialog disappearing.
778 // We can do better, but don't have cycles for it now.
Steve McKay7a3b8112016-02-23 10:06:50 -0800779 new AlertDialog.Builder(getActivity())
Steve McKayc8d4e222016-03-01 08:28:02 -0800780 .setView(message)
Steve McKay7a3b8112016-02-23 10:06:50 -0800781 .setPositiveButton(
782 android.R.string.yes,
783 new DialogInterface.OnClickListener() {
784 public void onClick(DialogInterface dialog, int id) {
Steve McKayda4b8aa2016-03-08 14:49:38 -0800785 // Finish selection mode first which clears selection so we
786 // don't end up trying to deselect deleted documents.
787 // This is done here, rather in the onActionItemClicked
788 // so we can avoid de-selecting items in the case where
789 // the user cancels the delete.
Ben Kwaeb61aac2016-03-14 15:12:12 -0700790 if (mActionMode != null) {
791 mActionMode.finish();
792 }
Steve McKayda4b8aa2016-03-08 14:49:38 -0800793 // Hide the files in the UI...since the operation
794 // might be queued up on FileOperationService.
795 // We're walking a line here.
Steve McKay7a3b8112016-02-23 10:06:50 -0800796 mAdapter.hide(selected.getAll());
797 FileOperations.delete(
798 getActivity(), docs, srcParent, getDisplayState().stack);
Ben Kwa304895a2015-08-27 16:06:33 -0700799 }
800 })
Steve McKay7a3b8112016-02-23 10:06:50 -0800801 .setNegativeButton(android.R.string.no, null)
802 .show();
803 }
804 }.execute(selected);
Jeff Sharkey4eb407a2013-08-18 17:38:20 -0700805 }
806
Ben Kwa1e2fa5e2016-02-02 23:00:02 -0800807 private void transferDocuments(final Selection selected, final @OpType int mode) {
Ben Kwaef3f2622015-04-07 15:43:39 -0700808 // Pop up a dialog to pick a destination. This is inadequate but works for now.
809 // TODO: Implement a picker that is to spec.
Daichi Hironobbe22922015-04-10 15:50:38 +0900810 final Intent intent = new Intent(
Ben Kwa84cebbe2015-09-25 14:48:29 -0700811 Shared.ACTION_PICK_COPY_DESTINATION,
Daichi Hironobbe22922015-04-10 15:50:38 +0900812 Uri.EMPTY,
813 getActivity(),
814 DocumentsActivity.class);
Steve McKayab7865c2015-05-27 16:11:42 -0700815
Steve McKaya6bbeab2016-02-17 15:02:01 -0800816 // Set an appropriate title on the drawer when it is shown in the picker.
817 // Coupled with the fact that we auto-open the drawer for copy/move operations
818 // it should basically be the thing people see first.
819 int drawerTitleId = mode == FileOperationService.OPERATION_MOVE
820 ? R.string.menu_move : R.string.menu_copy;
821 intent.putExtra(DocumentsContract.EXTRA_PROMPT, getResources().getString(drawerTitleId));
822
Steve McKayab7865c2015-05-27 16:11:42 -0700823 new GetDocumentsTask() {
824 @Override
825 void onDocumentsReady(List<DocumentInfo> docs) {
Steve McKaya6bbeab2016-02-17 15:02:01 -0800826 // TODO: Can this move to Fragment bundle state?
Steve McKayf69502c2015-11-09 17:38:35 +0900827 getDisplayState().selectedDocumentsForCopy = docs;
Steve McKayab7865c2015-05-27 16:11:42 -0700828
Steve McKaya6bbeab2016-02-17 15:02:01 -0800829 // Determine if there is a directory in the set of documents
830 // to be copied? Why? Directory creation isn't supported by some roots
831 // (like Downloads). This informs DocumentsActivity (the "picker")
832 // to restrict available roots to just those with support.
833 intent.putExtra(Shared.EXTRA_DIRECTORY_COPY, hasDirectory(docs));
Steve McKay14e827a2016-01-06 18:32:13 -0800834 intent.putExtra(FileOperationService.EXTRA_OPERATION, mode);
Steve McKaya6bbeab2016-02-17 15:02:01 -0800835
836 // This just identifies the type of request...we'll check it
837 // when we reveive a response.
Steve McKayab7865c2015-05-27 16:11:42 -0700838 startActivityForResult(intent, REQUEST_COPY_DESTINATION);
Daichi Hirono9be34292015-04-14 17:12:54 +0900839 }
Steve McKaya6bbeab2016-02-17 15:02:01 -0800840
Steve McKayab7865c2015-05-27 16:11:42 -0700841 }.execute(selected);
Ben Kwad99109f2015-03-31 10:11:43 -0700842 }
843
Steve McKaya6bbeab2016-02-17 15:02:01 -0800844 private static boolean hasDirectory(List<DocumentInfo> docs) {
845 for (DocumentInfo info : docs) {
846 if (Document.MIME_TYPE_DIR.equals(info.mimeType)) {
847 return true;
848 }
849 }
850 return false;
851 }
852
Aga Wronska3b327ef2016-01-20 16:32:33 -0800853 private void renameDocuments(Selection selected) {
854 // Batch renaming not supported
855 // Rename option is only available in menu when 1 document selected
Steve McKaya1f76802016-02-25 13:34:03 -0800856 assert(selected.size() == 1);
Aga Wronska3b327ef2016-01-20 16:32:33 -0800857
858 new GetDocumentsTask() {
859 @Override
860 void onDocumentsReady(List<DocumentInfo> docs) {
861 RenameDocumentFragment.show(getFragmentManager(), docs.get(0));
862 }
863 }.execute(selected);
864 }
865
Steve McKayef16f5f2015-12-22 18:15:31 -0800866 @Override
867 public void initDocumentHolder(DocumentHolder holder) {
Ben Kwa0436a752016-01-15 10:43:24 -0800868 holder.addEventListener(mItemEventListener);
Ben Kwa2036dad2016-02-10 07:46:35 -0800869 holder.itemView.setOnFocusChangeListener(mFocusManager);
Steve McKayef16f5f2015-12-22 18:15:31 -0800870 }
871
872 @Override
873 public void onBindDocumentHolder(DocumentHolder holder, Cursor cursor) {
Ben Kwa990489d2016-02-25 14:10:40 -0800874 setupDragAndDropOnDocumentView(holder.itemView, cursor);
Steve McKayef16f5f2015-12-22 18:15:31 -0800875 }
876
877 @Override
878 public State getDisplayState() {
Steve McKayf69502c2015-11-09 17:38:35 +0900879 return ((BaseActivity) getActivity()).getDisplayState();
Jeff Sharkey54e55b72013-06-30 20:02:59 -0700880 }
881
Steve McKayef16f5f2015-12-22 18:15:31 -0800882 @Override
883 public Model getModel() {
884 return mModel;
885 }
886
887 @Override
888 public boolean isDocumentEnabled(String docMimeType, int docFlags) {
889 return mTuner.isDocumentEnabled(docMimeType, docFlags);
890 }
891
Steve McKay9ed88a42016-01-21 18:46:15 -0800892 private void showEmptyDirectory() {
Ben Kwa0e9aae42016-02-04 16:35:27 -0800893 showEmptyView(R.string.empty, R.drawable.cabinet);
Ben Kwa91bec532015-09-16 13:15:38 -0700894 }
895
Steve McKay9ed88a42016-01-21 18:46:15 -0800896 private void showNoResults(RootInfo root) {
897 CharSequence msg = getContext().getResources().getText(R.string.no_results);
Ben Kwa0e9aae42016-02-04 16:35:27 -0800898 showEmptyView(String.format(String.valueOf(msg), root.title), R.drawable.cabinet);
Ben Kwa91bec532015-09-16 13:15:38 -0700899 }
900
Steve McKay9ed88a42016-01-21 18:46:15 -0800901 private void showQueryError() {
Ben Kwa0e9aae42016-02-04 16:35:27 -0800902 showEmptyView(R.string.query_error, R.drawable.hourglass);
Steve McKay9ed88a42016-01-21 18:46:15 -0800903 }
904
Ben Kwa0e9aae42016-02-04 16:35:27 -0800905 private void showEmptyView(@StringRes int id, int drawable) {
906 showEmptyView(getContext().getResources().getText(id), drawable);
Steve McKay9ed88a42016-01-21 18:46:15 -0800907 }
908
Ben Kwa0e9aae42016-02-04 16:35:27 -0800909 private void showEmptyView(CharSequence msg, int drawable) {
Steve McKay9ed88a42016-01-21 18:46:15 -0800910 View content = mEmptyView.findViewById(R.id.content);
911 TextView msgView = (TextView) mEmptyView.findViewById(R.id.message);
Ben Kwa0e9aae42016-02-04 16:35:27 -0800912 ImageView imageView = (ImageView) mEmptyView.findViewById(R.id.artwork);
Steve McKay9ed88a42016-01-21 18:46:15 -0800913 msgView.setText(msg);
Ben Kwa0e9aae42016-02-04 16:35:27 -0800914 imageView.setImageResource(drawable);
Steve McKay9ed88a42016-01-21 18:46:15 -0800915
Steve McKay9ed88a42016-01-21 18:46:15 -0800916 mEmptyView.setVisibility(View.VISIBLE);
Aga Wronskad4b17532016-02-12 09:15:32 -0800917 mEmptyView.requestFocus();
Steve McKay9ed88a42016-01-21 18:46:15 -0800918 mRecView.setVisibility(View.GONE);
Steve McKay9ed88a42016-01-21 18:46:15 -0800919 }
920
921 private void showDirectory() {
Ben Kwa91bec532015-09-16 13:15:38 -0700922 mEmptyView.setVisibility(View.GONE);
923 mRecView.setVisibility(View.VISIBLE);
Aga Wronskad4b17532016-02-12 09:15:32 -0800924 mRecView.requestFocus();
Ben Kwa91bec532015-09-16 13:15:38 -0700925 }
926
Jeff Sharkeyb5133112013-09-01 18:41:04 -0700927 private String findCommonMimeType(List<String> mimeTypes) {
928 String[] commonType = mimeTypes.get(0).split("/");
929 if (commonType.length != 2) {
930 return "*/*";
931 }
932
933 for (int i = 1; i < mimeTypes.size(); i++) {
934 String[] type = mimeTypes.get(i).split("/");
935 if (type.length != 2) continue;
936
937 if (!commonType[1].equals(type[1])) {
938 commonType[1] = "*";
939 }
940
941 if (!commonType[0].equals(type[0])) {
942 commonType[0] = "*";
943 commonType[1] = "*";
944 break;
945 }
946 }
947
948 return commonType[0] + "/" + commonType[1];
949 }
Jeff Sharkey3f4c2052013-09-09 16:51:06 -0700950
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700951 private void copyFromClipboard() {
952 new AsyncTask<Void, Void, List<DocumentInfo>>() {
953
954 @Override
955 protected List<DocumentInfo> doInBackground(Void... params) {
956 return mClipper.getClippedDocuments();
957 }
958
959 @Override
960 protected void onPostExecute(List<DocumentInfo> docs) {
961 DocumentInfo destination =
962 ((BaseActivity) getActivity()).getCurrentDirectory();
963 copyDocuments(docs, destination);
964 }
965 }.execute();
Steve McKay3da8afc2015-05-05 14:50:00 -0700966 }
967
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700968 private void copyFromClipData(final ClipData clipData, final DocumentInfo destination) {
Steve McKaya1f76802016-02-25 13:34:03 -0800969 assert(clipData != null);
970
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700971 new AsyncTask<Void, Void, List<DocumentInfo>>() {
Vladislav Kaznacheev335dba52015-05-01 13:46:57 -0700972
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700973 @Override
974 protected List<DocumentInfo> doInBackground(Void... params) {
975 return mClipper.getDocumentsFromClipData(clipData);
976 }
977
978 @Override
979 protected void onPostExecute(List<DocumentInfo> docs) {
980 copyDocuments(docs, destination);
981 }
982 }.execute();
983 }
984
985 private void copyDocuments(final List<DocumentInfo> docs, final DocumentInfo destination) {
Steve McKay4a1ca862016-02-17 18:25:47 -0800986 BaseActivity activity = (BaseActivity) getActivity();
987 if (!canCopy(docs, activity.getCurrentRoot(), destination)) {
Steve McKay5bbae102015-10-01 11:39:24 -0700988 Snackbars.makeSnackbar(
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700989 getActivity(),
Ben Kwa94b486d2015-09-30 10:00:10 -0700990 R.string.clipboard_files_cannot_paste,
991 Snackbar.LENGTH_SHORT)
992 .show();
Steve McKay3da8afc2015-05-05 14:50:00 -0700993 return;
994 }
995
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700996 if (docs.isEmpty()) {
Vladislav Kaznacheev335dba52015-05-01 13:46:57 -0700997 return;
Vladislav Kaznacheevd69d3c42015-05-05 12:09:47 -0700998 }
Vladislav Kaznacheev335dba52015-05-01 13:46:57 -0700999
Steve McKayf69502c2015-11-09 17:38:35 +09001000 final DocumentStack curStack = getDisplayState().stack;
Vladislav Kaznacheev335dba52015-05-01 13:46:57 -07001001 DocumentStack tmpStack = new DocumentStack();
Steve McKaybdbd0ff2015-05-20 15:58:42 -07001002 if (destination != null) {
1003 tmpStack.push(destination);
Vladislav Kaznacheev335dba52015-05-01 13:46:57 -07001004 tmpStack.addAll(curStack);
1005 } else {
1006 tmpStack = curStack;
1007 }
1008
Steve McKay14e827a2016-01-06 18:32:13 -08001009 FileOperations.copy(getActivity(), docs, tmpStack);
Vladislav Kaznacheev335dba52015-05-01 13:46:57 -07001010 }
1011
Steve McKayf8621552015-11-03 15:23:16 -08001012 public void copySelectedToClipboard() {
Steve McKayd28c5c32015-12-07 16:31:42 -08001013 Selection selection = mSelectionManager.getSelection(new Selection());
1014 if (!selection.isEmpty()) {
1015 copySelectionToClipboard(selection);
Steve McKaycbee5442016-01-28 15:30:10 -08001016 mSelectionManager.clearSelection();
Steve McKayd28c5c32015-12-07 16:31:42 -08001017 }
Steve McKayab7865c2015-05-27 16:11:42 -07001018 }
Steve McKay3da8afc2015-05-05 14:50:00 -07001019
Steve McKayd28c5c32015-12-07 16:31:42 -08001020 void copySelectionToClipboard(Selection selection) {
Steve McKaya1f76802016-02-25 13:34:03 -08001021 assert(!selection.isEmpty());
Steve McKayab7865c2015-05-27 16:11:42 -07001022 new GetDocumentsTask() {
1023 @Override
1024 void onDocumentsReady(List<DocumentInfo> docs) {
1025 mClipper.clipDocuments(docs);
Steve McKaybdbd0ff2015-05-20 15:58:42 -07001026 Activity activity = getActivity();
Steve McKay5bbae102015-10-01 11:39:24 -07001027 Snackbars.makeSnackbar(activity,
Steve McKaybdbd0ff2015-05-20 15:58:42 -07001028 activity.getResources().getQuantityString(
1029 R.plurals.clipboard_files_clipped, docs.size(), docs.size()),
Ben Kwa862b96412015-12-07 13:25:27 -08001030 Snackbar.LENGTH_SHORT).show();
Steve McKayab7865c2015-05-27 16:11:42 -07001031 }
Steve McKayd28c5c32015-12-07 16:31:42 -08001032 }.execute(selection);
Steve McKay3da8afc2015-05-05 14:50:00 -07001033 }
1034
Steve McKayf8621552015-11-03 15:23:16 -08001035 public void pasteFromClipboard() {
Steve McKaybdbd0ff2015-05-20 15:58:42 -07001036 copyFromClipboard();
1037 getActivity().invalidateOptionsMenu();
Steve McKay3da8afc2015-05-05 14:50:00 -07001038 }
1039
Steve McKay3da8afc2015-05-05 14:50:00 -07001040 /**
1041 * Returns true if the list of files can be copied to destination. Note that this
1042 * is a policy check only. Currently the method does not attempt to verify
1043 * available space or any other environmental aspects possibly resulting in
1044 * failure to copy.
1045 *
1046 * @return true if the list of files can be copied to destination.
1047 */
Steve McKay4a1ca862016-02-17 18:25:47 -08001048 private boolean canCopy(List<DocumentInfo> files, RootInfo root, DocumentInfo dest) {
1049 if (dest == null || !dest.isDirectory() || !dest.isCreateSupported()) {
1050 return false;
1051 }
Steve McKay3da8afc2015-05-05 14:50:00 -07001052
Steve McKay008e9482016-02-18 15:32:16 -08001053 // Can't copy folders to downloads, because we don't show folders there.
Aga Wronskad01aac32016-03-23 15:49:45 -07001054 if (root.isDownloads()) {
Steve McKay3da8afc2015-05-05 14:50:00 -07001055 for (DocumentInfo docs : files) {
1056 if (docs.isDirectory()) {
1057 return false;
1058 }
1059 }
1060 }
1061
Steve McKay4a1ca862016-02-17 18:25:47 -08001062 return true;
Steve McKay3da8afc2015-05-05 14:50:00 -07001063 }
1064
Steve McKayf8621552015-11-03 15:23:16 -08001065 public void selectAllFiles() {
Ben Kwa862b96412015-12-07 13:25:27 -08001066 // Only select things currently visible in the adapter.
1067 boolean changed = mSelectionManager.setItemsSelected(mAdapter.getModelIds(), true);
Steve McKayb04b1642015-07-24 13:14:20 -07001068 if (changed) {
1069 updateDisplayState();
1070 }
Steve McKay3da8afc2015-05-05 14:50:00 -07001071 }
1072
Ben Kwa2036dad2016-02-10 07:46:35 -08001073 /**
1074 * Attempts to restore focus on the directory listing.
1075 */
1076 public void requestFocus() {
1077 mFocusManager.restoreLastFocus();
1078 }
1079
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001080 private void setupDragAndDropOnDocumentView(View view, Cursor cursor) {
1081 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
1082 if (Document.MIME_TYPE_DIR.equals(docMimeType)) {
1083 // Make a directory item a drop target. Drop on non-directories and empty space
1084 // is handled at the list/grid view level.
1085 view.setOnDragListener(mOnDragListener);
1086 }
1087
Ben Kwa990489d2016-02-25 14:10:40 -08001088 // Make all items draggable.
Vladislav Kaznacheev0e842792016-03-23 13:00:26 -07001089 view.setOnLongClickListener(onLongClickListener);
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001090 }
1091
1092 private View.OnDragListener mOnDragListener = new View.OnDragListener() {
1093 @Override
1094 public boolean onDrag(View v, DragEvent event) {
1095 switch (event.getAction()) {
1096 case DragEvent.ACTION_DRAG_STARTED:
1097 // TODO: Check if the event contains droppable data.
1098 return true;
1099
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001100 // TODO: Expand drop target directory on hover?
1101 case DragEvent.ACTION_DRAG_ENTERED:
Ben Kwa1b685e52016-02-24 10:05:53 -08001102 setDropTargetHighlight(v, true);
Ben Kwae3dfebf2016-02-18 16:45:45 -08001103 return true;
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001104 case DragEvent.ACTION_DRAG_EXITED:
Ben Kwa1b685e52016-02-24 10:05:53 -08001105 setDropTargetHighlight(v, false);
Ben Kwae3dfebf2016-02-18 16:45:45 -08001106 return true;
1107
1108 case DragEvent.ACTION_DRAG_LOCATION:
Ben Kwa990489d2016-02-25 14:10:40 -08001109 return true;
1110
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001111 case DragEvent.ACTION_DRAG_ENDED:
Ben Kwa990489d2016-02-25 14:10:40 -08001112 if (event.getResult()) {
1113 // Exit selection mode if the drop was handled.
1114 mSelectionManager.clearSelection();
1115 }
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001116 return true;
1117
1118 case DragEvent.ACTION_DROP:
Ben Kwa990489d2016-02-25 14:10:40 -08001119 // After a drop event, always stop highlighting the target.
Ben Kwa1b685e52016-02-24 10:05:53 -08001120 setDropTargetHighlight(v, false);
Ben Kwa990489d2016-02-25 14:10:40 -08001121 // Don't copy from the cwd into the cwd. Note: this currently doesn't work for
1122 // multi-window drag, because localState isn't carried over from one process to
1123 // another.
1124 Object src = event.getLocalState();
1125 DocumentInfo dst = getDestination(v);
1126 if (Objects.equals(src, dst)) {
1127 return false;
1128 }
Aga Wronskac573bec2016-03-31 16:37:40 -07001129 Metrics.logDragNDrop(getContext());
Ben Kwa990489d2016-02-25 14:10:40 -08001130 copyFromClipData(event.getClipData(), dst);
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001131 return true;
1132 }
1133 return false;
1134 }
Ben Kwae3dfebf2016-02-18 16:45:45 -08001135
Ben Kwa990489d2016-02-25 14:10:40 -08001136 private DocumentInfo getDestination(View v) {
1137 String id = getModelId(v);
1138 if (id != null) {
1139 Cursor dstCursor = mModel.getItem(id);
Steve McKaya1f76802016-02-25 13:34:03 -08001140 assert(dstCursor != null);
Ben Kwa990489d2016-02-25 14:10:40 -08001141 return DocumentInfo.fromDirectoryCursor(dstCursor);
1142 }
1143
1144 if (v == mRecView || v == mEmptyView) {
1145 return getDisplayState().stack.peek();
1146 }
1147
1148 return null;
1149 }
1150
Ben Kwa1b685e52016-02-24 10:05:53 -08001151 private void setDropTargetHighlight(View v, boolean highlight) {
Ben Kwae3dfebf2016-02-18 16:45:45 -08001152 // Note: use exact comparison - this code is searching for views which are children of
1153 // the RecyclerView instance in the UI.
1154 if (v.getParent() == mRecView) {
1155 RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(v);
1156 if (vh instanceof DocumentHolder) {
1157 ((DocumentHolder) vh).setHighlighted(highlight);
1158 }
1159 }
1160 }
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001161 };
1162
Ben Kwa003a9992015-11-30 23:00:02 -08001163 /**
1164 * Gets the model ID for a given motion event (using the event position)
1165 */
1166 private String getModelId(MotionEvent e) {
1167 View view = mRecView.findChildViewUnder(e.getX(), e.getY());
1168 if (view == null) {
1169 return null;
1170 }
1171 RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(view);
1172 if (vh instanceof DocumentHolder) {
1173 return ((DocumentHolder) vh).modelId;
1174 } else {
1175 return null;
1176 }
1177 }
1178
1179 /**
1180 * Gets the model ID for a given RecyclerView item.
1181 * @param view A View that is a document item view, or a child of a document item view.
1182 * @return The Model ID for the given document, or null if the given view is not associated with
1183 * a document item view.
1184 */
1185 private String getModelId(View view) {
Ben Kwae3dfebf2016-02-18 16:45:45 -08001186 View itemView = mRecView.findContainingItemView(view);
1187 if (itemView != null) {
1188 RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(itemView);
1189 if (vh instanceof DocumentHolder) {
1190 return ((DocumentHolder) vh).modelId;
Vladislav Kaznacheevb5999ef2015-09-04 09:17:37 -07001191 }
Vladislav Kaznacheevb5999ef2015-09-04 09:17:37 -07001192 }
Ben Kwae3dfebf2016-02-18 16:45:45 -08001193 return null;
Vladislav Kaznacheevb5999ef2015-09-04 09:17:37 -07001194 }
1195
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001196 private List<DocumentInfo> getDraggableDocuments(View currentItemView) {
Ben Kwa003a9992015-11-30 23:00:02 -08001197 String modelId = getModelId(currentItemView);
1198 if (modelId == null) {
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001199 return Collections.EMPTY_LIST;
1200 }
1201
Ben Kwa8250db42015-10-07 14:15:12 -07001202 final List<DocumentInfo> selectedDocs =
1203 mModel.getDocuments(mSelectionManager.getSelection());
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001204 if (!selectedDocs.isEmpty()) {
Ben Kwa003a9992015-11-30 23:00:02 -08001205 if (!isSelected(modelId)) {
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001206 // There is a selection that does not include the current item, drag nothing.
1207 return Collections.EMPTY_LIST;
1208 }
1209 return selectedDocs;
1210 }
1211
Ben Kwa003a9992015-11-30 23:00:02 -08001212 final Cursor cursor = mModel.getItem(modelId);
Steve McKay58efce32015-08-20 16:19:38 +00001213
Steve McKaya1f76802016-02-25 13:34:03 -08001214 assert(cursor != null);
1215
1216 return Lists.newArrayList(
1217 DocumentInfo.fromDirectoryCursor(cursor));
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001218 }
1219
1220 private Drawable getDragShadowIcon(List<DocumentInfo> docs) {
1221 if (docs.size() == 1) {
1222 final DocumentInfo doc = docs.get(0);
Ben Kwad8391492015-12-17 10:37:00 -08001223 return mIconHelper.getDocumentIcon(getActivity(), doc.authority, doc.documentId,
1224 doc.mimeType, doc.icon);
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001225 }
1226 return getActivity().getDrawable(R.drawable.ic_doc_generic);
1227 }
1228
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001229 private class DrawableShadowBuilder extends View.DragShadowBuilder {
1230
1231 private final Drawable mShadow;
1232
1233 private final int mShadowDimension;
1234
1235 public DrawableShadowBuilder(Drawable shadow) {
1236 mShadow = shadow;
1237 mShadowDimension = getResources().getDimensionPixelSize(
1238 R.dimen.drag_shadow_size);
1239 mShadow.setBounds(0, 0, mShadowDimension, mShadowDimension);
1240 }
1241
Ben Kwac64cb252015-08-27 16:04:46 -07001242 @Override
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001243 public void onProvideShadowMetrics(
1244 Point shadowSize, Point shadowTouchPoint) {
1245 shadowSize.set(mShadowDimension, mShadowDimension);
1246 shadowTouchPoint.set(mShadowDimension / 2, mShadowDimension / 2);
1247 }
1248
Ben Kwac64cb252015-08-27 16:04:46 -07001249 @Override
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001250 public void onDrawShadow(Canvas canvas) {
1251 mShadow.draw(canvas);
1252 }
1253 }
Steve McKaybdbd0ff2015-05-20 15:58:42 -07001254
Steve McKaybdbd0ff2015-05-20 15:58:42 -07001255 /**
Steve McKayab7865c2015-05-27 16:11:42 -07001256 * Abstract task providing support for loading documents *off*
1257 * the main thread. And if it isn't obvious, creating a list
1258 * of documents (especially large lists) can be pretty expensive.
1259 */
1260 private abstract class GetDocumentsTask
Steve McKay4b3a13c2015-06-11 10:10:49 -07001261 extends AsyncTask<Selection, Void, List<DocumentInfo>> {
Steve McKayab7865c2015-05-27 16:11:42 -07001262 @Override
Steve McKay4b3a13c2015-06-11 10:10:49 -07001263 protected final List<DocumentInfo> doInBackground(Selection... selected) {
Ben Kwac64cb252015-08-27 16:04:46 -07001264 return mModel.getDocuments(selected[0]);
Steve McKayab7865c2015-05-27 16:11:42 -07001265 }
1266
1267 @Override
1268 protected final void onPostExecute(List<DocumentInfo> docs) {
1269 onDocumentsReady(docs);
1270 }
1271
1272 abstract void onDocumentsReady(List<DocumentInfo> docs);
1273 }
1274
Steve McKayef16f5f2015-12-22 18:15:31 -08001275 @Override
1276 public boolean isSelected(String modelId) {
Ben Kwa743c7c22015-12-01 19:56:57 -08001277 return mSelectionManager.getSelection().contains(modelId);
Ben Kwa91caed82015-09-21 10:49:52 -07001278 }
Ben Kwa18fce3c2015-09-01 11:03:01 -07001279
Ben Kwa0436a752016-01-15 10:43:24 -08001280 private class ItemEventListener implements DocumentHolder.EventListener {
Ben Kwabd964562015-10-14 08:00:27 -07001281 @Override
Ben Kwa0436a752016-01-15 10:43:24 -08001282 public boolean onActivate(DocumentHolder doc) {
Ben Kwa67924892016-01-27 09:58:36 -08001283 // Toggle selection if we're in selection mode, othewise, view item.
1284 if (mSelectionManager.hasSelection()) {
1285 mSelectionManager.toggleSelection(doc.modelId);
1286 } else {
1287 handleViewItem(doc.modelId);
1288 }
Ben Kwa0436a752016-01-15 10:43:24 -08001289 return true;
1290 }
1291
1292 @Override
1293 public boolean onSelect(DocumentHolder doc) {
1294 mSelectionManager.toggleSelection(doc.modelId);
1295 mSelectionManager.setSelectionRangeBegin(doc.getAdapterPosition());
1296 return true;
Ben Kwabd964562015-10-14 08:00:27 -07001297 }
Ben Kwa67924892016-01-27 09:58:36 -08001298
1299 @Override
1300 public boolean onKey(DocumentHolder doc, int keyCode, KeyEvent event) {
1301 // Only handle key-down events. This is simpler, consistent with most other UIs, and
1302 // enables the handling of repeated key events from holding down a key.
1303 if (event.getAction() != KeyEvent.ACTION_DOWN) {
1304 return false;
1305 }
1306
Ben Kwa22937c92016-02-23 23:00:01 -08001307 // Ignore tab key events. Those should be handled by the top-level key handler.
1308 if (keyCode == KeyEvent.KEYCODE_TAB) {
1309 return false;
1310 }
1311
Ben Kwab0bfe2d2016-02-09 11:27:45 -08001312 if (mFocusManager.handleKey(doc, keyCode, event)) {
Ben Kwa83df50f2016-02-10 14:01:19 -08001313 // Handle range selection adjustments. Extending the selection will adjust the
1314 // bounds of the in-progress range selection. Each time an unshifted navigation
1315 // event is received, the range selection is restarted.
1316 if (shouldExtendSelection(event)) {
1317 if (!mSelectionManager.isRangeSelectionActive()) {
1318 // Start a range selection if one isn't active
1319 mSelectionManager.startRangeSelection(doc.getAdapterPosition());
1320 }
1321 mSelectionManager.snapRangeSelection(mFocusManager.getFocusPosition());
1322 } else {
1323 mSelectionManager.endRangeSelection();
1324 }
Ben Kwab0bfe2d2016-02-09 11:27:45 -08001325 return true;
Ben Kwa67924892016-01-27 09:58:36 -08001326 }
1327
Ben Kwab0bfe2d2016-02-09 11:27:45 -08001328 // Handle enter key events
Ben Kwa57c8ab92016-03-11 23:00:01 -08001329 switch (keyCode) {
1330 case KeyEvent.KEYCODE_ENTER:
1331 if (event.isShiftPressed()) {
1332 return onSelect(doc);
1333 }
1334 // For non-shifted enter keypresses, fall through.
1335 case KeyEvent.KEYCODE_DPAD_CENTER:
1336 case KeyEvent.KEYCODE_BUTTON_A:
Ben Kwa09eb77b2016-02-10 08:57:25 -08001337 return onActivate(doc);
Ben Kwaeb61aac2016-03-14 15:12:12 -07001338 case KeyEvent.KEYCODE_FORWARD_DEL:
1339 // This has to be handled here instead of in a keyboard shortcut, because
1340 // keyboard shortcuts all have to be modified with the 'Ctrl' key.
1341 if (mSelectionManager.hasSelection()) {
Aga Wronska0614c162016-03-31 15:08:43 -07001342 Metrics.logKeyboardAction(getContext(), keyCode);
Ben Kwaeb61aac2016-03-14 15:12:12 -07001343 deleteDocuments(mSelectionManager.getSelection());
1344 }
1345 // Always handle the key, even if there was nothing to delete. This is a
1346 // precaution to prevent other handlers from potentially picking up the event
1347 // and triggering extra behaviours.
1348 return true;
Ben Kwab0bfe2d2016-02-09 11:27:45 -08001349 }
1350
1351 return false;
Ben Kwa67924892016-01-27 09:58:36 -08001352 }
Ben Kwa83df50f2016-02-10 14:01:19 -08001353
1354 private boolean shouldExtendSelection(KeyEvent event) {
1355 return Events.isNavigationKeyCode(event.getKeyCode()) &&
1356 event.isShiftPressed();
1357 }
Ben Kwabd964562015-10-14 08:00:27 -07001358 }
1359
Steve McKayef16f5f2015-12-22 18:15:31 -08001360 private final class ModelUpdateListener implements Model.UpdateListener {
Ben Kwa91caed82015-09-21 10:49:52 -07001361 @Override
1362 public void onModelUpdate(Model model) {
1363 if (model.info != null || model.error != null) {
1364 mMessageBar.setInfo(model.info);
1365 mMessageBar.setError(model.error);
1366 mMessageBar.show();
1367 }
Ben Kwa18fce3c2015-09-01 11:03:01 -07001368
Ben Kwa91caed82015-09-21 10:49:52 -07001369 mProgressBar.setVisibility(model.isLoading() ? View.VISIBLE : View.GONE);
1370
1371 if (model.isEmpty()) {
Aga Wronska893390b2016-02-17 13:50:42 -08001372 if (mSearchMode) {
Steve McKay9ed88a42016-01-21 18:46:15 -08001373 showNoResults(getDisplayState().stack.root);
1374 } else {
1375 showEmptyDirectory();
1376 }
Ben Kwa91caed82015-09-21 10:49:52 -07001377 } else {
Steve McKay9ed88a42016-01-21 18:46:15 -08001378 showDirectory();
Ben Kwa91bec532015-09-16 13:15:38 -07001379 mAdapter.notifyDataSetChanged();
Ben Kwa91caed82015-09-21 10:49:52 -07001380 }
Tomasz Mikolajewskiae6d6b42016-02-24 12:53:44 +09001381
1382 if (!model.isLoading()) {
1383 ((BaseActivity) getActivity()).notifyDirectoryLoaded(
1384 model.doc != null ? model.doc.derivedUri : null);
1385 }
Ben Kwa91caed82015-09-21 10:49:52 -07001386 }
1387
1388 @Override
1389 public void onModelUpdateFailed(Exception e) {
Steve McKay9ed88a42016-01-21 18:46:15 -08001390 showQueryError();
Ben Kwa18fce3c2015-09-01 11:03:01 -07001391 }
Ben Kwac64cb252015-08-27 16:04:46 -07001392 }
Ben Kwa0436a752016-01-15 10:43:24 -08001393
Vladislav Kaznacheev0e842792016-03-23 13:00:26 -07001394 private DragStartHelper.OnDragStartListener mOnDragStartListener =
1395 new DragStartHelper.OnDragStartListener() {
Steve McKay41585d52016-01-21 15:10:39 -08001396 @Override
Vladislav Kaznacheev0e842792016-03-23 13:00:26 -07001397 public boolean onDragStart(View v, DragStartHelper helper) {
Ben Kwae3dfebf2016-02-18 16:45:45 -08001398 if (isSelected(getModelId(v))) {
Steve McKay41585d52016-01-21 15:10:39 -08001399 List<DocumentInfo> docs = getDraggableDocuments(v);
1400 if (docs.isEmpty()) {
1401 return false;
1402 }
Ben Kwa990489d2016-02-25 14:10:40 -08001403 v.startDragAndDrop(
1404 mClipper.getClipDataForDocuments(docs),
Steve McKay41585d52016-01-21 15:10:39 -08001405 new DrawableShadowBuilder(getDragShadowIcon(docs)),
Ben Kwa990489d2016-02-25 14:10:40 -08001406 getDisplayState().stack.peek(),
Steve McKay41585d52016-01-21 15:10:39 -08001407 View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ |
1408 View.DRAG_FLAG_GLOBAL_URI_WRITE
1409 );
1410 return true;
1411 }
1412
1413 return false;
1414 }
1415 };
1416
Vladislav Kaznacheev0e842792016-03-23 13:00:26 -07001417 private DragStartHelper mDragHelper = new DragStartHelper(null, mOnDragStartListener);
1418
1419 private View.OnLongClickListener onLongClickListener = new View.OnLongClickListener() {
1420 @Override
1421 public boolean onLongClick(View v) {
1422 return mDragHelper.onLongClick(v);
1423 }
1424 };
1425
Steve McKay41585d52016-01-21 15:10:39 -08001426 // Previously we listened to events with one class, only to bounce them forward
1427 // to GestureDetector. We're still doing that here, but with a single class
1428 // that reduces overall complexity in our glue code.
1429 private static final class ListeningGestureDetector extends GestureDetector
1430 implements OnItemTouchListener {
1431
1432 private int mLastTool = -1;
Ben Kwae3dfebf2016-02-18 16:45:45 -08001433 private DragStartHelper mDragHelper;
Steve McKay41585d52016-01-21 15:10:39 -08001434
Ben Kwae3dfebf2016-02-18 16:45:45 -08001435 public ListeningGestureDetector(
1436 Context context, DragStartHelper dragHelper, GestureListener listener) {
Steve McKay41585d52016-01-21 15:10:39 -08001437 super(context, listener);
Ben Kwae3dfebf2016-02-18 16:45:45 -08001438 mDragHelper = dragHelper;
Steve McKay41585d52016-01-21 15:10:39 -08001439 setOnDoubleTapListener(listener);
1440 }
1441
1442 boolean mouseSpawnedLastEvent() {
1443 return Events.isMouseType(mLastTool);
1444 }
1445
1446 boolean touchSpawnedLastEvent() {
1447 return Events.isTouchType(mLastTool);
1448 }
1449
1450 @Override
1451 public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
1452 mLastTool = e.getToolType(0);
Ben Kwae3dfebf2016-02-18 16:45:45 -08001453
1454 // Detect drag events. When a drag is detected, intercept the rest of the gesture.
1455 View itemView = rv.findChildViewUnder(e.getX(), e.getY());
Vladislav Kaznacheev0e842792016-03-23 13:00:26 -07001456 if (itemView != null && mDragHelper.onTouch(itemView, e)) {
Ben Kwae3dfebf2016-02-18 16:45:45 -08001457 return true;
1458 }
1459 // Forward unhandled events to the GestureDetector.
1460 onTouchEvent(e);
1461
Steve McKay41585d52016-01-21 15:10:39 -08001462 return false;
1463 }
1464
1465 @Override
Ben Kwae3dfebf2016-02-18 16:45:45 -08001466 public void onTouchEvent(RecyclerView rv, MotionEvent e) {
1467 View itemView = rv.findChildViewUnder(e.getX(), e.getY());
Vladislav Kaznacheev0e842792016-03-23 13:00:26 -07001468 mDragHelper.onTouch(itemView, e);
Ben Kwae3dfebf2016-02-18 16:45:45 -08001469 // Note: even though this event is being handled as part of a drag gesture, continue
1470 // forwarding to the GestureDetector. The detector needs to see the entire cluster of
1471 // events in order to properly interpret gestures.
1472 onTouchEvent(e);
1473 }
Steve McKay41585d52016-01-21 15:10:39 -08001474
1475 @Override
1476 public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {}
1477 }
1478
Ben Kwa0436a752016-01-15 10:43:24 -08001479 /**
1480 * The gesture listener for items in the list/grid view. Interprets gestures and sends the
1481 * events to the target DocumentHolder, whence they are routed to the appropriate listener.
1482 */
1483 private class GestureListener extends GestureDetector.SimpleOnGestureListener {
1484 @Override
1485 public boolean onSingleTapUp(MotionEvent e) {
1486 // Single tap logic:
1487 // If the selection manager is active, it gets first whack at handling tap
1488 // events. Otherwise, tap events are routed to the target DocumentHolder.
1489 boolean handled = mSelectionManager.onSingleTapUp(
1490 new MotionInputEvent(e, mRecView));
1491
1492 if (handled) {
1493 return handled;
1494 }
1495
1496 // Give the DocumentHolder a crack at the event.
1497 DocumentHolder holder = getTarget(e);
1498 if (holder != null) {
1499 handled = holder.onSingleTapUp(e);
1500 }
1501
1502 return handled;
1503 }
1504
1505 @Override
1506 public void onLongPress(MotionEvent e) {
1507 // Long-press events get routed directly to the selection manager. They can be
1508 // changed to route through the DocumentHolder if necessary.
1509 mSelectionManager.onLongPress(new MotionInputEvent(e, mRecView));
1510 }
1511
1512 @Override
1513 public boolean onDoubleTap(MotionEvent e) {
1514 // Double-tap events are handled directly by the DirectoryFragment. They can be changed
1515 // to route through the DocumentHolder if necessary.
1516 return DirectoryFragment.this.onDoubleTap(e);
1517 }
1518
1519 private @Nullable DocumentHolder getTarget(MotionEvent e) {
1520 View childView = mRecView.findChildViewUnder(e.getX(), e.getY());
1521 if (childView != null) {
1522 return (DocumentHolder) mRecView.getChildViewHolder(childView);
1523 } else {
1524 return null;
1525 }
1526 }
1527 }
Steve McKay9ed88a42016-01-21 18:46:15 -08001528
1529 public static void showDirectory(
1530 FragmentManager fm, RootInfo root, DocumentInfo doc, int anim) {
Aga Wronska893390b2016-02-17 13:50:42 -08001531 create(fm, TYPE_NORMAL, root, doc, null, anim);
Steve McKay9ed88a42016-01-21 18:46:15 -08001532 }
1533
1534 public static void showRecentsOpen(FragmentManager fm, int anim) {
Aga Wronska893390b2016-02-17 13:50:42 -08001535 create(fm, TYPE_RECENT_OPEN, null, null, null, anim);
Steve McKay9ed88a42016-01-21 18:46:15 -08001536 }
1537
Aga Wronska893390b2016-02-17 13:50:42 -08001538 public static void reloadSearch(FragmentManager fm, RootInfo root, DocumentInfo doc,
1539 String query) {
1540 DirectoryFragment df = get(fm);
1541
1542 df.mQuery = query;
1543 df.mRoot = root;
1544 df.mDocument = doc;
1545 df.mSearchMode = query != null;
1546 df.getLoaderManager().restartLoader(LOADER_ID, null, df);
1547 }
1548
1549 public static void reload(FragmentManager fm, int type, RootInfo root, DocumentInfo doc,
1550 String query) {
1551 DirectoryFragment df = get(fm);
1552 df.mType = type;
1553 df.mQuery = query;
1554 df.mRoot = root;
1555 df.mDocument = doc;
1556 df.mSearchMode = query != null;
1557 df.getLoaderManager().restartLoader(LOADER_ID, null, df);
1558 }
1559
1560 public static void create(FragmentManager fm, int type, RootInfo root, DocumentInfo doc,
Steve McKay9ed88a42016-01-21 18:46:15 -08001561 String query, int anim) {
1562 final Bundle args = new Bundle();
Aga Wronska893390b2016-02-17 13:50:42 -08001563 args.putInt(Shared.EXTRA_TYPE, type);
1564 args.putParcelable(Shared.EXTRA_ROOT, root);
1565 args.putParcelable(Shared.EXTRA_DOC, doc);
1566 args.putString(Shared.EXTRA_QUERY, query);
Aga Wronskad5597432016-02-26 11:36:07 -08001567 args.putParcelable(Shared.EXTRA_SELECTION, new Selection());
Steve McKay9ed88a42016-01-21 18:46:15 -08001568
1569 final FragmentTransaction ft = fm.beginTransaction();
Steve McKayce710822016-03-11 10:49:32 -08001570 AnimationView.setupAnimations(ft, anim, args);
Steve McKay9ed88a42016-01-21 18:46:15 -08001571
1572 final DirectoryFragment fragment = new DirectoryFragment();
1573 fragment.setArguments(args);
1574
Aga Wronska893390b2016-02-17 13:50:42 -08001575 ft.replace(getFragmentId(), fragment);
Steve McKay9ed88a42016-01-21 18:46:15 -08001576 ft.commitAllowingStateLoss();
1577 }
1578
1579 private static String buildStateKey(RootInfo root, DocumentInfo doc) {
1580 final StringBuilder builder = new StringBuilder();
1581 builder.append(root != null ? root.authority : "null").append(';');
1582 builder.append(root != null ? root.rootId : "null").append(';');
1583 builder.append(doc != null ? doc.documentId : "null");
1584 return builder.toString();
1585 }
1586
1587 public static @Nullable DirectoryFragment get(FragmentManager fm) {
1588 // TODO: deal with multiple directories shown at once
Aga Wronska893390b2016-02-17 13:50:42 -08001589 Fragment fragment = fm.findFragmentById(getFragmentId());
Steve McKay9ed88a42016-01-21 18:46:15 -08001590 return fragment instanceof DirectoryFragment
1591 ? (DirectoryFragment) fragment
1592 : null;
1593 }
Aga Wronska893390b2016-02-17 13:50:42 -08001594
1595 private static int getFragmentId() {
1596 return R.id.container_directory;
1597 }
1598
1599 @Override
1600 public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) {
1601 Context context = getActivity();
1602 State state = getDisplayState();
1603
1604 Uri contentsUri;
1605 switch (mType) {
1606 case TYPE_NORMAL:
1607 contentsUri = mSearchMode ? DocumentsContract.buildSearchDocumentsUri(
1608 mRoot.authority, mRoot.rootId, mQuery)
1609 : DocumentsContract.buildChildDocumentsUri(
1610 mDocument.authority, mDocument.documentId);
Steve McKay75fe6bc2016-03-07 11:51:31 -08001611 if (mTuner.enableManagedMode()) {
Aga Wronska893390b2016-02-17 13:50:42 -08001612 contentsUri = DocumentsContract.setManageMode(contentsUri);
1613 }
1614 return new DirectoryLoader(
Steve McKay27d20a32016-02-22 18:38:09 -08001615 context, mType, mRoot, mDocument, contentsUri, state.userSortOrder,
1616 mSearchMode);
Aga Wronska893390b2016-02-17 13:50:42 -08001617 case TYPE_RECENT_OPEN:
1618 final RootsCache roots = DocumentsApplication.getRootsCache(context);
1619 return new RecentsLoader(context, roots, state);
Steve McKay75fe6bc2016-03-07 11:51:31 -08001620
Aga Wronska893390b2016-02-17 13:50:42 -08001621 default:
1622 throw new IllegalStateException("Unknown type " + mType);
1623 }
1624 }
1625
1626 @Override
1627 public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
1628 if (!isAdded()) return;
Aga Wronskac573bec2016-03-31 16:37:40 -07001629 if (mSearchMode) {
1630 Metrics.logSearch(getContext());
1631 }
Aga Wronska893390b2016-02-17 13:50:42 -08001632
1633 State state = getDisplayState();
1634
1635 mAdapter.notifyDataSetChanged();
1636 mModel.update(result);
1637
1638 state.derivedSortOrder = result.sortOrder;
1639
1640 updateLayout(state.derivedMode);
1641
1642 if (mSelection != null) {
1643 mSelectionManager.setItemsSelected(mSelection.toList(), true);
1644 }
1645
1646 // Restore any previous instance state
1647 final SparseArray<Parcelable> container = state.dirState.remove(mStateKey);
1648 if (container != null && !getArguments().getBoolean(Shared.EXTRA_IGNORE_STATE, false)) {
1649 getView().restoreHierarchyState(container);
1650 } else if (mLastSortOrder != state.derivedSortOrder) {
1651 // The derived sort order takes the user sort order into account, but applies
1652 // directory-specific defaults when the user doesn't explicitly set the sort
1653 // order. Scroll to the top if the sort order actually changed.
1654 mRecView.smoothScrollToPosition(0);
1655 }
1656
1657 mLastSortOrder = state.derivedSortOrder;
1658
1659 mTuner.onModelLoaded(mModel, mType, mSearchMode);
1660
1661 }
1662
1663 @Override
1664 public void onLoaderReset(Loader<DirectoryResult> loader) {
1665 mModel.update(null);
1666 }
1667 }