blob: 19679231c13053bca87e597ec8b86367c348341e [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;
Ben Kwa67924892016-01-27 09:58:36 -080066import android.view.KeyEvent;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070067import android.view.LayoutInflater;
Jeff Sharkey54e55b72013-06-30 20:02:59 -070068import android.view.Menu;
Jeff Sharkey54e55b72013-06-30 20:02:59 -070069import android.view.MenuItem;
Steve McKay4b3a13c2015-06-11 10:10:49 -070070import android.view.MotionEvent;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070071import android.view.View;
72import android.view.ViewGroup;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070073import android.widget.ImageView;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070074import android.widget.TextView;
75
Steve McKayf8621552015-11-03 15:23:16 -080076import com.android.documentsui.BaseActivity;
Steve McKayf8621552015-11-03 15:23:16 -080077import com.android.documentsui.DirectoryLoader;
78import com.android.documentsui.DirectoryResult;
79import com.android.documentsui.DocumentClipper;
80import com.android.documentsui.DocumentsActivity;
81import com.android.documentsui.DocumentsApplication;
82import com.android.documentsui.Events;
Ben Kwa0436a752016-01-15 10:43:24 -080083import com.android.documentsui.Events.MotionInputEvent;
Steve McKayf8621552015-11-03 15:23:16 -080084import com.android.documentsui.Menus;
85import com.android.documentsui.MessageBar;
86import com.android.documentsui.MimePredicate;
Steve McKayf8621552015-11-03 15:23:16 -080087import com.android.documentsui.R;
Steve McKaye2af0782016-02-18 09:48:39 -080088import com.android.documentsui.RecentsLoader;
Steve McKayf8621552015-11-03 15:23:16 -080089import com.android.documentsui.RootsCache;
90import com.android.documentsui.Shared;
91import com.android.documentsui.Snackbars;
92import com.android.documentsui.State;
Ben Kwa1e2fa5e2016-02-02 23:00:02 -080093import com.android.documentsui.State.ViewMode;
Steve McKayf8621552015-11-03 15:23:16 -080094import com.android.documentsui.dirlist.MultiSelectManager.Selection;
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -070095import com.android.documentsui.model.DocumentInfo;
Tomasz Mikolajewskif8c3f322015-04-14 16:32:41 +090096import com.android.documentsui.model.DocumentStack;
Jeff Sharkey251097b2013-09-02 15:07:28 -070097import com.android.documentsui.model.RootInfo;
Steve McKay14e827a2016-01-06 18:32:13 -080098import com.android.documentsui.services.FileOperationService;
Ben Kwa1e2fa5e2016-02-02 23:00:02 -080099import com.android.documentsui.services.FileOperationService.OpType;
Steve McKay14e827a2016-01-06 18:32:13 -0800100import com.android.documentsui.services.FileOperations;
Steve McKaya1f76802016-02-25 13:34:03 -0800101
Steve McKay58efce32015-08-20 16:19:38 +0000102import com.google.common.collect.Lists;
Jeff Sharkeybe8b12e2013-07-01 16:56:54 -0700103
Steve McKayaa15dae2016-02-09 16:17:24 -0800104import java.lang.annotation.Retention;
105import java.lang.annotation.RetentionPolicy;
Jeff Sharkeybe8b12e2013-07-01 16:56:54 -0700106import java.util.ArrayList;
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -0700107import java.util.Collections;
Jeff Sharkeyef7184a2013-08-05 17:56:48 -0700108import java.util.List;
Ben Kwa990489d2016-02-25 14:10:40 -0800109import java.util.Objects;
Jeff Sharkey54e55b72013-06-30 20:02:59 -0700110
111/**
112 * Display the documents inside a single directory.
113 */
Aga Wronska893390b2016-02-17 13:50:42 -0800114public class DirectoryFragment extends Fragment
115 implements DocumentsAdapter.Environment, LoaderCallbacks<DirectoryResult> {
Jeff Sharkey54e55b72013-06-30 20:02:59 -0700116
Steve McKayaa15dae2016-02-09 16:17:24 -0800117 @IntDef(flag = true, value = {
118 TYPE_NORMAL,
Steve McKayaa15dae2016-02-09 16:17:24 -0800119 TYPE_RECENT_OPEN
120 })
121 @Retention(RetentionPolicy.SOURCE)
122 public @interface ResultType {}
Jeff Sharkeyef7184a2013-08-05 17:56:48 -0700123 public static final int TYPE_NORMAL = 1;
Aga Wronska893390b2016-02-17 13:50:42 -0800124 public static final int TYPE_RECENT_OPEN = 2;
Jeff Sharkeydc2963a2013-08-02 15:55:26 -0700125
Aga Wronska893390b2016-02-17 13:50:42 -0800126 @IntDef(flag = true, value = {
127 ANIM_NONE,
128 ANIM_SIDE,
129 ANIM_LEAVE,
130 ANIM_ENTER
131 })
132 @Retention(RetentionPolicy.SOURCE)
133 public @interface AnimationType {}
Jeff Sharkeye20a3ac2013-09-18 16:26:49 -0700134 public static final int ANIM_NONE = 1;
135 public static final int ANIM_SIDE = 2;
Tomasz Mikolajewskie3fe9d72016-02-03 16:53:21 +0900136 public static final int ANIM_LEAVE = 3;
137 public static final int ANIM_ENTER = 4;
Jeff Sharkeye20a3ac2013-09-18 16:26:49 -0700138
Steve McKaya6bbeab2016-02-17 15:02:01 -0800139 @IntDef(flag = true, value = {
140 REQUEST_COPY_DESTINATION
141 })
142 @Retention(RetentionPolicy.SOURCE)
143 public @interface RequestCode {}
Ben Kwaef3f2622015-04-07 15:43:39 -0700144 public static final int REQUEST_COPY_DESTINATION = 1;
145
Steve McKay35645432016-01-20 15:09:35 -0800146 private static final String TAG = "DirectoryFragment";
147 private static final int LOADER_ID = 42;
Steve McKay35645432016-01-20 15:09:35 -0800148
Ben Kwa18fce3c2015-09-01 11:03:01 -0700149 private Model mModel;
Ben Kwa8250db42015-10-07 14:15:12 -0700150 private MultiSelectManager mSelectionManager;
Ben Kwa91caed82015-09-21 10:49:52 -0700151 private Model.UpdateListener mModelUpdateListener = new ModelUpdateListener();
Ben Kwa0436a752016-01-15 10:43:24 -0800152 private ItemEventListener mItemEventListener = new ItemEventListener();
Ben Kwab0bfe2d2016-02-09 11:27:45 -0800153 private FocusManager mFocusManager;
Ben Kwac64cb252015-08-27 16:04:46 -0700154
Ben Kwad8391492015-12-17 10:37:00 -0800155 private IconHelper mIconHelper;
156
Steve McKay4b3a13c2015-06-11 10:10:49 -0700157 private View mEmptyView;
158 private RecyclerView mRecView;
Steve McKay41585d52016-01-21 15:10:39 -0800159 private ListeningGestureDetector mGestureDetector;
Steve McKay4b3a13c2015-06-11 10:10:49 -0700160
Steve McKay4b3a13c2015-06-11 10:10:49 -0700161 private String mStateKey;
162
Steve McKay4b3a13c2015-06-11 10:10:49 -0700163 private int mLastSortOrder = SORT_ORDER_UNKNOWN;
Steve McKay4b3a13c2015-06-11 10:10:49 -0700164 private DocumentsAdapter mAdapter;
Steve McKayf69502c2015-11-09 17:38:35 +0900165 private FragmentTuner mTuner;
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700166 private DocumentClipper mClipper;
Ben Kwa1e2fa5e2016-02-02 23:00:02 -0800167 private GridLayoutManager mLayout;
Steve McKay0ba3a142015-07-23 16:33:41 -0700168 private int mColumnCount = 1; // This will get updated when layout changes.
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700169
Steve McKayc8d4e222016-03-01 08:28:02 -0800170 private LayoutInflater mInflater;
Ben Kwa3b19e312015-09-16 08:04:37 -0700171 private MessageBar mMessageBar;
Ben Kwa91caed82015-09-21 10:49:52 -0700172 private View mProgressBar;
Ben Kwa3b19e312015-09-16 08:04:37 -0700173
Aga Wronska893390b2016-02-17 13:50:42 -0800174 // Directory fragment state is defined by: root, document, query, type, selection
175 private @ResultType int mType = TYPE_NORMAL;
176 private RootInfo mRoot;
177 private DocumentInfo mDocument;
178 private String mQuery = null;
179 private Selection mSelection = null;
180 private boolean mSearchMode = false;
181
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700182 @Override
183 public View onCreateView(
184 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
Steve McKayc8d4e222016-03-01 08:28:02 -0800185 mInflater = inflater;
Jeff Sharkey54e55b72013-06-30 20:02:59 -0700186 final View view = inflater.inflate(R.layout.fragment_directory, container, false);
187
Ben Kwa3b19e312015-09-16 08:04:37 -0700188 mMessageBar = MessageBar.create(getChildFragmentManager());
Ben Kwa91caed82015-09-21 10:49:52 -0700189 mProgressBar = view.findViewById(R.id.progressbar);
Jeff Sharkey9fb567b2013-08-07 16:22:02 -0700190 mEmptyView = view.findViewById(android.R.id.empty);
Ben Kwa2036dad2016-02-10 07:46:35 -0800191 mRecView = (RecyclerView) view.findViewById(R.id.dir_list);
Steve McKay4b3a13c2015-06-11 10:10:49 -0700192 mRecView.setRecyclerListener(
193 new RecyclerListener() {
194 @Override
195 public void onViewRecycled(ViewHolder holder) {
196 cancelThumbnailTask(holder.itemView);
197 }
198 });
Steve McKayb46383b2015-05-06 14:27:57 -0700199
Ben Kwae48e4ca2015-10-20 15:02:33 -0700200 mRecView.setItemAnimator(new DirectoryItemAnimator(getActivity()));
Ben Kwabd964562015-10-14 08:00:27 -0700201
Ben Kwa990489d2016-02-25 14:10:40 -0800202 // Make the recycler and the empty views responsive to drop events.
203 mRecView.setOnDragListener(mOnDragListener);
204 mEmptyView.setOnDragListener(mOnDragListener);
Jeff Sharkey54e55b72013-06-30 20:02:59 -0700205
Jeff Sharkey5dfb3452013-08-31 21:27:44 -0700206 return view;
207 }
Jeff Sharkey54e55b72013-06-30 20:02:59 -0700208
Jeff Sharkey5dfb3452013-08-31 21:27:44 -0700209 @Override
Jeff Sharkeyd01571e2013-10-01 17:57:41 -0700210 public void onDestroyView() {
Aga Wronskad5597432016-02-26 11:36:07 -0800211 mSelectionManager.clearSelection();
Jeff Sharkeyd01571e2013-10-01 17:57:41 -0700212
213 // Cancel any outstanding thumbnail requests
Steve McKay4b3a13c2015-06-11 10:10:49 -0700214 final int count = mRecView.getChildCount();
Jeff Sharkeyd01571e2013-10-01 17:57:41 -0700215 for (int i = 0; i < count; i++) {
Steve McKay4b3a13c2015-06-11 10:10:49 -0700216 final View view = mRecView.getChildAt(i);
217 cancelThumbnailTask(view);
Jeff Sharkeyd01571e2013-10-01 17:57:41 -0700218 }
Aga Wronskad5597432016-02-26 11:36:07 -0800219
220 super.onDestroyView();
Jeff Sharkeyd01571e2013-10-01 17:57:41 -0700221 }
222
223 @Override
Jeff Sharkey5dfb3452013-08-31 21:27:44 -0700224 public void onActivityCreated(Bundle savedInstanceState) {
225 super.onActivityCreated(savedInstanceState);
226
227 final Context context = getActivity();
Steve McKayf69502c2015-11-09 17:38:35 +0900228 final State state = getDisplayState();
Jeff Sharkey5dfb3452013-08-31 21:27:44 -0700229
Aga Wronska893390b2016-02-17 13:50:42 -0800230 // Read arguments when object created for the first time.
231 // Restore state if fragment recreated.
232 Bundle args = savedInstanceState == null ? getArguments() : savedInstanceState;
233 mRoot = args.getParcelable(Shared.EXTRA_ROOT);
234 mDocument = args.getParcelable(Shared.EXTRA_DOC);
235 mStateKey = buildStateKey(mRoot, mDocument);
236 mQuery = args.getString(Shared.EXTRA_QUERY);
237 mType = args.getInt(Shared.EXTRA_TYPE);
238 mSelection = args.getParcelable(Shared.EXTRA_SELECTION);
239 mSearchMode = args.getBoolean(Shared.EXTRA_SEARCH_MODE);
Jeff Sharkeyf6db1542013-09-13 13:42:19 -0700240
Steve McKay3eb2d072016-01-25 19:00:22 -0800241 mIconHelper = new IconHelper(context, MODE_GRID);
Steve McKayef16f5f2015-12-22 18:15:31 -0800242
243 mAdapter = new SectionBreakDocumentsAdapterWrapper(
244 this, new ModelBackedDocumentsAdapter(this, mIconHelper));
245
Steve McKay4b3a13c2015-06-11 10:10:49 -0700246 mRecView.setAdapter(mAdapter);
247
Ben Kwa1e2fa5e2016-02-02 23:00:02 -0800248 mLayout = new GridLayoutManager(getContext(), mColumnCount);
249 SpanSizeLookup lookup = mAdapter.createSpanSizeLookup();
250 if (lookup != null) {
251 mLayout.setSpanSizeLookup(lookup);
252 }
253 mRecView.setLayoutManager(mLayout);
254
Ben Kwae3dfebf2016-02-18 16:45:45 -0800255 mGestureDetector =
256 new ListeningGestureDetector(this.getContext(), mDragHelper, new GestureListener());
Steve McKay669ebe72015-10-19 12:04:21 -0700257
Steve McKay41585d52016-01-21 15:10:39 -0800258 mRecView.addOnItemTouchListener(mGestureDetector);
Steve McKay669ebe72015-10-19 12:04:21 -0700259
Ben Kwac64cb252015-08-27 16:04:46 -0700260 // TODO: instead of inserting the view into the constructor, extract listener-creation code
261 // 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 -0700262 // into the selection manager.
263 mSelectionManager = new MultiSelectManager(
Steve McKaydbec47a2015-08-12 14:48:34 -0700264 mRecView,
Steve McKay44408262016-01-05 15:27:17 -0800265 mAdapter,
Steve McKaydbec47a2015-08-12 14:48:34 -0700266 state.allowMultiple
267 ? MultiSelectManager.MODE_MULTIPLE
Steve McKaye852d932016-02-08 19:09:42 -0800268 : MultiSelectManager.MODE_SINGLE,
Aga Wronska893390b2016-02-17 13:50:42 -0800269 null);
Steve McKaye852d932016-02-08 19:09:42 -0800270
Aga Wronska893390b2016-02-17 13:50:42 -0800271 mSelectionManager.addCallback(new SelectionModeListener());
Ben Kwa18fce3c2015-09-01 11:03:01 -0700272
Steve McKay35645432016-01-20 15:09:35 -0800273 mModel = new Model();
Ben Kwa743c7c22015-12-01 19:56:57 -0800274 mModel.addUpdateListener(mAdapter);
Ben Kwa91caed82015-09-21 10:49:52 -0700275 mModel.addUpdateListener(mModelUpdateListener);
Steve McKay4b3a13c2015-06-11 10:10:49 -0700276
Ben Kwaa4acc902016-02-10 15:48:25 -0800277 // Make sure this is done after the RecyclerView is set up.
278 mFocusManager = new FocusManager(context, mRecView, mModel);
279
Steve McKayaa15dae2016-02-09 16:17:24 -0800280 mTuner = FragmentTuner.pick(getContext(), state);
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700281 mClipper = new DocumentClipper(context);
282
Ben Kwad8391492015-12-17 10:37:00 -0800283 boolean hideGridTitles;
Jeff Sharkeyf6db1542013-09-13 13:42:19 -0700284 if (mType == TYPE_RECENT_OPEN) {
285 // Hide titles when showing recents for picking images/videos
Ben Kwad8391492015-12-17 10:37:00 -0800286 hideGridTitles = MimePredicate.mimeMatches(
Jeff Sharkeyf6db1542013-09-13 13:42:19 -0700287 MimePredicate.VISUAL_MIMES, state.acceptMimes);
288 } else {
Aga Wronska893390b2016-02-17 13:50:42 -0800289 hideGridTitles = (mDocument != null) && mDocument.isGridTitlesHidden();
Jeff Sharkeyf6db1542013-09-13 13:42:19 -0700290 }
Ben Kwad8391492015-12-17 10:37:00 -0800291 GridDocumentHolder.setHideTitles(hideGridTitles);
Jeff Sharkeyf6db1542013-09-13 13:42:19 -0700292
Jeff Sharkeyd01571e2013-10-01 17:57:41 -0700293 final ActivityManager am = (ActivityManager) context.getSystemService(
294 Context.ACTIVITY_SERVICE);
Ben Kwad8391492015-12-17 10:37:00 -0800295 boolean svelte = am.isLowRamDevice() && (mType == TYPE_RECENT_OPEN);
296 mIconHelper.setThumbnailsEnabled(!svelte);
Jeff Sharkeyd01571e2013-10-01 17:57:41 -0700297
Jeff Sharkeyd182bb62013-09-07 14:45:03 -0700298 // Kick off loader at least once
Aga Wronska893390b2016-02-17 13:50:42 -0800299 getLoaderManager().restartLoader(LOADER_ID, null, this);
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700300 }
301
Jeff Sharkey28c05ee2013-09-06 13:22:09 -0700302 @Override
Steve McKaye852d932016-02-08 19:09:42 -0800303 public void onSaveInstanceState(Bundle outState) {
Aga Wronska893390b2016-02-17 13:50:42 -0800304 super.onSaveInstanceState(outState);
305
Aga Wronskad5597432016-02-26 11:36:07 -0800306 mSelectionManager.getSelection(mSelection);
307
Aga Wronska893390b2016-02-17 13:50:42 -0800308 outState.putInt(Shared.EXTRA_TYPE, mType);
309 outState.putParcelable(Shared.EXTRA_ROOT, mRoot);
310 outState.putParcelable(Shared.EXTRA_DOC, mDocument);
311 outState.putString(Shared.EXTRA_QUERY, mQuery);
Aga Wronskad5597432016-02-26 11:36:07 -0800312 outState.putParcelable(Shared.EXTRA_SELECTION, mSelection);
Aga Wronska893390b2016-02-17 13:50:42 -0800313 outState.putBoolean(Shared.EXTRA_SEARCH_MODE, mSearchMode);
Aga Wronskad5597432016-02-26 11:36:07 -0800314
Steve McKaye852d932016-02-08 19:09:42 -0800315 }
316
317 @Override
Steve McKaya6bbeab2016-02-17 15:02:01 -0800318 public void onActivityResult(@RequestCode int requestCode, int resultCode, Intent data) {
319 switch(requestCode) {
320 case REQUEST_COPY_DESTINATION:
321 handleCopyResult(resultCode, data);
322 break;
323 default:
324 throw new UnsupportedOperationException("Unknown request code: " + requestCode);
Ben Kwaef3f2622015-04-07 15:43:39 -0700325 }
Steve McKaya6bbeab2016-02-17 15:02:01 -0800326 }
327
328 private void handleCopyResult(int resultCode, Intent data) {
Ben Kwaef3f2622015-04-07 15:43:39 -0700329 if (resultCode == Activity.RESULT_CANCELED || data == null) {
330 // User pressed the back button or otherwise cancelled the destination pick. Don't
331 // proceed with the copy.
332 return;
333 }
334
Steve McKaya6bbeab2016-02-17 15:02:01 -0800335 @OpType int operationType = data.getIntExtra(
Steve McKay14e827a2016-01-06 18:32:13 -0800336 FileOperationService.EXTRA_OPERATION,
337 FileOperationService.OPERATION_COPY);
338
339 FileOperations.start(
340 getActivity(),
341 getDisplayState().selectedDocumentsForCopy,
Tomasz Mikolajewskib8436af2016-01-25 16:20:15 +0900342 getDisplayState().stack.peek(),
Steve McKay323ee3e2015-09-25 16:02:56 -0700343 (DocumentStack) data.getParcelableExtra(Shared.EXTRA_STACK),
Steve McKay14e827a2016-01-06 18:32:13 -0800344 operationType);
Ben Kwaef3f2622015-04-07 15:43:39 -0700345 }
346
Steve McKay5353a1e2015-07-30 12:27:44 -0700347 protected boolean onDoubleTap(MotionEvent e) {
348 if (Events.isMouseEvent(e)) {
Ben Kwa003a9992015-11-30 23:00:02 -0800349 String id = getModelId(e);
350 if (id != null) {
351 return handleViewItem(id);
Steve McKay5353a1e2015-07-30 12:27:44 -0700352 }
353 }
354 return false;
355 }
356
Ben Kwa003a9992015-11-30 23:00:02 -0800357 private boolean handleViewItem(String id) {
358 final Cursor cursor = mModel.getItem(id);
Steve McKaya1f76802016-02-25 13:34:03 -0800359
360 assert(cursor != null);
361
Steve McKay5353a1e2015-07-30 12:27:44 -0700362 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
363 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
Steve McKayf69502c2015-11-09 17:38:35 +0900364 if (mTuner.isDocumentEnabled(docMimeType, docFlags)) {
Steve McKay5353a1e2015-07-30 12:27:44 -0700365 final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
Ben Kwac64cb252015-08-27 16:04:46 -0700366 ((BaseActivity) getActivity()).onDocumentPicked(doc, mModel);
Ben Kwa8250db42015-10-07 14:15:12 -0700367 mSelectionManager.clearSelection();
Steve McKay5353a1e2015-07-30 12:27:44 -0700368 return true;
369 }
Steve McKay4b3a13c2015-06-11 10:10:49 -0700370 return false;
371 }
372
Ben Kwaef3f2622015-04-07 15:43:39 -0700373 @Override
Jeff Sharkeye20a3ac2013-09-18 16:26:49 -0700374 public void onStop() {
375 super.onStop();
376
377 // Remember last scroll location
378 final SparseArray<Parcelable> container = new SparseArray<Parcelable>();
379 getView().saveHierarchyState(container);
Steve McKayf69502c2015-11-09 17:38:35 +0900380 final State state = getDisplayState();
Jeff Sharkeye20a3ac2013-09-18 16:26:49 -0700381 state.dirState.put(mStateKey, container);
382 }
383
Jeff Sharkey669f8e72014-08-08 15:10:03 -0700384 public void onDisplayStateChanged() {
385 updateDisplayState();
386 }
387
Steve McKay3eb2d072016-01-25 19:00:22 -0800388 public void onSortOrderChanged() {
389 // Sort order is implemented as a sorting wrapper around directory
390 // results. So when sort order changes, we force a reload of the directory.
Aga Wronska893390b2016-02-17 13:50:42 -0800391 getLoaderManager().restartLoader(LOADER_ID, null, this);
Jeff Sharkeyd182bb62013-09-07 14:45:03 -0700392 }
393
Steve McKay3eb2d072016-01-25 19:00:22 -0800394 public void onViewModeChanged() {
395 // Mode change is just visual change; no need to kick loader.
Jeff Sharkeyd182bb62013-09-07 14:45:03 -0700396 updateDisplayState();
397 }
398
399 private void updateDisplayState() {
Steve McKay3eb2d072016-01-25 19:00:22 -0800400 State state = getDisplayState();
Steve McKay4b3a13c2015-06-11 10:10:49 -0700401 updateLayout(state.derivedMode);
Steve McKay4b3a13c2015-06-11 10:10:49 -0700402 mRecView.setAdapter(mAdapter);
403 }
404
405 /**
Ben Kwa1e2fa5e2016-02-02 23:00:02 -0800406 * Updates the layout after the view mode switches.
407 * @param mode The new view mode.
Steve McKay4b3a13c2015-06-11 10:10:49 -0700408 */
Ben Kwa1e2fa5e2016-02-02 23:00:02 -0800409 private void updateLayout(@ViewMode int mode) {
410 mColumnCount = calculateColumnCount(mode);
411 if (mLayout != null) {
412 mLayout.setSpanCount(mColumnCount);
Jeff Sharkey54e55b72013-06-30 20:02:59 -0700413 }
Jeff Sharkey9d0843d2013-05-07 12:41:33 -0700414
Steve McKay76be6202016-01-12 11:14:33 -0800415 int pad = getDirectoryPadding(mode);
416 mRecView.setPadding(pad, pad, pad, pad);
Ben Kwa1e2fa5e2016-02-02 23:00:02 -0800417 mRecView.requestLayout();
Steve McKay76be6202016-01-12 11:14:33 -0800418 mSelectionManager.handleLayoutChanged(); // RecyclerView doesn't do this for us
Steve McKay3eb2d072016-01-25 19:00:22 -0800419 mIconHelper.setViewMode(mode);
Jeff Sharkey54e55b72013-06-30 20:02:59 -0700420 }
421
Ben Kwa1e2fa5e2016-02-02 23:00:02 -0800422 private int calculateColumnCount(@ViewMode int mode) {
423 if (mode == MODE_LIST) {
424 // List mode is a "grid" with 1 column.
425 return 1;
426 }
427
Steve McKay58efce32015-08-20 16:19:38 +0000428 int cellWidth = getResources().getDimensionPixelSize(R.dimen.grid_width);
429 int cellMargin = 2 * getResources().getDimensionPixelSize(R.dimen.grid_item_margin);
Steve McKay0ba3a142015-07-23 16:33:41 -0700430 int viewPadding = mRecView.getPaddingLeft() + mRecView.getPaddingRight();
Steve McKay58efce32015-08-20 16:19:38 +0000431
Steve McKaya1f76802016-02-25 13:34:03 -0800432 assert(mRecView.getWidth() > 0);
433
Steve McKay0ba3a142015-07-23 16:33:41 -0700434 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 McKaya1f76802016-02-25 13:34:03 -0800474
475 assert(cursor != null);
476
Jeff Sharkey2a030b02013-09-26 10:54:16 -0700477 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
478 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
Tomasz Mikolajewskia8057a92015-11-16 11:41:28 +0900479 return mTuner.canSelectType(docMimeType, docFlags);
Steve McKay4b3a13c2015-06-11 10:10:49 -0700480 }
481 return true;
482 }
483
484 @Override
Ben Kwa743c7c22015-12-01 19:56:57 -0800485 public void onItemStateChanged(String modelId, boolean selected) {
486 final Cursor cursor = mModel.getItem(modelId);
Steve McKay30551a22016-02-16 13:08:10 -0800487 if (cursor == null) {
488 Log.e(TAG, "Model returned null cursor for document: " + modelId
489 + ". Ignoring state changed event.");
490 return;
491 }
Steve McKay503648d2015-07-22 12:13:46 -0700492
Ben Kwa67924892016-01-27 09:58:36 -0800493 // TODO: Should this be happening in onSelectionChanged? Technically this callback is
494 // triggered on "silent" selection updates (i.e. we might be reacting to unfinalized
495 // selection changes here)
Steve McKay503648d2015-07-22 12:13:46 -0700496 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
Steve McKay27d20a32016-02-22 18:38:09 -0800497 if ((docFlags & Document.FLAG_SUPPORTS_DELETE) == 0
498 && (docFlags & Document.FLAG_SUPPORTS_DELETE) == 0) {
Steve McKay503648d2015-07-22 12:13:46 -0700499 mNoDeleteCount += selected ? 1 : -1;
500 }
Aga Wronska3b327ef2016-01-20 16:32:33 -0800501 if ((docFlags & Document.FLAG_SUPPORTS_RENAME) != 0) {
502 mNoRenameCount += selected ? 1 : -1;
503 }
Steve McKaydbec47a2015-08-12 14:48:34 -0700504 }
Steve McKay503648d2015-07-22 12:13:46 -0700505
Steve McKaydbec47a2015-08-12 14:48:34 -0700506 @Override
507 public void onSelectionChanged() {
Ben Kwa8250db42015-10-07 14:15:12 -0700508 mSelectionManager.getSelection(mSelected);
Ben Kwa7cc80012015-09-11 15:40:18 -0700509 TypedValue color = new TypedValue();
Steve McKay503648d2015-07-22 12:13:46 -0700510 if (mSelected.size() > 0) {
Steve McKay4b3a13c2015-06-11 10:10:49 -0700511 if (DEBUG) Log.d(TAG, "Maybe starting action mode.");
512 if (mActionMode == null) {
513 if (DEBUG) Log.d(TAG, "Yeah. Starting action mode.");
514 mActionMode = getActivity().startActionMode(this);
Jeff Sharkey3f4c2052013-09-09 16:51:06 -0700515 }
Ben Kwa8250db42015-10-07 14:15:12 -0700516 getActivity().getTheme().resolveAttribute(R.attr.colorActionMode, color, true);
Steve McKay503648d2015-07-22 12:13:46 -0700517 updateActionMenu();
518 } else {
519 if (DEBUG) Log.d(TAG, "Finishing action mode.");
520 if (mActionMode != null) {
521 mActionMode.finish();
522 }
Ben Kwa0f7078f02015-09-08 07:31:19 -0700523 getActivity().getTheme().resolveAttribute(
524 android.R.attr.colorPrimaryDark, color, true);
Jeff Sharkeya5599ef2013-08-15 16:17:41 -0700525 }
Ben Kwa7cc80012015-09-11 15:40:18 -0700526 getActivity().getWindow().setStatusBarColor(color.data);
Jeff Sharkey54e55b72013-06-30 20:02:59 -0700527
Steve McKay4b3a13c2015-06-11 10:10:49 -0700528 if (mActionMode != null) {
Steve McKay5aacf1f2015-10-27 14:15:58 -0700529 mActionMode.setTitle(String.valueOf(mSelected.size()));
Steve McKay4b3a13c2015-06-11 10:10:49 -0700530 }
531 }
532
533 // Called when the user exits the action mode
534 @Override
535 public void onDestroyActionMode(ActionMode mode) {
536 if (DEBUG) Log.d(TAG, "Handling action mode destroyed.");
537 mActionMode = null;
538 // clear selection
Ben Kwa8250db42015-10-07 14:15:12 -0700539 mSelectionManager.clearSelection();
Steve McKay503648d2015-07-22 12:13:46 -0700540 mSelected.clear();
541 mNoDeleteCount = 0;
Aga Wronska3b327ef2016-01-20 16:32:33 -0800542 mNoRenameCount = -1;
Steve McKay4b3a13c2015-06-11 10:10:49 -0700543 }
544
Jeff Sharkeybe8b12e2013-07-01 16:56:54 -0700545 @Override
546 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
Ben Kwa8250db42015-10-07 14:15:12 -0700547 int size = mSelectionManager.getSelection().size();
Jeff Sharkeybe8b12e2013-07-01 16:56:54 -0700548 mode.getMenuInflater().inflate(R.menu.mode_directory, menu);
Ben Kwa8250db42015-10-07 14:15:12 -0700549 mode.setTitle(TextUtils.formatSelectedCount(size));
550 return (size > 0);
Jeff Sharkeybe8b12e2013-07-01 16:56:54 -0700551 }
552
553 @Override
554 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
Steve McKay503648d2015-07-22 12:13:46 -0700555 mMenu = menu;
556 updateActionMenu();
Jeff Sharkeybe8b12e2013-07-01 16:56:54 -0700557 return true;
558 }
559
Aga Wronska3b327ef2016-01-20 16:32:33 -0800560 boolean canRenameSelection() {
561 return mNoRenameCount == 0 && mSelectionManager.getSelection().size() == 1;
562 }
563
564 boolean canDeleteSelection() {
565 return mNoDeleteCount == 0;
566 }
567
Steve McKay503648d2015-07-22 12:13:46 -0700568 private void updateActionMenu() {
Steve McKaya1f76802016-02-25 13:34:03 -0800569 assert(mMenu != null);
Aga Wronska3b327ef2016-01-20 16:32:33 -0800570
Steve McKay503648d2015-07-22 12:13:46 -0700571 // Delegate update logic to our owning action, since specialized logic is desired.
Aga Wronska3b327ef2016-01-20 16:32:33 -0800572 mTuner.updateActionMenu(mMenu, mType, canDeleteSelection(), canRenameSelection());
Steve McKay5bbae102015-10-01 11:39:24 -0700573 Menus.disableHiddenItems(mMenu);
Steve McKay503648d2015-07-22 12:13:46 -0700574 }
575
Jeff Sharkeybe8b12e2013-07-01 16:56:54 -0700576 @Override
Steve McKay4b3a13c2015-06-11 10:10:49 -0700577 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700578
Ben Kwa8250db42015-10-07 14:15:12 -0700579 Selection selection = mSelectionManager.getSelection(new Selection());
Jeff Sharkeybe8b12e2013-07-01 16:56:54 -0700580
Steve McKayc2b4b3d2015-12-01 17:02:42 -0800581 switch (item.getItemId()) {
582 case R.id.menu_open:
583 openDocuments(selection);
584 mode.finish();
585 return true;
Jeff Sharkey4eb407a2013-08-18 17:38:20 -0700586
Steve McKayc2b4b3d2015-12-01 17:02:42 -0800587 case R.id.menu_share:
588 shareDocuments(selection);
589 mode.finish();
590 return true;
Jeff Sharkey4eb407a2013-08-18 17:38:20 -0700591
Steve McKayc2b4b3d2015-12-01 17:02:42 -0800592 case R.id.menu_delete:
593 // Exit selection mode first, so we avoid deselecting deleted documents.
594 mode.finish();
595 deleteDocuments(selection);
596 return true;
Jeff Sharkey4eb407a2013-08-18 17:38:20 -0700597
Steve McKayc2b4b3d2015-12-01 17:02:42 -0800598 case R.id.menu_copy_to:
Steve McKay14e827a2016-01-06 18:32:13 -0800599 transferDocuments(selection, FileOperationService.OPERATION_COPY);
Steve McKayc2b4b3d2015-12-01 17:02:42 -0800600 mode.finish();
601 return true;
Ben Kwa0b4a3c42015-05-05 11:50:11 -0700602
Steve McKayc2b4b3d2015-12-01 17:02:42 -0800603 case R.id.menu_move_to:
604 // Exit selection mode first, so we avoid deselecting deleted documents.
605 mode.finish();
Steve McKay14e827a2016-01-06 18:32:13 -0800606 transferDocuments(selection, FileOperationService.OPERATION_MOVE);
Steve McKayc2b4b3d2015-12-01 17:02:42 -0800607 return true;
Ben Kwad99109f2015-03-31 10:11:43 -0700608
Steve McKayc2b4b3d2015-12-01 17:02:42 -0800609 case R.id.menu_copy_to_clipboard:
Steve McKaycbee5442016-01-28 15:30:10 -0800610 copySelectedToClipboard();
Steve McKayc2b4b3d2015-12-01 17:02:42 -0800611 return true;
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700612
Steve McKayc2b4b3d2015-12-01 17:02:42 -0800613 case R.id.menu_select_all:
614 selectAllFiles();
615 return true;
Ben Kwa3bcc94882015-03-31 08:15:21 -0700616
Aga Wronska3b327ef2016-01-20 16:32:33 -0800617 case R.id.menu_rename:
618 renameDocuments(selection);
619 mode.finish();
620 return true;
621
Steve McKayc2b4b3d2015-12-01 17:02:42 -0800622 default:
623 if (DEBUG) Log.d(TAG, "Unhandled menu item selected: " + item);
624 return false;
Jeff Sharkeybe8b12e2013-07-01 16:56:54 -0700625 }
626 }
Steve McKay4b3a13c2015-06-11 10:10:49 -0700627 }
Jeff Sharkeybe8b12e2013-07-01 16:56:54 -0700628
Steve McKaycbee5442016-01-28 15:30:10 -0800629 public final boolean onBackPressed() {
630 if (mSelectionManager.hasSelection()) {
631 if (DEBUG) Log.d(TAG, "Clearing selection on back pressed.");
632 mSelectionManager.clearSelection();
633 return true;
634 }
635 return false;
636 }
637
Ben Kwad8391492015-12-17 10:37:00 -0800638 private void cancelThumbnailTask(View view) {
Steve McKay4b3a13c2015-06-11 10:10:49 -0700639 final ImageView iconThumb = (ImageView) view.findViewById(R.id.icon_thumb);
640 if (iconThumb != null) {
Ben Kwad8391492015-12-17 10:37:00 -0800641 mIconHelper.stopLoading(iconThumb);
Jeff Sharkey4ec97392013-09-10 12:04:26 -0700642 }
Steve McKay4b3a13c2015-06-11 10:10:49 -0700643 }
Jeff Sharkey4ec97392013-09-10 12:04:26 -0700644
Steve McKay4b3a13c2015-06-11 10:10:49 -0700645 private void openDocuments(final Selection selected) {
Steve McKayab7865c2015-05-27 16:11:42 -0700646 new GetDocumentsTask() {
647 @Override
648 void onDocumentsReady(List<DocumentInfo> docs) {
Steve McKay12055472015-08-20 16:48:49 -0700649 // TODO: Implement support in Files activity for opening multiple docs.
Steve McKayab7865c2015-05-27 16:11:42 -0700650 BaseActivity.get(DirectoryFragment.this).onDocumentsPicked(docs);
Ben Kwa726cf702015-04-08 15:03:35 -0700651 }
Steve McKayab7865c2015-05-27 16:11:42 -0700652 }.execute(selected);
Jeff Sharkey4eb407a2013-08-18 17:38:20 -0700653 }
654
Steve McKay4b3a13c2015-06-11 10:10:49 -0700655 private void shareDocuments(final Selection selected) {
Steve McKayab7865c2015-05-27 16:11:42 -0700656 new GetDocumentsTask() {
657 @Override
658 void onDocumentsReady(List<DocumentInfo> docs) {
659 Intent intent;
660
661 // Filter out directories - those can't be shared.
Steve McKay58efce32015-08-20 16:19:38 +0000662 List<DocumentInfo> docsForSend = new ArrayList<>();
Steve McKayab7865c2015-05-27 16:11:42 -0700663 for (DocumentInfo doc: docs) {
664 if (!Document.MIME_TYPE_DIR.equals(doc.mimeType)) {
665 docsForSend.add(doc);
666 }
667 }
668
669 if (docsForSend.size() == 1) {
670 final DocumentInfo doc = docsForSend.get(0);
671
672 intent = new Intent(Intent.ACTION_SEND);
673 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
674 intent.addCategory(Intent.CATEGORY_DEFAULT);
675 intent.setType(doc.mimeType);
676 intent.putExtra(Intent.EXTRA_STREAM, doc.derivedUri);
677
678 } else if (docsForSend.size() > 1) {
679 intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
680 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
681 intent.addCategory(Intent.CATEGORY_DEFAULT);
682
Steve McKay58efce32015-08-20 16:19:38 +0000683 final ArrayList<String> mimeTypes = new ArrayList<>();
684 final ArrayList<Uri> uris = new ArrayList<>();
Steve McKayab7865c2015-05-27 16:11:42 -0700685 for (DocumentInfo doc : docsForSend) {
686 mimeTypes.add(doc.mimeType);
687 uris.add(doc.derivedUri);
688 }
689
690 intent.setType(findCommonMimeType(mimeTypes));
691 intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
692
693 } else {
694 return;
695 }
696
697 intent = Intent.createChooser(intent, getActivity().getText(R.string.share_via));
698 startActivity(intent);
699 }
700 }.execute(selected);
701 }
702
Steve McKay4b3a13c2015-06-11 10:10:49 -0700703 private void deleteDocuments(final Selection selected) {
Steve McKaya1f76802016-02-25 13:34:03 -0800704 assert(!selected.isEmpty());
Jeff Sharkey4eb407a2013-08-18 17:38:20 -0700705
Tomasz Mikolajewskib8436af2016-01-25 16:20:15 +0900706 final DocumentInfo srcParent = getDisplayState().stack.peek();
707 new GetDocumentsTask() {
708 @Override
Steve McKay7a3b8112016-02-23 10:06:50 -0800709 void onDocumentsReady(final List<DocumentInfo> docs) {
Steve McKayc8d4e222016-03-01 08:28:02 -0800710
711 TextView message =
712 (TextView) mInflater.inflate(R.layout.dialog_delete_confirmation, null);
713 message.setText(
714 Shared.getQuantityString(
715 getActivity(),
716 R.plurals.delete_confirmation_message,
717 docs.size()));
718
719 // This "insta-hides" files that are being deleted, because
720 // the delete operation may be not execute immediately (it
721 // may be queued up on the FileOperationService.)
722 // To hide the files locally, we call the hide method on the adapter
723 // ...which a live object...cannot be parceled.
724 // For that reason, for now, we implement this dialog NOT
725 // as a fragment (which can survive rotation and have its own state),
726 // but as a simple runtime dialog. So rotating a device with an
727 // active delete dialog...results in that dialog disappearing.
728 // We can do better, but don't have cycles for it now.
Steve McKay7a3b8112016-02-23 10:06:50 -0800729 new AlertDialog.Builder(getActivity())
Steve McKayc8d4e222016-03-01 08:28:02 -0800730 .setView(message)
Steve McKay7a3b8112016-02-23 10:06:50 -0800731 .setPositiveButton(
732 android.R.string.yes,
733 new DialogInterface.OnClickListener() {
734 public void onClick(DialogInterface dialog, int id) {
735 // Hide the files in the UI.
736 mAdapter.hide(selected.getAll());
737 FileOperations.delete(
738 getActivity(), docs, srcParent, getDisplayState().stack);
Ben Kwa304895a2015-08-27 16:06:33 -0700739 }
740 })
Steve McKay7a3b8112016-02-23 10:06:50 -0800741 .setNegativeButton(android.R.string.no, null)
742 .show();
743 }
744 }.execute(selected);
Jeff Sharkey4eb407a2013-08-18 17:38:20 -0700745 }
746
Ben Kwa1e2fa5e2016-02-02 23:00:02 -0800747 private void transferDocuments(final Selection selected, final @OpType int mode) {
Ben Kwaef3f2622015-04-07 15:43:39 -0700748 // Pop up a dialog to pick a destination. This is inadequate but works for now.
749 // TODO: Implement a picker that is to spec.
Daichi Hironobbe22922015-04-10 15:50:38 +0900750 final Intent intent = new Intent(
Ben Kwa84cebbe2015-09-25 14:48:29 -0700751 Shared.ACTION_PICK_COPY_DESTINATION,
Daichi Hironobbe22922015-04-10 15:50:38 +0900752 Uri.EMPTY,
753 getActivity(),
754 DocumentsActivity.class);
Steve McKayab7865c2015-05-27 16:11:42 -0700755
Steve McKaya6bbeab2016-02-17 15:02:01 -0800756 // Set an appropriate title on the drawer when it is shown in the picker.
757 // Coupled with the fact that we auto-open the drawer for copy/move operations
758 // it should basically be the thing people see first.
759 int drawerTitleId = mode == FileOperationService.OPERATION_MOVE
760 ? R.string.menu_move : R.string.menu_copy;
761 intent.putExtra(DocumentsContract.EXTRA_PROMPT, getResources().getString(drawerTitleId));
762
Steve McKayab7865c2015-05-27 16:11:42 -0700763 new GetDocumentsTask() {
764 @Override
765 void onDocumentsReady(List<DocumentInfo> docs) {
Steve McKaya6bbeab2016-02-17 15:02:01 -0800766 // TODO: Can this move to Fragment bundle state?
Steve McKayf69502c2015-11-09 17:38:35 +0900767 getDisplayState().selectedDocumentsForCopy = docs;
Steve McKayab7865c2015-05-27 16:11:42 -0700768
Steve McKaya6bbeab2016-02-17 15:02:01 -0800769 // Determine if there is a directory in the set of documents
770 // to be copied? Why? Directory creation isn't supported by some roots
771 // (like Downloads). This informs DocumentsActivity (the "picker")
772 // to restrict available roots to just those with support.
773 intent.putExtra(Shared.EXTRA_DIRECTORY_COPY, hasDirectory(docs));
Steve McKay14e827a2016-01-06 18:32:13 -0800774 intent.putExtra(FileOperationService.EXTRA_OPERATION, mode);
Steve McKaya6bbeab2016-02-17 15:02:01 -0800775
776 // This just identifies the type of request...we'll check it
777 // when we reveive a response.
Steve McKayab7865c2015-05-27 16:11:42 -0700778 startActivityForResult(intent, REQUEST_COPY_DESTINATION);
Daichi Hirono9be34292015-04-14 17:12:54 +0900779 }
Steve McKaya6bbeab2016-02-17 15:02:01 -0800780
Steve McKayab7865c2015-05-27 16:11:42 -0700781 }.execute(selected);
Ben Kwad99109f2015-03-31 10:11:43 -0700782 }
783
Steve McKaya6bbeab2016-02-17 15:02:01 -0800784 private static boolean hasDirectory(List<DocumentInfo> docs) {
785 for (DocumentInfo info : docs) {
786 if (Document.MIME_TYPE_DIR.equals(info.mimeType)) {
787 return true;
788 }
789 }
790 return false;
791 }
792
Aga Wronska3b327ef2016-01-20 16:32:33 -0800793 private void renameDocuments(Selection selected) {
794 // Batch renaming not supported
795 // Rename option is only available in menu when 1 document selected
Steve McKaya1f76802016-02-25 13:34:03 -0800796 assert(selected.size() == 1);
Aga Wronska3b327ef2016-01-20 16:32:33 -0800797
798 new GetDocumentsTask() {
799 @Override
800 void onDocumentsReady(List<DocumentInfo> docs) {
801 RenameDocumentFragment.show(getFragmentManager(), docs.get(0));
802 }
803 }.execute(selected);
804 }
805
Steve McKayef16f5f2015-12-22 18:15:31 -0800806 @Override
807 public void initDocumentHolder(DocumentHolder holder) {
Ben Kwa0436a752016-01-15 10:43:24 -0800808 holder.addEventListener(mItemEventListener);
Ben Kwa2036dad2016-02-10 07:46:35 -0800809 holder.itemView.setOnFocusChangeListener(mFocusManager);
Steve McKayef16f5f2015-12-22 18:15:31 -0800810 }
811
812 @Override
813 public void onBindDocumentHolder(DocumentHolder holder, Cursor cursor) {
Ben Kwa990489d2016-02-25 14:10:40 -0800814 setupDragAndDropOnDocumentView(holder.itemView, cursor);
Steve McKayef16f5f2015-12-22 18:15:31 -0800815 }
816
817 @Override
818 public State getDisplayState() {
Steve McKayf69502c2015-11-09 17:38:35 +0900819 return ((BaseActivity) getActivity()).getDisplayState();
Jeff Sharkey54e55b72013-06-30 20:02:59 -0700820 }
821
Steve McKayef16f5f2015-12-22 18:15:31 -0800822 @Override
823 public Model getModel() {
824 return mModel;
825 }
826
827 @Override
828 public boolean isDocumentEnabled(String docMimeType, int docFlags) {
829 return mTuner.isDocumentEnabled(docMimeType, docFlags);
830 }
831
Steve McKay9ed88a42016-01-21 18:46:15 -0800832 private void showEmptyDirectory() {
Ben Kwa0e9aae42016-02-04 16:35:27 -0800833 showEmptyView(R.string.empty, R.drawable.cabinet);
Ben Kwa91bec532015-09-16 13:15:38 -0700834 }
835
Steve McKay9ed88a42016-01-21 18:46:15 -0800836 private void showNoResults(RootInfo root) {
837 CharSequence msg = getContext().getResources().getText(R.string.no_results);
Ben Kwa0e9aae42016-02-04 16:35:27 -0800838 showEmptyView(String.format(String.valueOf(msg), root.title), R.drawable.cabinet);
Ben Kwa91bec532015-09-16 13:15:38 -0700839 }
840
Steve McKay9ed88a42016-01-21 18:46:15 -0800841 private void showQueryError() {
Ben Kwa0e9aae42016-02-04 16:35:27 -0800842 showEmptyView(R.string.query_error, R.drawable.hourglass);
Steve McKay9ed88a42016-01-21 18:46:15 -0800843 }
844
Ben Kwa0e9aae42016-02-04 16:35:27 -0800845 private void showEmptyView(@StringRes int id, int drawable) {
846 showEmptyView(getContext().getResources().getText(id), drawable);
Steve McKay9ed88a42016-01-21 18:46:15 -0800847 }
848
Ben Kwa0e9aae42016-02-04 16:35:27 -0800849 private void showEmptyView(CharSequence msg, int drawable) {
Steve McKay9ed88a42016-01-21 18:46:15 -0800850 View content = mEmptyView.findViewById(R.id.content);
851 TextView msgView = (TextView) mEmptyView.findViewById(R.id.message);
Ben Kwa0e9aae42016-02-04 16:35:27 -0800852 ImageView imageView = (ImageView) mEmptyView.findViewById(R.id.artwork);
Steve McKay9ed88a42016-01-21 18:46:15 -0800853 msgView.setText(msg);
Ben Kwa0e9aae42016-02-04 16:35:27 -0800854 imageView.setImageResource(drawable);
Steve McKay9ed88a42016-01-21 18:46:15 -0800855
Steve McKay9ed88a42016-01-21 18:46:15 -0800856 mEmptyView.setVisibility(View.VISIBLE);
Aga Wronskad4b17532016-02-12 09:15:32 -0800857 mEmptyView.requestFocus();
Steve McKay9ed88a42016-01-21 18:46:15 -0800858 mRecView.setVisibility(View.GONE);
Steve McKay9ed88a42016-01-21 18:46:15 -0800859 }
860
861 private void showDirectory() {
Ben Kwa91bec532015-09-16 13:15:38 -0700862 mEmptyView.setVisibility(View.GONE);
863 mRecView.setVisibility(View.VISIBLE);
Aga Wronskad4b17532016-02-12 09:15:32 -0800864 mRecView.requestFocus();
Ben Kwa91bec532015-09-16 13:15:38 -0700865 }
866
Jeff Sharkeyb5133112013-09-01 18:41:04 -0700867 private String findCommonMimeType(List<String> mimeTypes) {
868 String[] commonType = mimeTypes.get(0).split("/");
869 if (commonType.length != 2) {
870 return "*/*";
871 }
872
873 for (int i = 1; i < mimeTypes.size(); i++) {
874 String[] type = mimeTypes.get(i).split("/");
875 if (type.length != 2) continue;
876
877 if (!commonType[1].equals(type[1])) {
878 commonType[1] = "*";
879 }
880
881 if (!commonType[0].equals(type[0])) {
882 commonType[0] = "*";
883 commonType[1] = "*";
884 break;
885 }
886 }
887
888 return commonType[0] + "/" + commonType[1];
889 }
Jeff Sharkey3f4c2052013-09-09 16:51:06 -0700890
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700891 private void copyFromClipboard() {
892 new AsyncTask<Void, Void, List<DocumentInfo>>() {
893
894 @Override
895 protected List<DocumentInfo> doInBackground(Void... params) {
896 return mClipper.getClippedDocuments();
897 }
898
899 @Override
900 protected void onPostExecute(List<DocumentInfo> docs) {
901 DocumentInfo destination =
902 ((BaseActivity) getActivity()).getCurrentDirectory();
903 copyDocuments(docs, destination);
904 }
905 }.execute();
Steve McKay3da8afc2015-05-05 14:50:00 -0700906 }
907
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700908 private void copyFromClipData(final ClipData clipData, final DocumentInfo destination) {
Steve McKaya1f76802016-02-25 13:34:03 -0800909 assert(clipData != null);
910
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700911 new AsyncTask<Void, Void, List<DocumentInfo>>() {
Vladislav Kaznacheev335dba52015-05-01 13:46:57 -0700912
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700913 @Override
914 protected List<DocumentInfo> doInBackground(Void... params) {
915 return mClipper.getDocumentsFromClipData(clipData);
916 }
917
918 @Override
919 protected void onPostExecute(List<DocumentInfo> docs) {
920 copyDocuments(docs, destination);
921 }
922 }.execute();
923 }
924
925 private void copyDocuments(final List<DocumentInfo> docs, final DocumentInfo destination) {
Steve McKay4a1ca862016-02-17 18:25:47 -0800926 BaseActivity activity = (BaseActivity) getActivity();
927 if (!canCopy(docs, activity.getCurrentRoot(), destination)) {
Steve McKay5bbae102015-10-01 11:39:24 -0700928 Snackbars.makeSnackbar(
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700929 getActivity(),
Ben Kwa94b486d2015-09-30 10:00:10 -0700930 R.string.clipboard_files_cannot_paste,
931 Snackbar.LENGTH_SHORT)
932 .show();
Steve McKay3da8afc2015-05-05 14:50:00 -0700933 return;
934 }
935
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700936 if (docs.isEmpty()) {
Vladislav Kaznacheev335dba52015-05-01 13:46:57 -0700937 return;
Vladislav Kaznacheevd69d3c42015-05-05 12:09:47 -0700938 }
Vladislav Kaznacheev335dba52015-05-01 13:46:57 -0700939
Steve McKayf69502c2015-11-09 17:38:35 +0900940 final DocumentStack curStack = getDisplayState().stack;
Vladislav Kaznacheev335dba52015-05-01 13:46:57 -0700941 DocumentStack tmpStack = new DocumentStack();
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700942 if (destination != null) {
943 tmpStack.push(destination);
Vladislav Kaznacheev335dba52015-05-01 13:46:57 -0700944 tmpStack.addAll(curStack);
945 } else {
946 tmpStack = curStack;
947 }
948
Steve McKay14e827a2016-01-06 18:32:13 -0800949 FileOperations.copy(getActivity(), docs, tmpStack);
Vladislav Kaznacheev335dba52015-05-01 13:46:57 -0700950 }
951
Steve McKayf8621552015-11-03 15:23:16 -0800952 public void copySelectedToClipboard() {
Steve McKayd28c5c32015-12-07 16:31:42 -0800953 Selection selection = mSelectionManager.getSelection(new Selection());
954 if (!selection.isEmpty()) {
955 copySelectionToClipboard(selection);
Steve McKaycbee5442016-01-28 15:30:10 -0800956 mSelectionManager.clearSelection();
Steve McKayd28c5c32015-12-07 16:31:42 -0800957 }
Steve McKayab7865c2015-05-27 16:11:42 -0700958 }
Steve McKay3da8afc2015-05-05 14:50:00 -0700959
Steve McKayd28c5c32015-12-07 16:31:42 -0800960 void copySelectionToClipboard(Selection selection) {
Steve McKaya1f76802016-02-25 13:34:03 -0800961 assert(!selection.isEmpty());
Steve McKayab7865c2015-05-27 16:11:42 -0700962 new GetDocumentsTask() {
963 @Override
964 void onDocumentsReady(List<DocumentInfo> docs) {
965 mClipper.clipDocuments(docs);
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700966 Activity activity = getActivity();
Steve McKay5bbae102015-10-01 11:39:24 -0700967 Snackbars.makeSnackbar(activity,
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700968 activity.getResources().getQuantityString(
969 R.plurals.clipboard_files_clipped, docs.size(), docs.size()),
Ben Kwa862b96412015-12-07 13:25:27 -0800970 Snackbar.LENGTH_SHORT).show();
Steve McKayab7865c2015-05-27 16:11:42 -0700971 }
Steve McKayd28c5c32015-12-07 16:31:42 -0800972 }.execute(selection);
Steve McKay3da8afc2015-05-05 14:50:00 -0700973 }
974
Steve McKayf8621552015-11-03 15:23:16 -0800975 public void pasteFromClipboard() {
Steve McKaybdbd0ff2015-05-20 15:58:42 -0700976 copyFromClipboard();
977 getActivity().invalidateOptionsMenu();
Steve McKay3da8afc2015-05-05 14:50:00 -0700978 }
979
Steve McKay3da8afc2015-05-05 14:50:00 -0700980 /**
981 * Returns true if the list of files can be copied to destination. Note that this
982 * is a policy check only. Currently the method does not attempt to verify
983 * available space or any other environmental aspects possibly resulting in
984 * failure to copy.
985 *
986 * @return true if the list of files can be copied to destination.
987 */
Steve McKay4a1ca862016-02-17 18:25:47 -0800988 private boolean canCopy(List<DocumentInfo> files, RootInfo root, DocumentInfo dest) {
989 if (dest == null || !dest.isDirectory() || !dest.isCreateSupported()) {
990 return false;
991 }
Steve McKay3da8afc2015-05-05 14:50:00 -0700992
Steve McKay008e9482016-02-18 15:32:16 -0800993 // Can't copy folders to downloads, because we don't show folders there.
994 if (!root.isDownloads()) {
Steve McKay3da8afc2015-05-05 14:50:00 -0700995 for (DocumentInfo docs : files) {
996 if (docs.isDirectory()) {
997 return false;
998 }
999 }
1000 }
1001
Steve McKay4a1ca862016-02-17 18:25:47 -08001002 return true;
Steve McKay3da8afc2015-05-05 14:50:00 -07001003 }
1004
Steve McKayf8621552015-11-03 15:23:16 -08001005 public void selectAllFiles() {
Ben Kwa862b96412015-12-07 13:25:27 -08001006 // Only select things currently visible in the adapter.
1007 boolean changed = mSelectionManager.setItemsSelected(mAdapter.getModelIds(), true);
Steve McKayb04b1642015-07-24 13:14:20 -07001008 if (changed) {
1009 updateDisplayState();
1010 }
Steve McKay3da8afc2015-05-05 14:50:00 -07001011 }
1012
Ben Kwa2036dad2016-02-10 07:46:35 -08001013 /**
1014 * Attempts to restore focus on the directory listing.
1015 */
1016 public void requestFocus() {
1017 mFocusManager.restoreLastFocus();
1018 }
1019
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001020 private void setupDragAndDropOnDocumentView(View view, Cursor cursor) {
1021 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
1022 if (Document.MIME_TYPE_DIR.equals(docMimeType)) {
1023 // Make a directory item a drop target. Drop on non-directories and empty space
1024 // is handled at the list/grid view level.
1025 view.setOnDragListener(mOnDragListener);
1026 }
1027
Ben Kwa990489d2016-02-25 14:10:40 -08001028 // Make all items draggable.
Ben Kwae3dfebf2016-02-18 16:45:45 -08001029 view.setOnLongClickListener(mDragHelper);
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001030 }
1031
1032 private View.OnDragListener mOnDragListener = new View.OnDragListener() {
1033 @Override
1034 public boolean onDrag(View v, DragEvent event) {
1035 switch (event.getAction()) {
1036 case DragEvent.ACTION_DRAG_STARTED:
1037 // TODO: Check if the event contains droppable data.
1038 return true;
1039
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001040 // TODO: Expand drop target directory on hover?
1041 case DragEvent.ACTION_DRAG_ENTERED:
Ben Kwa1b685e52016-02-24 10:05:53 -08001042 setDropTargetHighlight(v, true);
Ben Kwae3dfebf2016-02-18 16:45:45 -08001043 return true;
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001044 case DragEvent.ACTION_DRAG_EXITED:
Ben Kwa1b685e52016-02-24 10:05:53 -08001045 setDropTargetHighlight(v, false);
Ben Kwae3dfebf2016-02-18 16:45:45 -08001046 return true;
1047
1048 case DragEvent.ACTION_DRAG_LOCATION:
Ben Kwa990489d2016-02-25 14:10:40 -08001049 return true;
1050
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001051 case DragEvent.ACTION_DRAG_ENDED:
Ben Kwa990489d2016-02-25 14:10:40 -08001052 if (event.getResult()) {
1053 // Exit selection mode if the drop was handled.
1054 mSelectionManager.clearSelection();
1055 }
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001056 return true;
1057
1058 case DragEvent.ACTION_DROP:
Ben Kwa990489d2016-02-25 14:10:40 -08001059 // After a drop event, always stop highlighting the target.
Ben Kwa1b685e52016-02-24 10:05:53 -08001060 setDropTargetHighlight(v, false);
Ben Kwa990489d2016-02-25 14:10:40 -08001061 // Don't copy from the cwd into the cwd. Note: this currently doesn't work for
1062 // multi-window drag, because localState isn't carried over from one process to
1063 // another.
1064 Object src = event.getLocalState();
1065 DocumentInfo dst = getDestination(v);
1066 if (Objects.equals(src, dst)) {
1067 return false;
1068 }
1069 copyFromClipData(event.getClipData(), dst);
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001070 return true;
1071 }
1072 return false;
1073 }
Ben Kwae3dfebf2016-02-18 16:45:45 -08001074
Ben Kwa990489d2016-02-25 14:10:40 -08001075 private DocumentInfo getDestination(View v) {
1076 String id = getModelId(v);
1077 if (id != null) {
1078 Cursor dstCursor = mModel.getItem(id);
Steve McKaya1f76802016-02-25 13:34:03 -08001079 assert(dstCursor != null);
Ben Kwa990489d2016-02-25 14:10:40 -08001080 return DocumentInfo.fromDirectoryCursor(dstCursor);
1081 }
1082
1083 if (v == mRecView || v == mEmptyView) {
1084 return getDisplayState().stack.peek();
1085 }
1086
1087 return null;
1088 }
1089
Ben Kwa1b685e52016-02-24 10:05:53 -08001090 private void setDropTargetHighlight(View v, boolean highlight) {
Ben Kwae3dfebf2016-02-18 16:45:45 -08001091 // Note: use exact comparison - this code is searching for views which are children of
1092 // the RecyclerView instance in the UI.
1093 if (v.getParent() == mRecView) {
1094 RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(v);
1095 if (vh instanceof DocumentHolder) {
1096 ((DocumentHolder) vh).setHighlighted(highlight);
1097 }
1098 }
1099 }
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001100 };
1101
Ben Kwa003a9992015-11-30 23:00:02 -08001102 /**
1103 * Gets the model ID for a given motion event (using the event position)
1104 */
1105 private String getModelId(MotionEvent e) {
1106 View view = mRecView.findChildViewUnder(e.getX(), e.getY());
1107 if (view == null) {
1108 return null;
1109 }
1110 RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(view);
1111 if (vh instanceof DocumentHolder) {
1112 return ((DocumentHolder) vh).modelId;
1113 } else {
1114 return null;
1115 }
1116 }
1117
1118 /**
1119 * Gets the model ID for a given RecyclerView item.
1120 * @param view A View that is a document item view, or a child of a document item view.
1121 * @return The Model ID for the given document, or null if the given view is not associated with
1122 * a document item view.
1123 */
1124 private String getModelId(View view) {
Ben Kwae3dfebf2016-02-18 16:45:45 -08001125 View itemView = mRecView.findContainingItemView(view);
1126 if (itemView != null) {
1127 RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(itemView);
1128 if (vh instanceof DocumentHolder) {
1129 return ((DocumentHolder) vh).modelId;
Vladislav Kaznacheevb5999ef2015-09-04 09:17:37 -07001130 }
Vladislav Kaznacheevb5999ef2015-09-04 09:17:37 -07001131 }
Ben Kwae3dfebf2016-02-18 16:45:45 -08001132 return null;
Vladislav Kaznacheevb5999ef2015-09-04 09:17:37 -07001133 }
1134
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001135 private List<DocumentInfo> getDraggableDocuments(View currentItemView) {
Ben Kwa003a9992015-11-30 23:00:02 -08001136 String modelId = getModelId(currentItemView);
1137 if (modelId == null) {
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001138 return Collections.EMPTY_LIST;
1139 }
1140
Ben Kwa8250db42015-10-07 14:15:12 -07001141 final List<DocumentInfo> selectedDocs =
1142 mModel.getDocuments(mSelectionManager.getSelection());
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001143 if (!selectedDocs.isEmpty()) {
Ben Kwa003a9992015-11-30 23:00:02 -08001144 if (!isSelected(modelId)) {
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001145 // There is a selection that does not include the current item, drag nothing.
1146 return Collections.EMPTY_LIST;
1147 }
1148 return selectedDocs;
1149 }
1150
Ben Kwa003a9992015-11-30 23:00:02 -08001151 final Cursor cursor = mModel.getItem(modelId);
Steve McKay58efce32015-08-20 16:19:38 +00001152
Steve McKaya1f76802016-02-25 13:34:03 -08001153 assert(cursor != null);
1154
1155 return Lists.newArrayList(
1156 DocumentInfo.fromDirectoryCursor(cursor));
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001157 }
1158
1159 private Drawable getDragShadowIcon(List<DocumentInfo> docs) {
1160 if (docs.size() == 1) {
1161 final DocumentInfo doc = docs.get(0);
Ben Kwad8391492015-12-17 10:37:00 -08001162 return mIconHelper.getDocumentIcon(getActivity(), doc.authority, doc.documentId,
1163 doc.mimeType, doc.icon);
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001164 }
1165 return getActivity().getDrawable(R.drawable.ic_doc_generic);
1166 }
1167
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001168 private class DrawableShadowBuilder extends View.DragShadowBuilder {
1169
1170 private final Drawable mShadow;
1171
1172 private final int mShadowDimension;
1173
1174 public DrawableShadowBuilder(Drawable shadow) {
1175 mShadow = shadow;
1176 mShadowDimension = getResources().getDimensionPixelSize(
1177 R.dimen.drag_shadow_size);
1178 mShadow.setBounds(0, 0, mShadowDimension, mShadowDimension);
1179 }
1180
Ben Kwac64cb252015-08-27 16:04:46 -07001181 @Override
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001182 public void onProvideShadowMetrics(
1183 Point shadowSize, Point shadowTouchPoint) {
1184 shadowSize.set(mShadowDimension, mShadowDimension);
1185 shadowTouchPoint.set(mShadowDimension / 2, mShadowDimension / 2);
1186 }
1187
Ben Kwac64cb252015-08-27 16:04:46 -07001188 @Override
Vladislav Kaznacheev42a6bbc2015-05-01 14:18:57 -07001189 public void onDrawShadow(Canvas canvas) {
1190 mShadow.draw(canvas);
1191 }
1192 }
Steve McKaybdbd0ff2015-05-20 15:58:42 -07001193
Steve McKaybdbd0ff2015-05-20 15:58:42 -07001194 /**
Steve McKayab7865c2015-05-27 16:11:42 -07001195 * Abstract task providing support for loading documents *off*
1196 * the main thread. And if it isn't obvious, creating a list
1197 * of documents (especially large lists) can be pretty expensive.
1198 */
1199 private abstract class GetDocumentsTask
Steve McKay4b3a13c2015-06-11 10:10:49 -07001200 extends AsyncTask<Selection, Void, List<DocumentInfo>> {
Steve McKayab7865c2015-05-27 16:11:42 -07001201 @Override
Steve McKay4b3a13c2015-06-11 10:10:49 -07001202 protected final List<DocumentInfo> doInBackground(Selection... selected) {
Ben Kwac64cb252015-08-27 16:04:46 -07001203 return mModel.getDocuments(selected[0]);
Steve McKayab7865c2015-05-27 16:11:42 -07001204 }
1205
1206 @Override
1207 protected final void onPostExecute(List<DocumentInfo> docs) {
1208 onDocumentsReady(docs);
1209 }
1210
1211 abstract void onDocumentsReady(List<DocumentInfo> docs);
1212 }
1213
Steve McKayef16f5f2015-12-22 18:15:31 -08001214 @Override
1215 public boolean isSelected(String modelId) {
Ben Kwa743c7c22015-12-01 19:56:57 -08001216 return mSelectionManager.getSelection().contains(modelId);
Ben Kwa91caed82015-09-21 10:49:52 -07001217 }
Ben Kwa18fce3c2015-09-01 11:03:01 -07001218
Ben Kwa0436a752016-01-15 10:43:24 -08001219 private class ItemEventListener implements DocumentHolder.EventListener {
Ben Kwabd964562015-10-14 08:00:27 -07001220 @Override
Ben Kwa0436a752016-01-15 10:43:24 -08001221 public boolean onActivate(DocumentHolder doc) {
Ben Kwa67924892016-01-27 09:58:36 -08001222 // Toggle selection if we're in selection mode, othewise, view item.
1223 if (mSelectionManager.hasSelection()) {
1224 mSelectionManager.toggleSelection(doc.modelId);
1225 } else {
1226 handleViewItem(doc.modelId);
1227 }
Ben Kwa0436a752016-01-15 10:43:24 -08001228 return true;
1229 }
1230
1231 @Override
1232 public boolean onSelect(DocumentHolder doc) {
1233 mSelectionManager.toggleSelection(doc.modelId);
1234 mSelectionManager.setSelectionRangeBegin(doc.getAdapterPosition());
1235 return true;
Ben Kwabd964562015-10-14 08:00:27 -07001236 }
Ben Kwa67924892016-01-27 09:58:36 -08001237
1238 @Override
1239 public boolean onKey(DocumentHolder doc, int keyCode, KeyEvent event) {
1240 // Only handle key-down events. This is simpler, consistent with most other UIs, and
1241 // enables the handling of repeated key events from holding down a key.
1242 if (event.getAction() != KeyEvent.ACTION_DOWN) {
1243 return false;
1244 }
1245
Ben Kwa22937c92016-02-23 23:00:01 -08001246 // Ignore tab key events. Those should be handled by the top-level key handler.
1247 if (keyCode == KeyEvent.KEYCODE_TAB) {
1248 return false;
1249 }
1250
Ben Kwab0bfe2d2016-02-09 11:27:45 -08001251 if (mFocusManager.handleKey(doc, keyCode, event)) {
Ben Kwa83df50f2016-02-10 14:01:19 -08001252 // Handle range selection adjustments. Extending the selection will adjust the
1253 // bounds of the in-progress range selection. Each time an unshifted navigation
1254 // event is received, the range selection is restarted.
1255 if (shouldExtendSelection(event)) {
1256 if (!mSelectionManager.isRangeSelectionActive()) {
1257 // Start a range selection if one isn't active
1258 mSelectionManager.startRangeSelection(doc.getAdapterPosition());
1259 }
1260 mSelectionManager.snapRangeSelection(mFocusManager.getFocusPosition());
1261 } else {
1262 mSelectionManager.endRangeSelection();
1263 }
Ben Kwab0bfe2d2016-02-09 11:27:45 -08001264 return true;
Ben Kwa67924892016-01-27 09:58:36 -08001265 }
1266
Ben Kwab0bfe2d2016-02-09 11:27:45 -08001267 // Handle enter key events
1268 if (keyCode == KeyEvent.KEYCODE_ENTER) {
Ben Kwa09eb77b2016-02-10 08:57:25 -08001269 if (event.isShiftPressed()) {
1270 return onSelect(doc);
1271 } else {
1272 return onActivate(doc);
1273 }
Ben Kwab0bfe2d2016-02-09 11:27:45 -08001274 }
1275
1276 return false;
Ben Kwa67924892016-01-27 09:58:36 -08001277 }
Ben Kwa83df50f2016-02-10 14:01:19 -08001278
1279 private boolean shouldExtendSelection(KeyEvent event) {
1280 return Events.isNavigationKeyCode(event.getKeyCode()) &&
1281 event.isShiftPressed();
1282 }
Ben Kwabd964562015-10-14 08:00:27 -07001283 }
1284
Steve McKayef16f5f2015-12-22 18:15:31 -08001285 private final class ModelUpdateListener implements Model.UpdateListener {
Ben Kwa91caed82015-09-21 10:49:52 -07001286 @Override
1287 public void onModelUpdate(Model model) {
1288 if (model.info != null || model.error != null) {
1289 mMessageBar.setInfo(model.info);
1290 mMessageBar.setError(model.error);
1291 mMessageBar.show();
1292 }
Ben Kwa18fce3c2015-09-01 11:03:01 -07001293
Ben Kwa91caed82015-09-21 10:49:52 -07001294 mProgressBar.setVisibility(model.isLoading() ? View.VISIBLE : View.GONE);
1295
1296 if (model.isEmpty()) {
Aga Wronska893390b2016-02-17 13:50:42 -08001297 if (mSearchMode) {
Steve McKay9ed88a42016-01-21 18:46:15 -08001298 showNoResults(getDisplayState().stack.root);
1299 } else {
1300 showEmptyDirectory();
1301 }
Ben Kwa91caed82015-09-21 10:49:52 -07001302 } else {
Steve McKay9ed88a42016-01-21 18:46:15 -08001303 showDirectory();
Ben Kwa91bec532015-09-16 13:15:38 -07001304 mAdapter.notifyDataSetChanged();
Ben Kwa91caed82015-09-21 10:49:52 -07001305 }
Tomasz Mikolajewskiae6d6b42016-02-24 12:53:44 +09001306
1307 if (!model.isLoading()) {
1308 ((BaseActivity) getActivity()).notifyDirectoryLoaded(
1309 model.doc != null ? model.doc.derivedUri : null);
1310 }
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 }
Ben Kwa990489d2016-02-25 14:10:40 -08001327 v.startDragAndDrop(
1328 mClipper.getClipDataForDocuments(docs),
Steve McKay41585d52016-01-21 15:10:39 -08001329 new DrawableShadowBuilder(getDragShadowIcon(docs)),
Ben Kwa990489d2016-02-25 14:10:40 -08001330 getDisplayState().stack.peek(),
Steve McKay41585d52016-01-21 15:10:39 -08001331 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);
Aga Wronskad5597432016-02-26 11:36:07 -08001482 args.putParcelable(Shared.EXTRA_SELECTION, new Selection());
Steve McKay9ed88a42016-01-21 18:46:15 -08001483
1484 final FragmentTransaction ft = fm.beginTransaction();
1485 switch (anim) {
1486 case ANIM_SIDE:
Aga Wronska893390b2016-02-17 13:50:42 -08001487 args.putBoolean(Shared.EXTRA_IGNORE_STATE, true);
Steve McKay9ed88a42016-01-21 18:46:15 -08001488 break;
Tomasz Mikolajewskie3fe9d72016-02-03 16:53:21 +09001489 case ANIM_ENTER:
Aga Wronska893390b2016-02-17 13:50:42 -08001490 args.putBoolean(Shared.EXTRA_IGNORE_STATE, true);
Tomasz Mikolajewskie3fe9d72016-02-03 16:53:21 +09001491 ft.setCustomAnimations(R.animator.dir_enter, R.animator.dir_frozen);
Steve McKay9ed88a42016-01-21 18:46:15 -08001492 break;
Tomasz Mikolajewskie3fe9d72016-02-03 16:53:21 +09001493 case ANIM_LEAVE:
1494 ft.setCustomAnimations(R.animator.dir_frozen, R.animator.dir_leave);
Steve McKay9ed88a42016-01-21 18:46:15 -08001495 break;
1496 }
1497
1498 final DirectoryFragment fragment = new DirectoryFragment();
1499 fragment.setArguments(args);
1500
Aga Wronska893390b2016-02-17 13:50:42 -08001501 ft.replace(getFragmentId(), fragment);
Steve McKay9ed88a42016-01-21 18:46:15 -08001502 ft.commitAllowingStateLoss();
1503 }
1504
1505 private static String buildStateKey(RootInfo root, DocumentInfo doc) {
1506 final StringBuilder builder = new StringBuilder();
1507 builder.append(root != null ? root.authority : "null").append(';');
1508 builder.append(root != null ? root.rootId : "null").append(';');
1509 builder.append(doc != null ? doc.documentId : "null");
1510 return builder.toString();
1511 }
1512
1513 public static @Nullable DirectoryFragment get(FragmentManager fm) {
1514 // TODO: deal with multiple directories shown at once
Aga Wronska893390b2016-02-17 13:50:42 -08001515 Fragment fragment = fm.findFragmentById(getFragmentId());
Steve McKay9ed88a42016-01-21 18:46:15 -08001516 return fragment instanceof DirectoryFragment
1517 ? (DirectoryFragment) fragment
1518 : null;
1519 }
Aga Wronska893390b2016-02-17 13:50:42 -08001520
1521 private static int getFragmentId() {
1522 return R.id.container_directory;
1523 }
1524
1525 @Override
1526 public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) {
1527 Context context = getActivity();
1528 State state = getDisplayState();
1529
1530 Uri contentsUri;
1531 switch (mType) {
1532 case TYPE_NORMAL:
1533 contentsUri = mSearchMode ? DocumentsContract.buildSearchDocumentsUri(
1534 mRoot.authority, mRoot.rootId, mQuery)
1535 : DocumentsContract.buildChildDocumentsUri(
1536 mDocument.authority, mDocument.documentId);
Steve McKay75fe6bc2016-03-07 11:51:31 -08001537 if (mTuner.enableManagedMode()) {
Aga Wronska893390b2016-02-17 13:50:42 -08001538 contentsUri = DocumentsContract.setManageMode(contentsUri);
1539 }
1540 return new DirectoryLoader(
Steve McKay27d20a32016-02-22 18:38:09 -08001541 context, mType, mRoot, mDocument, contentsUri, state.userSortOrder,
1542 mSearchMode);
Aga Wronska893390b2016-02-17 13:50:42 -08001543 case TYPE_RECENT_OPEN:
1544 final RootsCache roots = DocumentsApplication.getRootsCache(context);
1545 return new RecentsLoader(context, roots, state);
Steve McKay75fe6bc2016-03-07 11:51:31 -08001546
Aga Wronska893390b2016-02-17 13:50:42 -08001547 default:
1548 throw new IllegalStateException("Unknown type " + mType);
1549 }
1550 }
1551
1552 @Override
1553 public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
1554 if (!isAdded()) return;
1555
1556 State state = getDisplayState();
1557
1558 mAdapter.notifyDataSetChanged();
1559 mModel.update(result);
1560
1561 state.derivedSortOrder = result.sortOrder;
1562
1563 updateLayout(state.derivedMode);
1564
1565 if (mSelection != null) {
1566 mSelectionManager.setItemsSelected(mSelection.toList(), true);
1567 }
1568
1569 // Restore any previous instance state
1570 final SparseArray<Parcelable> container = state.dirState.remove(mStateKey);
1571 if (container != null && !getArguments().getBoolean(Shared.EXTRA_IGNORE_STATE, false)) {
1572 getView().restoreHierarchyState(container);
1573 } else if (mLastSortOrder != state.derivedSortOrder) {
1574 // The derived sort order takes the user sort order into account, but applies
1575 // directory-specific defaults when the user doesn't explicitly set the sort
1576 // order. Scroll to the top if the sort order actually changed.
1577 mRecView.smoothScrollToPosition(0);
1578 }
1579
1580 mLastSortOrder = state.derivedSortOrder;
1581
1582 mTuner.onModelLoaded(mModel, mType, mSearchMode);
1583
1584 }
1585
1586 @Override
1587 public void onLoaderReset(Loader<DirectoryResult> loader) {
1588 mModel.update(null);
1589 }
1590 }