blob: ef7169363299ea9ac66538e928fad98ff0ce3d8f [file] [log] [blame]
Jeff Sharkeye22d02e2013-04-26 16:54:55 -07001/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
Steve McKayf68210e2015-11-03 15:23:16 -080017package com.android.documentsui.dirlist;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070018
Steve McKayf8a5e082015-09-23 17:21:40 -070019import static com.android.documentsui.Shared.DEBUG;
Steve McKayf8a5e082015-09-23 17:21:40 -070020import static com.android.documentsui.State.ACTION_MANAGE;
21import static com.android.documentsui.State.MODE_GRID;
22import static com.android.documentsui.State.MODE_LIST;
23import static com.android.documentsui.State.MODE_UNKNOWN;
24import static com.android.documentsui.State.SORT_ORDER_UNKNOWN;
Jeff Sharkeyac9e6272013-08-31 21:27:44 -070025import static com.android.documentsui.model.DocumentInfo.getCursorInt;
26import static com.android.documentsui.model.DocumentInfo.getCursorLong;
27import static com.android.documentsui.model.DocumentInfo.getCursorString;
Steve McKayef280152015-06-11 10:10:49 -070028import static com.android.internal.util.Preconditions.checkNotNull;
Steve McKayd57f5fa2015-07-23 16:33:41 -070029import static com.android.internal.util.Preconditions.checkState;
Steve McKaye11ef242015-12-07 16:31:42 -080030import static com.google.common.base.Preconditions.checkArgument;
Steve McKay0599a442015-05-05 14:50:00 -070031
Ben Kwaf5858932015-04-07 15:43:39 -070032import android.app.Activity;
Jeff Sharkeyf63b7772013-10-01 17:57:41 -070033import android.app.ActivityManager;
Jeff Sharkey09c10bf2013-06-30 20:02:59 -070034import android.app.Fragment;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070035import android.app.FragmentManager;
36import android.app.FragmentTransaction;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070037import android.app.LoaderManager.LoaderCallbacks;
Vladislav Kaznacheev89b90332015-05-01 13:46:57 -070038import android.content.ClipData;
Jeff Sharkey3fd11772013-09-30 14:26:27 -070039import android.content.ContentProviderClient;
Jeff Sharkey873daa32013-08-18 17:38:20 -070040import android.content.ContentResolver;
Jeff Sharkeyd10f0492013-09-09 17:35:46 -070041import android.content.ContentValues;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070042import android.content.Context;
Jeff Sharkey873daa32013-08-18 17:38:20 -070043import android.content.Intent;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070044import android.content.Loader;
Jeff Sharkeyac9e6272013-08-31 21:27:44 -070045import android.database.Cursor;
Jeff Sharkey8a8fb672013-05-07 12:41:33 -070046import android.graphics.Bitmap;
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -070047import android.graphics.Canvas;
Jeff Sharkey8a8fb672013-05-07 12:41:33 -070048import android.graphics.Point;
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -070049import android.graphics.drawable.Drawable;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070050import android.net.Uri;
Jeff Sharkey8a8fb672013-05-07 12:41:33 -070051import android.os.AsyncTask;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070052import android.os.Bundle;
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -070053import android.os.CancellationSignal;
Jeff Sharkeye39a89b2013-10-29 11:56:37 -070054import android.os.OperationCanceledException;
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -070055import android.os.Parcelable;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070056import android.provider.DocumentsContract;
Jeff Sharkeyac9e6272013-08-31 21:27:44 -070057import android.provider.DocumentsContract.Document;
Ben Kwa24be5d32015-08-27 16:04:46 -070058import android.support.annotation.Nullable;
Ben Kwa91923182015-08-27 16:06:33 -070059import android.support.design.widget.Snackbar;
Steve McKayef280152015-06-11 10:10:49 -070060import android.support.v7.widget.GridLayoutManager;
61import android.support.v7.widget.LinearLayoutManager;
62import android.support.v7.widget.RecyclerView;
63import android.support.v7.widget.RecyclerView.LayoutManager;
Steve McKay9eabc502015-10-19 12:04:21 -070064import android.support.v7.widget.RecyclerView.OnItemTouchListener;
Steve McKayef280152015-06-11 10:10:49 -070065import android.support.v7.widget.RecyclerView.RecyclerListener;
66import android.support.v7.widget.RecyclerView.ViewHolder;
Jeff Sharkey6d579272015-06-11 09:16:19 -070067import android.text.TextUtils;
Jeff Sharkey09c10bf2013-06-30 20:02:59 -070068import android.text.format.DateUtils;
Jeff Sharkey2e694f82013-08-06 16:26:14 -070069import android.text.format.Formatter;
Jeff Sharkey8a8fb672013-05-07 12:41:33 -070070import android.text.format.Time;
71import android.util.Log;
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -070072import android.util.SparseArray;
Ben Kwa0574b182015-09-08 07:31:19 -070073import android.util.TypedValue;
Jeff Sharkeyc317af82013-07-01 16:56:54 -070074import android.view.ActionMode;
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -070075import android.view.DragEvent;
Steve McKayef280152015-06-11 10:10:49 -070076import android.view.GestureDetector;
Ben Kwa98bb6cf2015-10-14 08:00:27 -070077import android.view.KeyEvent;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070078import android.view.LayoutInflater;
Jeff Sharkey09c10bf2013-06-30 20:02:59 -070079import android.view.Menu;
Jeff Sharkey09c10bf2013-06-30 20:02:59 -070080import android.view.MenuItem;
Steve McKayef280152015-06-11 10:10:49 -070081import android.view.MotionEvent;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070082import android.view.View;
83import android.view.ViewGroup;
Vladislav Kaznacheev9400b892015-09-04 09:17:37 -070084import android.view.ViewParent;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070085import android.widget.ImageView;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070086import android.widget.TextView;
87
Steve McKayf68210e2015-11-03 15:23:16 -080088import com.android.documentsui.BaseActivity;
89import com.android.documentsui.CopyService;
90import com.android.documentsui.DirectoryLoader;
91import com.android.documentsui.DirectoryResult;
92import com.android.documentsui.DocumentClipper;
93import com.android.documentsui.DocumentsActivity;
94import com.android.documentsui.DocumentsApplication;
95import com.android.documentsui.Events;
96import com.android.documentsui.IconUtils;
97import com.android.documentsui.Menus;
98import com.android.documentsui.MessageBar;
99import com.android.documentsui.MimePredicate;
100import com.android.documentsui.ProviderExecutor;
Steve McKaye11ef242015-12-07 16:31:42 -0800101import com.android.documentsui.ProviderExecutor.Preemptable;
Steve McKayf68210e2015-11-03 15:23:16 -0800102import com.android.documentsui.R;
103import com.android.documentsui.RecentLoader;
104import com.android.documentsui.RecentsProvider;
Steve McKaye11ef242015-12-07 16:31:42 -0800105import com.android.documentsui.RecentsProvider.StateColumns;
Steve McKayf68210e2015-11-03 15:23:16 -0800106import com.android.documentsui.RootCursorWrapper;
107import com.android.documentsui.RootsCache;
108import com.android.documentsui.Shared;
109import com.android.documentsui.Snackbars;
110import com.android.documentsui.State;
111import com.android.documentsui.ThumbnailCache;
Steve McKayf68210e2015-11-03 15:23:16 -0800112import com.android.documentsui.dirlist.MultiSelectManager.Selection;
Jeff Sharkey724deeb2013-08-31 15:02:20 -0700113import com.android.documentsui.model.DocumentInfo;
Tomasz Mikolajewski9452c442015-04-14 16:32:41 +0900114import com.android.documentsui.model.DocumentStack;
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700115import com.android.documentsui.model.RootInfo;
Steve McKayfefcd702015-08-20 16:19:38 +0000116import com.google.common.collect.Lists;
Ben Kwada858bf2015-12-09 14:33:49 -0800117import com.google.common.collect.Sets;
Jeff Sharkeyc317af82013-07-01 16:56:54 -0700118
119import java.util.ArrayList;
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -0700120import java.util.Collections;
Jeff Sharkeya5defe32013-08-05 17:56:48 -0700121import java.util.List;
Ben Kwada858bf2015-12-09 14:33:49 -0800122import java.util.Set;
Jeff Sharkey09c10bf2013-06-30 20:02:59 -0700123
124/**
125 * Display the documents inside a single directory.
126 */
127public class DirectoryFragment extends Fragment {
128
Jeff Sharkeya5defe32013-08-05 17:56:48 -0700129 public static final int TYPE_NORMAL = 1;
130 public static final int TYPE_SEARCH = 2;
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700131 public static final int TYPE_RECENT_OPEN = 3;
Jeff Sharkey5b535922013-08-02 15:55:26 -0700132
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700133 public static final int ANIM_NONE = 1;
134 public static final int ANIM_SIDE = 2;
135 public static final int ANIM_DOWN = 3;
136 public static final int ANIM_UP = 4;
137
Ben Kwaf5858932015-04-07 15:43:39 -0700138 public static final int REQUEST_COPY_DESTINATION = 1;
139
Ben Kwa0497da82015-11-30 23:00:02 -0800140 private static final String TAG = "DirectoryFragment";
141
Steve McKayef280152015-06-11 10:10:49 -0700142 private static final int LOADER_ID = 42;
Steve McKay6035b3c2015-12-04 11:19:09 -0800143 private static final boolean DEBUG_ENABLE_DND = true;
Steve McKay8e258c62015-05-06 14:27:57 -0700144
Jeff Sharkey2e694f82013-08-06 16:26:14 -0700145 private static final String EXTRA_TYPE = "type";
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700146 private static final String EXTRA_ROOT = "root";
147 private static final String EXTRA_DOC = "doc";
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700148 private static final String EXTRA_QUERY = "query";
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700149 private static final String EXTRA_IGNORE_STATE = "ignoreState";
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700150
Ben Kwa7461a952015-09-01 11:03:01 -0700151 private Model mModel;
Ben Kwaa017bc12015-10-07 14:15:12 -0700152 private MultiSelectManager mSelectionManager;
Ben Kwa379e1762015-09-21 10:49:52 -0700153 private Model.UpdateListener mModelUpdateListener = new ModelUpdateListener();
Ben Kwa98bb6cf2015-10-14 08:00:27 -0700154 private ItemClickListener mItemClickListener = new ItemClickListener();
Ben Kwa24be5d32015-08-27 16:04:46 -0700155
Steve McKayef280152015-06-11 10:10:49 -0700156 private View mEmptyView;
157 private RecyclerView mRecView;
158
159 private int mType = TYPE_NORMAL;
160 private String mStateKey;
161
162 private int mLastMode = MODE_UNKNOWN;
163 private int mLastSortOrder = SORT_ORDER_UNKNOWN;
164 private boolean mLastShowSize;
165 private boolean mHideGridTitles;
166 private boolean mSvelteRecents;
167 private Point mThumbSize;
168 private DocumentsAdapter mAdapter;
169 private LoaderCallbacks<DirectoryResult> mCallbacks;
Steve McKayb85f90c2015-11-09 17:38:35 +0900170 private FragmentTuner mTuner;
Steve McKay1f199482015-05-20 15:58:42 -0700171 private DocumentClipper mClipper;
Steve McKayef280152015-06-11 10:10:49 -0700172 // These are lazily initialized.
Steve McKayd57f5fa2015-07-23 16:33:41 -0700173 private LinearLayoutManager mListLayout;
174 private GridLayoutManager mGridLayout;
Steve McKayd57f5fa2015-07-23 16:33:41 -0700175 private int mColumnCount = 1; // This will get updated when layout changes.
Steve McKay1f199482015-05-20 15:58:42 -0700176
Ben Kwac42fa402015-09-16 08:04:37 -0700177 private MessageBar mMessageBar;
Ben Kwa379e1762015-09-21 10:49:52 -0700178 private View mProgressBar;
Ben Kwac42fa402015-09-16 08:04:37 -0700179
Ben Kwa60d91e42015-10-20 15:02:33 -0700180 private int mSelectedItemColor;
181 private int mDefaultItemColor;
182
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700183 public static void showNormal(FragmentManager fm, RootInfo root, DocumentInfo doc, int anim) {
184 show(fm, TYPE_NORMAL, root, doc, null, anim);
Jeff Sharkey2e694f82013-08-06 16:26:14 -0700185 }
186
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700187 public static void showSearch(FragmentManager fm, RootInfo root, String query, int anim) {
188 show(fm, TYPE_SEARCH, root, null, query, anim);
Jeff Sharkey2e694f82013-08-06 16:26:14 -0700189 }
190
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700191 public static void showRecentsOpen(FragmentManager fm, int anim) {
192 show(fm, TYPE_RECENT_OPEN, null, null, null, anim);
Jeff Sharkey2e694f82013-08-06 16:26:14 -0700193 }
194
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700195 private static void show(FragmentManager fm, int type, RootInfo root, DocumentInfo doc,
196 String query, int anim) {
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700197 final Bundle args = new Bundle();
Jeff Sharkey2e694f82013-08-06 16:26:14 -0700198 args.putInt(EXTRA_TYPE, type);
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700199 args.putParcelable(EXTRA_ROOT, root);
200 args.putParcelable(EXTRA_DOC, doc);
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700201 args.putString(EXTRA_QUERY, query);
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700202
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700203 final FragmentTransaction ft = fm.beginTransaction();
204 switch (anim) {
205 case ANIM_SIDE:
206 args.putBoolean(EXTRA_IGNORE_STATE, true);
207 break;
208 case ANIM_DOWN:
209 args.putBoolean(EXTRA_IGNORE_STATE, true);
210 ft.setCustomAnimations(R.animator.dir_down, R.animator.dir_frozen);
211 break;
212 case ANIM_UP:
213 ft.setCustomAnimations(R.animator.dir_frozen, R.animator.dir_up);
214 break;
215 }
216
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700217 final DirectoryFragment fragment = new DirectoryFragment();
218 fragment.setArguments(args);
219
Jeff Sharkey76112212013-08-06 11:26:10 -0700220 ft.replace(R.id.container_directory, fragment);
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700221 ft.commitAllowingStateLoss();
222 }
223
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700224 private static String buildStateKey(RootInfo root, DocumentInfo doc) {
225 final StringBuilder builder = new StringBuilder();
226 builder.append(root != null ? root.authority : "null").append(';');
227 builder.append(root != null ? root.rootId : "null").append(';');
228 builder.append(doc != null ? doc.documentId : "null");
229 return builder.toString();
230 }
231
Steve McKay0fe95042015-10-14 17:41:13 -0700232 public static @Nullable DirectoryFragment get(FragmentManager fm) {
Jeff Sharkeya5defe32013-08-05 17:56:48 -0700233 // TODO: deal with multiple directories shown at once
Steve McKay0fe95042015-10-14 17:41:13 -0700234 Fragment fragment = fm.findFragmentById(R.id.container_directory);
235 return fragment instanceof DirectoryFragment
236 ? (DirectoryFragment) fragment
237 : null;
Jeff Sharkeya5defe32013-08-05 17:56:48 -0700238 }
239
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700240 @Override
241 public View onCreateView(
242 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
Jeff Sharkey09c10bf2013-06-30 20:02:59 -0700243 final View view = inflater.inflate(R.layout.fragment_directory, container, false);
244
Ben Kwac42fa402015-09-16 08:04:37 -0700245 mMessageBar = MessageBar.create(getChildFragmentManager());
Ben Kwa379e1762015-09-21 10:49:52 -0700246 mProgressBar = view.findViewById(R.id.progressbar);
Ben Kwac42fa402015-09-16 08:04:37 -0700247
Jeff Sharkeyc6cbdf12013-08-07 16:22:02 -0700248 mEmptyView = view.findViewById(android.R.id.empty);
249
Steve McKay47d8c322015-10-09 15:12:40 -0700250 mRecView = (RecyclerView) view.findViewById(R.id.list);
Steve McKayef280152015-06-11 10:10:49 -0700251 mRecView.setRecyclerListener(
252 new RecyclerListener() {
253 @Override
254 public void onViewRecycled(ViewHolder holder) {
255 cancelThumbnailTask(holder.itemView);
256 }
257 });
Steve McKay8e258c62015-05-06 14:27:57 -0700258
Steve McKayd57f5fa2015-07-23 16:33:41 -0700259 // TODO: Rather than update columns on layout changes, push this
260 // code (or something like it) into GridLayoutManager.
261 mRecView.addOnLayoutChangeListener(
Ben Kwa98bb6cf2015-10-14 08:00:27 -0700262 new View.OnLayoutChangeListener() {
Steve McKayd57f5fa2015-07-23 16:33:41 -0700263
264 @Override
265 public void onLayoutChange(
266 View v, int left, int top, int right, int bottom, int oldLeft,
267 int oldTop, int oldRight, int oldBottom) {
Steve McKayfefcd702015-08-20 16:19:38 +0000268 mColumnCount = calculateColumnCount();
Steve McKayd57f5fa2015-07-23 16:33:41 -0700269 if (mGridLayout != null) {
270 mGridLayout.setSpanCount(mColumnCount);
271 }
272 }
273 });
274
Ben Kwa60d91e42015-10-20 15:02:33 -0700275 mRecView.setItemAnimator(new DirectoryItemAnimator(getActivity()));
Ben Kwa98bb6cf2015-10-14 08:00:27 -0700276
Steve McKayd57f5fa2015-07-23 16:33:41 -0700277 // TODO: Add a divider between views (which might use RecyclerView.ItemDecoration).
Steve McKay8e258c62015-05-06 14:27:57 -0700278 if (DEBUG_ENABLE_DND) {
Steve McKayef280152015-06-11 10:10:49 -0700279 setupDragAndDropOnDirectoryView(mRecView);
Steve McKay8e258c62015-05-06 14:27:57 -0700280 }
Jeff Sharkey09c10bf2013-06-30 20:02:59 -0700281
Jeff Sharkeyac9e6272013-08-31 21:27:44 -0700282 return view;
283 }
Jeff Sharkey09c10bf2013-06-30 20:02:59 -0700284
Jeff Sharkeyac9e6272013-08-31 21:27:44 -0700285 @Override
Jeff Sharkeyf63b7772013-10-01 17:57:41 -0700286 public void onDestroyView() {
287 super.onDestroyView();
288
289 // Cancel any outstanding thumbnail requests
Steve McKayef280152015-06-11 10:10:49 -0700290 final int count = mRecView.getChildCount();
Jeff Sharkeyf63b7772013-10-01 17:57:41 -0700291 for (int i = 0; i < count; i++) {
Steve McKayef280152015-06-11 10:10:49 -0700292 final View view = mRecView.getChildAt(i);
293 cancelThumbnailTask(view);
Jeff Sharkeyf63b7772013-10-01 17:57:41 -0700294 }
Jeff Sharkeyfaaeb392013-10-04 14:44:56 -0700295
Steve McKayef280152015-06-11 10:10:49 -0700296 // Clear any outstanding selection
Ben Kwaa017bc12015-10-07 14:15:12 -0700297 mSelectionManager.clearSelection();
Jeff Sharkeyf63b7772013-10-01 17:57:41 -0700298 }
299
300 @Override
Jeff Sharkeyac9e6272013-08-31 21:27:44 -0700301 public void onActivityCreated(Bundle savedInstanceState) {
302 super.onActivityCreated(savedInstanceState);
303
304 final Context context = getActivity();
Steve McKayb85f90c2015-11-09 17:38:35 +0900305 final State state = getDisplayState();
Jeff Sharkeyac9e6272013-08-31 21:27:44 -0700306
Jeff Sharkey9656a532013-09-13 13:42:19 -0700307 final RootInfo root = getArguments().getParcelable(EXTRA_ROOT);
308 final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC);
309
Steve McKayef280152015-06-11 10:10:49 -0700310 mAdapter = new DocumentsAdapter(context);
311 mRecView.setAdapter(mAdapter);
312
Ben Kwa4e12b162015-12-09 14:48:59 -0800313 mDefaultItemColor = context.getResources().getColor(R.color.item_doc_background);
314 mSelectedItemColor = context.getResources().getColor(R.color.item_doc_background_selected);
Ben Kwa60d91e42015-10-20 15:02:33 -0700315
Steve McKayef280152015-06-11 10:10:49 -0700316 GestureDetector.SimpleOnGestureListener listener =
317 new GestureDetector.SimpleOnGestureListener() {
318 @Override
319 public boolean onSingleTapUp(MotionEvent e) {
320 return DirectoryFragment.this.onSingleTapUp(e);
321 }
Steve McKay93d8ef42015-07-30 12:27:44 -0700322 @Override
323 public boolean onDoubleTap(MotionEvent e) {
324 Log.d(TAG, "Handling double tap.");
325 return DirectoryFragment.this.onDoubleTap(e);
326 }
Steve McKayef280152015-06-11 10:10:49 -0700327 };
328
Steve McKay9eabc502015-10-19 12:04:21 -0700329 final GestureDetector detector = new GestureDetector(this.getContext(), listener);
330 detector.setOnDoubleTapListener(listener);
331
332 mRecView.addOnItemTouchListener(
333 new OnItemTouchListener() {
334 @Override
335 public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
336 detector.onTouchEvent(e);
337 return false;
338 }
339
340 @Override
341 public void onTouchEvent(RecyclerView rv, MotionEvent e) {}
342
343 @Override
344 public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {}
345 });
346
Ben Kwa24be5d32015-08-27 16:04:46 -0700347 // TODO: instead of inserting the view into the constructor, extract listener-creation code
348 // and set the listener on the view after the fact. Then the view doesn't need to be passed
Ben Kwaa017bc12015-10-07 14:15:12 -0700349 // into the selection manager.
350 mSelectionManager = new MultiSelectManager(
Steve McKay57394872015-08-12 14:48:34 -0700351 mRecView,
Steve McKay57394872015-08-12 14:48:34 -0700352 state.allowMultiple
353 ? MultiSelectManager.MODE_MULTIPLE
354 : MultiSelectManager.MODE_SINGLE);
Ben Kwaa017bc12015-10-07 14:15:12 -0700355 mSelectionManager.addCallback(new SelectionModeListener());
Ben Kwa7461a952015-09-01 11:03:01 -0700356
Ben Kwaa017bc12015-10-07 14:15:12 -0700357 mModel = new Model(context, mAdapter);
Ben Kwad72a1da2015-12-01 19:56:57 -0800358 mModel.addUpdateListener(mAdapter);
Ben Kwa379e1762015-09-21 10:49:52 -0700359 mModel.addUpdateListener(mModelUpdateListener);
Steve McKayef280152015-06-11 10:10:49 -0700360
Jeff Sharkey2e694f82013-08-06 16:26:14 -0700361 mType = getArguments().getInt(EXTRA_TYPE);
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700362 mStateKey = buildStateKey(root, doc);
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700363
Steve McKayb85f90c2015-11-09 17:38:35 +0900364 mTuner = FragmentTuner.pick(state);
Steve McKay1f199482015-05-20 15:58:42 -0700365 mClipper = new DocumentClipper(context);
366
Jeff Sharkey9656a532013-09-13 13:42:19 -0700367 if (mType == TYPE_RECENT_OPEN) {
368 // Hide titles when showing recents for picking images/videos
369 mHideGridTitles = MimePredicate.mimeMatches(
370 MimePredicate.VISUAL_MIMES, state.acceptMimes);
371 } else {
372 mHideGridTitles = (doc != null) && doc.isGridTitlesHidden();
373 }
374
Jeff Sharkeyf63b7772013-10-01 17:57:41 -0700375 final ActivityManager am = (ActivityManager) context.getSystemService(
376 Context.ACTIVITY_SERVICE);
377 mSvelteRecents = am.isLowRamDevice() && (mType == TYPE_RECENT_OPEN);
378
Jeff Sharkey46899c82013-08-18 22:26:48 -0700379 mCallbacks = new LoaderCallbacks<DirectoryResult>() {
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700380 @Override
Jeff Sharkey46899c82013-08-18 22:26:48 -0700381 public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) {
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700382 final String query = getArguments().getString(EXTRA_QUERY);
Jeff Sharkey46165b52013-07-31 20:53:22 -0700383
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700384 Uri contentsUri;
385 switch (mType) {
386 case TYPE_NORMAL:
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700387 contentsUri = DocumentsContract.buildChildDocumentsUri(
388 doc.authority, doc.documentId);
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -0700389 if (state.action == ACTION_MANAGE) {
390 contentsUri = DocumentsContract.setManageMode(contentsUri);
391 }
Jeff Sharkeyd10f0492013-09-09 17:35:46 -0700392 return new DirectoryLoader(
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -0700393 context, mType, root, doc, contentsUri, state.userSortOrder);
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700394 case TYPE_SEARCH:
395 contentsUri = DocumentsContract.buildSearchDocumentsUri(
Jeff Sharkey0e8c8712013-09-12 21:59:06 -0700396 root.authority, root.rootId, query);
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -0700397 if (state.action == ACTION_MANAGE) {
398 contentsUri = DocumentsContract.setManageMode(contentsUri);
399 }
Jeff Sharkeyd10f0492013-09-09 17:35:46 -0700400 return new DirectoryLoader(
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -0700401 context, mType, root, doc, contentsUri, state.userSortOrder);
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700402 case TYPE_RECENT_OPEN:
Jeff Sharkey1c903cc2013-09-02 17:19:40 -0700403 final RootsCache roots = DocumentsApplication.getRootsCache(context);
Jeff Sharkey8b997042013-09-19 15:25:56 -0700404 return new RecentLoader(context, roots, state);
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700405 default:
406 throw new IllegalStateException("Unknown type " + mType);
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700407 }
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700408 }
409
410 @Override
Jeff Sharkey46899c82013-08-18 22:26:48 -0700411 public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700412 if (!isAdded()) return;
413
Ben Kwa24be5d32015-08-27 16:04:46 -0700414 mModel.update(result);
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700415
416 // Push latest state up to UI
417 // TODO: if mode change was racing with us, don't overwrite it
Jeff Sharkey7d58fc62013-09-12 16:25:02 -0700418 if (result.mode != MODE_UNKNOWN) {
419 state.derivedMode = result.mode;
420 }
Jeff Sharkeyd10f0492013-09-09 17:35:46 -0700421 state.derivedSortOrder = result.sortOrder;
Steve McKayd0a2a2c2015-03-25 14:35:33 -0700422 ((BaseActivity) context).onStateChanged();
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700423
424 updateDisplayState();
425
Jeff Sharkey25f10b32013-10-07 14:08:17 -0700426 // When launched into empty recents, show drawer
Ben Kwa24be5d32015-08-27 16:04:46 -0700427 if (mType == TYPE_RECENT_OPEN && mModel.isEmpty() && !state.stackTouched &&
Steve McKayb68dd222015-04-20 17:18:15 -0700428 context instanceof DocumentsActivity) {
429 ((DocumentsActivity) context).setRootsDrawerOpen(true);
Jeff Sharkey25f10b32013-10-07 14:08:17 -0700430 }
431
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700432 // Restore any previous instance state
433 final SparseArray<Parcelable> container = state.dirState.remove(mStateKey);
434 if (container != null && !getArguments().getBoolean(EXTRA_IGNORE_STATE, false)) {
435 getView().restoreHierarchyState(container);
436 } else if (mLastSortOrder != state.derivedSortOrder) {
Ben Kwab8a5e082015-12-07 13:25:27 -0800437 // The derived sort order takes the user sort order into account, but applies
438 // directory-specific defaults when the user doesn't explicitly set the sort
439 // order. Scroll to the top if the sort order actually changed.
Steve McKayef280152015-06-11 10:10:49 -0700440 mRecView.smoothScrollToPosition(0);
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700441 }
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700442
443 mLastSortOrder = state.derivedSortOrder;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700444 }
445
446 @Override
Jeff Sharkey46899c82013-08-18 22:26:48 -0700447 public void onLoaderReset(Loader<DirectoryResult> loader) {
Ben Kwa24be5d32015-08-27 16:04:46 -0700448 mModel.update(null);
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700449 }
450 };
451
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700452 // Kick off loader at least once
Steve McKayef280152015-06-11 10:10:49 -0700453 getLoaderManager().restartLoader(LOADER_ID, null, mCallbacks);
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700454
Jeff Sharkey2e694f82013-08-06 16:26:14 -0700455 updateDisplayState();
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700456 }
457
Jeff Sharkey42d26792013-09-06 13:22:09 -0700458 @Override
Ben Kwaf5858932015-04-07 15:43:39 -0700459 public void onActivityResult(int requestCode, int resultCode, Intent data) {
Ben Kwaf5858932015-04-07 15:43:39 -0700460 // There's only one request code right now. Replace this with a switch statement or
461 // something more scalable when more codes are added.
462 if (requestCode != REQUEST_COPY_DESTINATION) {
463 return;
464 }
465 if (resultCode == Activity.RESULT_CANCELED || data == null) {
466 // User pressed the back button or otherwise cancelled the destination pick. Don't
467 // proceed with the copy.
468 return;
469 }
470
Steve McKayb85f90c2015-11-09 17:38:35 +0900471 CopyService.start(getActivity(), getDisplayState().selectedDocumentsForCopy,
Steve McKay4d0255f2015-09-25 16:02:56 -0700472 (DocumentStack) data.getParcelableExtra(Shared.EXTRA_STACK),
Tomasz Mikolajewski85578962015-11-04 15:33:20 +0900473 data.getIntExtra(CopyService.EXTRA_TRANSFER_MODE, CopyService.TRANSFER_MODE_COPY));
Ben Kwaf5858932015-04-07 15:43:39 -0700474 }
475
Steve McKayef280152015-06-11 10:10:49 -0700476 private boolean onSingleTapUp(MotionEvent e) {
Ben Kwa98bb6cf2015-10-14 08:00:27 -0700477 // Only respond to touch events. Single-click mouse events are selection events and are
478 // handled by the selection manager. Tap events that occur while the selection manager is
479 // active are also selection events.
480 if (Events.isTouchEvent(e) && !mSelectionManager.hasSelection()) {
Ben Kwa0497da82015-11-30 23:00:02 -0800481 String id = getModelId(e);
482 if (id != null) {
483 return handleViewItem(id);
Steve McKayef280152015-06-11 10:10:49 -0700484 }
485 }
Steve McKay93d8ef42015-07-30 12:27:44 -0700486 return false;
487 }
Steve McKayef280152015-06-11 10:10:49 -0700488
Steve McKay93d8ef42015-07-30 12:27:44 -0700489 protected boolean onDoubleTap(MotionEvent e) {
490 if (Events.isMouseEvent(e)) {
491 Log.d(TAG, "Handling double tap from mouse.");
Ben Kwa0497da82015-11-30 23:00:02 -0800492 String id = getModelId(e);
493 if (id != null) {
494 return handleViewItem(id);
Steve McKay93d8ef42015-07-30 12:27:44 -0700495 }
496 }
497 return false;
498 }
499
Ben Kwa0497da82015-11-30 23:00:02 -0800500 private boolean handleViewItem(String id) {
501 final Cursor cursor = mModel.getItem(id);
Steve McKay93d8ef42015-07-30 12:27:44 -0700502 checkNotNull(cursor, "Cursor cannot be null.");
503 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
504 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
Steve McKayb85f90c2015-11-09 17:38:35 +0900505 if (mTuner.isDocumentEnabled(docMimeType, docFlags)) {
Steve McKay93d8ef42015-07-30 12:27:44 -0700506 final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
Ben Kwa24be5d32015-08-27 16:04:46 -0700507 ((BaseActivity) getActivity()).onDocumentPicked(doc, mModel);
Ben Kwaa017bc12015-10-07 14:15:12 -0700508 mSelectionManager.clearSelection();
Steve McKay93d8ef42015-07-30 12:27:44 -0700509 return true;
510 }
Steve McKayef280152015-06-11 10:10:49 -0700511 return false;
512 }
513
Ben Kwaf5858932015-04-07 15:43:39 -0700514 @Override
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700515 public void onStop() {
516 super.onStop();
517
518 // Remember last scroll location
519 final SparseArray<Parcelable> container = new SparseArray<Parcelable>();
520 getView().saveHierarchyState(container);
Steve McKayb85f90c2015-11-09 17:38:35 +0900521 final State state = getDisplayState();
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700522 state.dirState.put(mStateKey, container);
523 }
524
525 @Override
Jeff Sharkey7d58fc62013-09-12 16:25:02 -0700526 public void onResume() {
527 super.onResume();
Jeff Sharkey42d26792013-09-06 13:22:09 -0700528 updateDisplayState();
529 }
530
Jeff Sharkeye8d13ea2014-08-08 15:10:03 -0700531 public void onDisplayStateChanged() {
532 updateDisplayState();
533 }
534
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700535 public void onUserSortOrderChanged() {
Jeff Sharkeyd10f0492013-09-09 17:35:46 -0700536 // Sort order change always triggers reload; we'll trigger state change
537 // on the flip side.
Steve McKayef280152015-06-11 10:10:49 -0700538 getLoaderManager().restartLoader(LOADER_ID, null, mCallbacks);
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700539 }
540
541 public void onUserModeChanged() {
Jeff Sharkeyd10f0492013-09-09 17:35:46 -0700542 final ContentResolver resolver = getActivity().getContentResolver();
Steve McKayb85f90c2015-11-09 17:38:35 +0900543 final State state = getDisplayState();
Jeff Sharkeyd10f0492013-09-09 17:35:46 -0700544
545 final RootInfo root = getArguments().getParcelable(EXTRA_ROOT);
546 final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC);
547
Jeff Sharkey0e8c8712013-09-12 21:59:06 -0700548 if (root != null && doc != null) {
Jeff Sharkey7d58fc62013-09-12 16:25:02 -0700549 final Uri stateUri = RecentsProvider.buildState(
550 root.authority, root.rootId, doc.documentId);
551 final ContentValues values = new ContentValues();
552 values.put(StateColumns.MODE, state.userMode);
Jeff Sharkeyd10f0492013-09-09 17:35:46 -0700553
Jeff Sharkey7d58fc62013-09-12 16:25:02 -0700554 new AsyncTask<Void, Void, Void>() {
555 @Override
556 protected Void doInBackground(Void... params) {
557 resolver.insert(stateUri, values);
558 return null;
559 }
560 }.execute();
561 }
Jeff Sharkeyd10f0492013-09-09 17:35:46 -0700562
563 // Mode change is just visual change; no need to kick loader, and
564 // deliver change event immediately.
565 state.derivedMode = state.userMode;
Steve McKayd0a2a2c2015-03-25 14:35:33 -0700566 ((BaseActivity) getActivity()).onStateChanged();
Jeff Sharkeyd10f0492013-09-09 17:35:46 -0700567
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700568 updateDisplayState();
569 }
570
571 private void updateDisplayState() {
Steve McKayb85f90c2015-11-09 17:38:35 +0900572 final State state = getDisplayState();
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700573
Jeff Sharkey5e1884d2013-09-10 17:56:39 -0700574 if (mLastMode == state.derivedMode && mLastShowSize == state.showSize) return;
Jeff Sharkeyd10f0492013-09-09 17:35:46 -0700575 mLastMode = state.derivedMode;
Jeff Sharkey5e1884d2013-09-10 17:56:39 -0700576 mLastShowSize = state.showSize;
Jeff Sharkey09c10bf2013-06-30 20:02:59 -0700577
Steve McKayef280152015-06-11 10:10:49 -0700578 updateLayout(state.derivedMode);
Jeff Sharkey09c10bf2013-06-30 20:02:59 -0700579
Steve McKayef280152015-06-11 10:10:49 -0700580 mRecView.setAdapter(mAdapter);
581 }
582
583 /**
584 * Returns a {@code LayoutManager} for {@code mode}, lazily initializing
585 * classes as needed.
586 */
587 private void updateLayout(int mode) {
Jeff Sharkey8a8fb672013-05-07 12:41:33 -0700588 final int thumbSize;
Steve McKayef280152015-06-11 10:10:49 -0700589
590 final LayoutManager layout;
591 switch (mode) {
592 case MODE_GRID:
Steve McKayef280152015-06-11 10:10:49 -0700593 thumbSize = getResources().getDimensionPixelSize(R.dimen.grid_width);
Steve McKaya9be7182015-07-22 16:03:35 -0700594 if (mGridLayout == null) {
Steve McKayd57f5fa2015-07-23 16:33:41 -0700595 mGridLayout = new GridLayoutManager(getContext(), mColumnCount );
Steve McKaya9be7182015-07-22 16:03:35 -0700596 }
Steve McKayef280152015-06-11 10:10:49 -0700597 layout = mGridLayout;
598 break;
599 case MODE_LIST:
Steve McKaya9be7182015-07-22 16:03:35 -0700600 thumbSize = getResources().getDimensionPixelSize(R.dimen.icon_size);
Steve McKayef280152015-06-11 10:10:49 -0700601 if (mListLayout == null) {
602 mListLayout = new LinearLayoutManager(getContext());
603 }
Steve McKayef280152015-06-11 10:10:49 -0700604 layout = mListLayout;
605 break;
606 case MODE_UNKNOWN:
607 default:
608 throw new IllegalArgumentException("Unsupported layout mode: " + mode);
Jeff Sharkey09c10bf2013-06-30 20:02:59 -0700609 }
Jeff Sharkey8a8fb672013-05-07 12:41:33 -0700610
Steve McKayef280152015-06-11 10:10:49 -0700611 mRecView.setLayoutManager(layout);
Kyle Horimoto2da6e4a2015-08-27 16:44:00 -0700612 // TODO: Once b/23691541 is resolved, use a listener within MultiSelectManager instead of
613 // imperatively calling this function.
Ben Kwaa017bc12015-10-07 14:15:12 -0700614 mSelectionManager.handleLayoutChanged();
Steve McKayef280152015-06-11 10:10:49 -0700615 // setting layout manager automatically invalidates existing ViewHolders.
Jeff Sharkey8a8fb672013-05-07 12:41:33 -0700616 mThumbSize = new Point(thumbSize, thumbSize);
Jeff Sharkey09c10bf2013-06-30 20:02:59 -0700617 }
618
Steve McKayfefcd702015-08-20 16:19:38 +0000619 private int calculateColumnCount() {
620 int cellWidth = getResources().getDimensionPixelSize(R.dimen.grid_width);
621 int cellMargin = 2 * getResources().getDimensionPixelSize(R.dimen.grid_item_margin);
Steve McKayd57f5fa2015-07-23 16:33:41 -0700622 int viewPadding = mRecView.getPaddingLeft() + mRecView.getPaddingRight();
Steve McKayfefcd702015-08-20 16:19:38 +0000623
Steve McKayd57f5fa2015-07-23 16:33:41 -0700624 checkState(mRecView.getWidth() > 0);
625 int columnCount = Math.max(1,
Steve McKayfefcd702015-08-20 16:19:38 +0000626 (mRecView.getWidth() - viewPadding) / (cellWidth + cellMargin));
627
Steve McKayd57f5fa2015-07-23 16:33:41 -0700628 return columnCount;
629 }
630
Steve McKayef280152015-06-11 10:10:49 -0700631 /**
632 * Manages the integration between our ActionMode and MultiSelectManager, initiating
633 * ActionMode when there is a selection, canceling it when there is no selection,
634 * and clearing selection when action mode is explicitly exited by the user.
635 */
636 private final class SelectionModeListener
637 implements MultiSelectManager.Callback, ActionMode.Callback {
638
639 private Selection mSelected = new Selection();
640 private ActionMode mActionMode;
Steve McKay4f4232d2015-07-22 12:13:46 -0700641 private int mNoDeleteCount = 0;
642 private Menu mMenu;
Steve McKayef280152015-06-11 10:10:49 -0700643
Jeff Sharkey09c10bf2013-06-30 20:02:59 -0700644 @Override
Ben Kwad72a1da2015-12-01 19:56:57 -0800645 public boolean onBeforeItemStateChange(String modelId, boolean selected) {
Steve McKayef280152015-06-11 10:10:49 -0700646 if (selected) {
Ben Kwad72a1da2015-12-01 19:56:57 -0800647 final Cursor cursor = mModel.getItem(modelId);
Steve McKayef280152015-06-11 10:10:49 -0700648 checkNotNull(cursor, "Cursor cannot be null.");
Jeff Sharkey7cf49032013-09-26 10:54:16 -0700649 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
650 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
Tomasz Mikolajewski95d9c252015-11-16 11:41:28 +0900651 return mTuner.canSelectType(docMimeType, docFlags);
Steve McKayef280152015-06-11 10:10:49 -0700652 }
653 return true;
654 }
655
656 @Override
Ben Kwad72a1da2015-12-01 19:56:57 -0800657 public void onItemStateChanged(String modelId, boolean selected) {
658 final Cursor cursor = mModel.getItem(modelId);
Steve McKay4f4232d2015-07-22 12:13:46 -0700659 checkNotNull(cursor, "Cursor cannot be null.");
660
661 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
662 if ((docFlags & Document.FLAG_SUPPORTS_DELETE) == 0) {
663 mNoDeleteCount += selected ? 1 : -1;
664 }
Steve McKay57394872015-08-12 14:48:34 -0700665 }
Steve McKay4f4232d2015-07-22 12:13:46 -0700666
Steve McKay57394872015-08-12 14:48:34 -0700667 @Override
668 public void onSelectionChanged() {
Ben Kwaa017bc12015-10-07 14:15:12 -0700669 mSelectionManager.getSelection(mSelected);
Ben Kwafe18c1b2015-09-11 15:40:18 -0700670 TypedValue color = new TypedValue();
Steve McKay4f4232d2015-07-22 12:13:46 -0700671 if (mSelected.size() > 0) {
Steve McKayef280152015-06-11 10:10:49 -0700672 if (DEBUG) Log.d(TAG, "Maybe starting action mode.");
673 if (mActionMode == null) {
674 if (DEBUG) Log.d(TAG, "Yeah. Starting action mode.");
675 mActionMode = getActivity().startActionMode(this);
Jeff Sharkeyaed873d2013-09-09 16:51:06 -0700676 }
Ben Kwaa017bc12015-10-07 14:15:12 -0700677 getActivity().getTheme().resolveAttribute(R.attr.colorActionMode, color, true);
Steve McKay4f4232d2015-07-22 12:13:46 -0700678 updateActionMenu();
679 } else {
680 if (DEBUG) Log.d(TAG, "Finishing action mode.");
681 if (mActionMode != null) {
682 mActionMode.finish();
683 }
Ben Kwa0574b182015-09-08 07:31:19 -0700684 getActivity().getTheme().resolveAttribute(
685 android.R.attr.colorPrimaryDark, color, true);
Jeff Sharkeyf339f252013-08-15 16:17:41 -0700686 }
Ben Kwafe18c1b2015-09-11 15:40:18 -0700687 getActivity().getWindow().setStatusBarColor(color.data);
Jeff Sharkey09c10bf2013-06-30 20:02:59 -0700688
Steve McKayef280152015-06-11 10:10:49 -0700689 if (mActionMode != null) {
Steve McKay467f2ba2015-10-27 14:15:58 -0700690 mActionMode.setTitle(String.valueOf(mSelected.size()));
Steve McKayef280152015-06-11 10:10:49 -0700691 }
692 }
693
694 // Called when the user exits the action mode
695 @Override
696 public void onDestroyActionMode(ActionMode mode) {
697 if (DEBUG) Log.d(TAG, "Handling action mode destroyed.");
698 mActionMode = null;
699 // clear selection
Ben Kwaa017bc12015-10-07 14:15:12 -0700700 mSelectionManager.clearSelection();
Steve McKay4f4232d2015-07-22 12:13:46 -0700701 mSelected.clear();
702 mNoDeleteCount = 0;
Steve McKayef280152015-06-11 10:10:49 -0700703 }
704
Jeff Sharkeyc317af82013-07-01 16:56:54 -0700705 @Override
706 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
Ben Kwaa017bc12015-10-07 14:15:12 -0700707 int size = mSelectionManager.getSelection().size();
Jeff Sharkeyc317af82013-07-01 16:56:54 -0700708 mode.getMenuInflater().inflate(R.menu.mode_directory, menu);
Ben Kwaa017bc12015-10-07 14:15:12 -0700709 mode.setTitle(TextUtils.formatSelectedCount(size));
710 return (size > 0);
Jeff Sharkeyc317af82013-07-01 16:56:54 -0700711 }
712
713 @Override
714 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
Steve McKay4f4232d2015-07-22 12:13:46 -0700715 mMenu = menu;
716 updateActionMenu();
Jeff Sharkeyc317af82013-07-01 16:56:54 -0700717 return true;
718 }
719
Steve McKay4f4232d2015-07-22 12:13:46 -0700720 private void updateActionMenu() {
721 checkNotNull(mMenu);
722 // Delegate update logic to our owning action, since specialized logic is desired.
Steve McKayb85f90c2015-11-09 17:38:35 +0900723 mTuner.updateActionMenu(mMenu, mType, mNoDeleteCount == 0);
Steve McKaye9809272015-10-01 11:39:24 -0700724 Menus.disableHiddenItems(mMenu);
Steve McKay4f4232d2015-07-22 12:13:46 -0700725 }
726
Jeff Sharkeyc317af82013-07-01 16:56:54 -0700727 @Override
Steve McKayef280152015-06-11 10:10:49 -0700728 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
Steve McKay1f199482015-05-20 15:58:42 -0700729
Ben Kwaa017bc12015-10-07 14:15:12 -0700730 Selection selection = mSelectionManager.getSelection(new Selection());
Jeff Sharkeyc317af82013-07-01 16:56:54 -0700731
Steve McKay3abb0e32015-12-01 17:02:42 -0800732 switch (item.getItemId()) {
733 case R.id.menu_open:
734 openDocuments(selection);
735 mode.finish();
736 return true;
Jeff Sharkey873daa32013-08-18 17:38:20 -0700737
Steve McKay3abb0e32015-12-01 17:02:42 -0800738 case R.id.menu_share:
739 shareDocuments(selection);
740 mode.finish();
741 return true;
Jeff Sharkey873daa32013-08-18 17:38:20 -0700742
Steve McKay3abb0e32015-12-01 17:02:42 -0800743 case R.id.menu_delete:
744 // Exit selection mode first, so we avoid deselecting deleted documents.
745 mode.finish();
746 deleteDocuments(selection);
747 return true;
Jeff Sharkey873daa32013-08-18 17:38:20 -0700748
Steve McKay3abb0e32015-12-01 17:02:42 -0800749 case R.id.menu_copy_to:
750 transferDocuments(selection, CopyService.TRANSFER_MODE_COPY);
751 mode.finish();
752 return true;
Ben Kwacb4461f2015-05-05 11:50:11 -0700753
Steve McKay3abb0e32015-12-01 17:02:42 -0800754 case R.id.menu_move_to:
755 // Exit selection mode first, so we avoid deselecting deleted documents.
756 mode.finish();
757 transferDocuments(selection, CopyService.TRANSFER_MODE_MOVE);
758 return true;
Ben Kwa41b26c12015-03-31 10:11:43 -0700759
Steve McKay3abb0e32015-12-01 17:02:42 -0800760 case R.id.menu_copy_to_clipboard:
Steve McKaye11ef242015-12-07 16:31:42 -0800761 if (!selection.isEmpty()) {
762 copySelectionToClipboard(selection);
763 }
Steve McKay3abb0e32015-12-01 17:02:42 -0800764 return true;
Steve McKay1f199482015-05-20 15:58:42 -0700765
Steve McKay3abb0e32015-12-01 17:02:42 -0800766 case R.id.menu_select_all:
767 selectAllFiles();
768 return true;
Ben Kwa512a6ba2015-03-31 08:15:21 -0700769
Steve McKay3abb0e32015-12-01 17:02:42 -0800770 default:
771 if (DEBUG) Log.d(TAG, "Unhandled menu item selected: " + item);
772 return false;
Jeff Sharkeyc317af82013-07-01 16:56:54 -0700773 }
774 }
Steve McKayef280152015-06-11 10:10:49 -0700775 }
Jeff Sharkeyc317af82013-07-01 16:56:54 -0700776
Steve McKayef280152015-06-11 10:10:49 -0700777 private static void cancelThumbnailTask(View view) {
778 final ImageView iconThumb = (ImageView) view.findViewById(R.id.icon_thumb);
779 if (iconThumb != null) {
780 final ThumbnailAsyncTask oldTask = (ThumbnailAsyncTask) iconThumb.getTag();
781 if (oldTask != null) {
782 oldTask.preempt();
783 iconThumb.setTag(null);
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -0700784 }
785 }
Steve McKayef280152015-06-11 10:10:49 -0700786 }
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -0700787
Steve McKayef280152015-06-11 10:10:49 -0700788 private void openDocuments(final Selection selected) {
Steve McKay9276f3b2015-05-27 16:11:42 -0700789 new GetDocumentsTask() {
790 @Override
791 void onDocumentsReady(List<DocumentInfo> docs) {
Steve McKay0fbfc652015-08-20 16:48:49 -0700792 // TODO: Implement support in Files activity for opening multiple docs.
Steve McKay9276f3b2015-05-27 16:11:42 -0700793 BaseActivity.get(DirectoryFragment.this).onDocumentsPicked(docs);
Ben Kwaf527c632015-04-08 15:03:35 -0700794 }
Steve McKay9276f3b2015-05-27 16:11:42 -0700795 }.execute(selected);
Jeff Sharkey873daa32013-08-18 17:38:20 -0700796 }
797
Steve McKayef280152015-06-11 10:10:49 -0700798 private void shareDocuments(final Selection selected) {
Steve McKay9276f3b2015-05-27 16:11:42 -0700799 new GetDocumentsTask() {
800 @Override
801 void onDocumentsReady(List<DocumentInfo> docs) {
802 Intent intent;
803
804 // Filter out directories - those can't be shared.
Steve McKayfefcd702015-08-20 16:19:38 +0000805 List<DocumentInfo> docsForSend = new ArrayList<>();
Steve McKay9276f3b2015-05-27 16:11:42 -0700806 for (DocumentInfo doc: docs) {
807 if (!Document.MIME_TYPE_DIR.equals(doc.mimeType)) {
808 docsForSend.add(doc);
809 }
810 }
811
812 if (docsForSend.size() == 1) {
813 final DocumentInfo doc = docsForSend.get(0);
814
815 intent = new Intent(Intent.ACTION_SEND);
816 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
817 intent.addCategory(Intent.CATEGORY_DEFAULT);
818 intent.setType(doc.mimeType);
819 intent.putExtra(Intent.EXTRA_STREAM, doc.derivedUri);
820
821 } else if (docsForSend.size() > 1) {
822 intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
823 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
824 intent.addCategory(Intent.CATEGORY_DEFAULT);
825
Steve McKayfefcd702015-08-20 16:19:38 +0000826 final ArrayList<String> mimeTypes = new ArrayList<>();
827 final ArrayList<Uri> uris = new ArrayList<>();
Steve McKay9276f3b2015-05-27 16:11:42 -0700828 for (DocumentInfo doc : docsForSend) {
829 mimeTypes.add(doc.mimeType);
830 uris.add(doc.derivedUri);
831 }
832
833 intent.setType(findCommonMimeType(mimeTypes));
834 intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
835
836 } else {
837 return;
838 }
839
840 intent = Intent.createChooser(intent, getActivity().getText(R.string.share_via));
841 startActivity(intent);
842 }
843 }.execute(selected);
844 }
845
Steve McKayef280152015-06-11 10:10:49 -0700846 private void deleteDocuments(final Selection selected) {
Ben Kwa91923182015-08-27 16:06:33 -0700847 Context context = getActivity();
Ben Kwa91923182015-08-27 16:06:33 -0700848 String message = Shared.getQuantityString(context, R.plurals.deleting, selected.size());
Jeff Sharkey873daa32013-08-18 17:38:20 -0700849
Ben Kwada858bf2015-12-09 14:33:49 -0800850 // Hide the files in the UI.
851 final SparseArray<String> toDelete = mAdapter.hide(selected.getAll());
Jeff Sharkey873daa32013-08-18 17:38:20 -0700852
Ben Kwada858bf2015-12-09 14:33:49 -0800853 // Show a snackbar informing the user that files will be deleted, and give them an option to
854 // cancel.
Ben Kwac4693342015-09-30 10:00:10 -0700855 final Activity activity = getActivity();
Steve McKaye9809272015-10-01 11:39:24 -0700856 Snackbars.makeSnackbar(activity, message, Snackbar.LENGTH_LONG)
Ben Kwa91923182015-08-27 16:06:33 -0700857 .setAction(
858 R.string.undo,
Ben Kwa98bb6cf2015-10-14 08:00:27 -0700859 new View.OnClickListener() {
Ben Kwa91923182015-08-27 16:06:33 -0700860 @Override
861 public void onClick(View view) {}
862 })
863 .setCallback(
864 new Snackbar.Callback() {
865 @Override
866 public void onDismissed(Snackbar snackbar, int event) {
867 if (event == Snackbar.Callback.DISMISS_EVENT_ACTION) {
Ben Kwada858bf2015-12-09 14:33:49 -0800868 // If the delete was cancelled, just unhide the files.
869 mAdapter.unhide(toDelete);
Ben Kwa91923182015-08-27 16:06:33 -0700870 } else {
Ben Kwada858bf2015-12-09 14:33:49 -0800871 // Actually kick off the delete.
872 mModel.delete(
873 selected,
Ben Kwac21888e2015-09-30 14:14:16 -0700874 new Model.DeletionListener() {
Ben Kwa83cedf22015-09-11 15:15:45 -0700875 @Override
Ben Kwada858bf2015-12-09 14:33:49 -0800876 public void onError() {
877 Snackbars.makeSnackbar(
878 activity,
879 R.string.toast_failed_delete,
880 Snackbar.LENGTH_LONG)
881 .show();
Ben Kwa83cedf22015-09-11 15:15:45 -0700882
Ben Kwada858bf2015-12-09 14:33:49 -0800883 }
Ben Kwa83cedf22015-09-11 15:15:45 -0700884 });
Ben Kwa91923182015-08-27 16:06:33 -0700885 }
Ben Kwa91923182015-08-27 16:06:33 -0700886 }
887 })
888 .show();
Jeff Sharkey873daa32013-08-18 17:38:20 -0700889 }
890
Steve McKayef280152015-06-11 10:10:49 -0700891 private void transferDocuments(final Selection selected, final int mode) {
Ben Kwaf5858932015-04-07 15:43:39 -0700892 // Pop up a dialog to pick a destination. This is inadequate but works for now.
893 // TODO: Implement a picker that is to spec.
Daichi Hironocaadd412015-04-10 15:50:38 +0900894 final Intent intent = new Intent(
Ben Kwaae967802015-09-25 14:48:29 -0700895 Shared.ACTION_PICK_COPY_DESTINATION,
Daichi Hironocaadd412015-04-10 15:50:38 +0900896 Uri.EMPTY,
897 getActivity(),
898 DocumentsActivity.class);
Steve McKay9276f3b2015-05-27 16:11:42 -0700899
900 new GetDocumentsTask() {
901 @Override
902 void onDocumentsReady(List<DocumentInfo> docs) {
Steve McKayb85f90c2015-11-09 17:38:35 +0900903 getDisplayState().selectedDocumentsForCopy = docs;
Steve McKay9276f3b2015-05-27 16:11:42 -0700904
905 boolean directoryCopy = false;
906 for (DocumentInfo info : docs) {
907 if (Document.MIME_TYPE_DIR.equals(info.mimeType)) {
908 directoryCopy = true;
909 break;
910 }
911 }
Ben Kwaae967802015-09-25 14:48:29 -0700912 intent.putExtra(Shared.EXTRA_DIRECTORY_COPY, directoryCopy);
Steve McKay9276f3b2015-05-27 16:11:42 -0700913 intent.putExtra(CopyService.EXTRA_TRANSFER_MODE, mode);
914 startActivityForResult(intent, REQUEST_COPY_DESTINATION);
Daichi Hironof2a822d2015-04-14 17:12:54 +0900915 }
Steve McKay9276f3b2015-05-27 16:11:42 -0700916 }.execute(selected);
Ben Kwa41b26c12015-03-31 10:11:43 -0700917 }
918
Steve McKayb85f90c2015-11-09 17:38:35 +0900919 private State getDisplayState() {
920 return ((BaseActivity) getActivity()).getDisplayState();
Jeff Sharkey09c10bf2013-06-30 20:02:59 -0700921 }
922
Steve McKayef280152015-06-11 10:10:49 -0700923 // Provide a reference to the views for each data item
924 // Complex data items may need more than one view per item, and
925 // you provide access to all the views for a data item in a view holder
Ben Kwad72a1da2015-12-01 19:56:57 -0800926 final class DocumentHolder
Ben Kwa98bb6cf2015-10-14 08:00:27 -0700927 extends RecyclerView.ViewHolder
928 implements View.OnKeyListener
929 {
Ben Kwa0497da82015-11-30 23:00:02 -0800930 public String modelId;
Ben Kwa98bb6cf2015-10-14 08:00:27 -0700931 private ClickListener mClickListener;
932 private View.OnKeyListener mKeyListener;
933
Steve McKayef280152015-06-11 10:10:49 -0700934 public DocumentHolder(View view) {
935 super(view);
Ben Kwa60d91e42015-10-20 15:02:33 -0700936 view.setOnKeyListener(this);
937 }
938
939 public void setSelected(boolean selected) {
940 itemView.setActivated(selected);
Ben Kwa4e12b162015-12-09 14:48:59 -0800941 itemView.setBackgroundColor(selected ? mSelectedItemColor : mDefaultItemColor);
Ben Kwa98bb6cf2015-10-14 08:00:27 -0700942 }
943
944 @Override
945 public boolean onKey(View v, int keyCode, KeyEvent event) {
946 // Intercept enter key-up events, and treat them as clicks. Forward other events.
947 if (event.getAction() == KeyEvent.ACTION_UP &&
948 keyCode == KeyEvent.KEYCODE_ENTER) {
949 if (mClickListener != null) {
950 mClickListener.onClick(this);
951 }
952 return true;
953 } else if (mKeyListener != null) {
954 return mKeyListener.onKey(v, keyCode, event);
955 }
956 return false;
957 }
958
959 public void addClickListener(ClickListener listener) {
960 // Just handle one for now; switch to a list if necessary.
961 checkState(mClickListener == null);
962 mClickListener = listener;
963 }
964
965 public void addOnKeyListener(View.OnKeyListener listener) {
966 // Just handle one for now; switch to a list if necessary.
967 checkState(mKeyListener == null);
968 mKeyListener = listener;
969 }
Ben Kwa60d91e42015-10-20 15:02:33 -0700970 }
Ben Kwa98bb6cf2015-10-14 08:00:27 -0700971
Ben Kwa60d91e42015-10-20 15:02:33 -0700972 interface ClickListener {
973 public void onClick(DocumentHolder doc);
Steve McKayef280152015-06-11 10:10:49 -0700974 }
975
Ben Kwa2f975262015-09-16 13:15:38 -0700976 void showEmptyView() {
977 mEmptyView.setVisibility(View.VISIBLE);
978 mRecView.setVisibility(View.GONE);
979 TextView msg = (TextView) mEmptyView.findViewById(R.id.message);
980 msg.setText(R.string.empty);
981 // No retry button for the empty view.
982 mEmptyView.findViewById(R.id.button_retry).setVisibility(View.GONE);
983 }
984
985 void showErrorView() {
986 mEmptyView.setVisibility(View.VISIBLE);
987 mRecView.setVisibility(View.GONE);
988 TextView msg = (TextView) mEmptyView.findViewById(R.id.message);
989 msg.setText(R.string.query_error);
990 // TODO: Enable this once the retry button does something.
991 mEmptyView.findViewById(R.id.button_retry).setVisibility(View.GONE);
992 }
993
994 void showRecyclerView() {
995 mEmptyView.setVisibility(View.GONE);
996 mRecView.setVisibility(View.VISIBLE);
997 }
998
Ben Kwad72a1da2015-12-01 19:56:57 -0800999 final class DocumentsAdapter
Ben Kwab8a5e082015-12-07 13:25:27 -08001000 extends RecyclerView.Adapter<DocumentHolder>
1001 implements Model.UpdateListener {
Steve McKayef280152015-06-11 10:10:49 -07001002
Ben Kwad72a1da2015-12-01 19:56:57 -08001003 static private final String TAG = "DocumentsAdapter";
Steve McKayef280152015-06-11 10:10:49 -07001004 private final Context mContext;
Ben Kwad72a1da2015-12-01 19:56:57 -08001005 /**
1006 * Map of model IDs to adapter positions. This is the data structure that determines what
Ben Kwab8a5e082015-12-07 13:25:27 -08001007 * shows up in the UI, and where.
Ben Kwad72a1da2015-12-01 19:56:57 -08001008 */
1009 // TODO(stable-id): need to keep this up-to-date when items are added/removed
1010 private List<String> mModelIds = new ArrayList<>();
Steve McKayef280152015-06-11 10:10:49 -07001011
Steve McKayef280152015-06-11 10:10:49 -07001012 public DocumentsAdapter(Context context) {
1013 mContext = context;
Steve McKayef280152015-06-11 10:10:49 -07001014 }
Jeff Sharkeyac9e6272013-08-31 21:27:44 -07001015
Ben Kwac42fa402015-09-16 08:04:37 -07001016 @Override
Steve McKayef280152015-06-11 10:10:49 -07001017 public DocumentHolder onCreateViewHolder(ViewGroup parent, int viewType) {
Steve McKayb85f90c2015-11-09 17:38:35 +09001018 final State state = getDisplayState();
Steve McKayef280152015-06-11 10:10:49 -07001019 final LayoutInflater inflater = LayoutInflater.from(getContext());
Ben Kwaa017bc12015-10-07 14:15:12 -07001020 View item = null;
Steve McKayef280152015-06-11 10:10:49 -07001021 switch (state.derivedMode) {
1022 case MODE_GRID:
Ben Kwaa017bc12015-10-07 14:15:12 -07001023 item = inflater.inflate(R.layout.item_doc_grid, parent, false);
1024 break;
Steve McKayef280152015-06-11 10:10:49 -07001025 case MODE_LIST:
Ben Kwaa017bc12015-10-07 14:15:12 -07001026 item = inflater.inflate(R.layout.item_doc_list, parent, false);
1027 break;
Steve McKayef280152015-06-11 10:10:49 -07001028 case MODE_UNKNOWN:
1029 default:
1030 throw new IllegalStateException("Unsupported layout mode.");
Jeff Sharkey20b32272013-09-03 15:25:52 -07001031 }
Ben Kwa98bb6cf2015-10-14 08:00:27 -07001032
1033 DocumentHolder holder = new DocumentHolder(item);
1034 holder.addClickListener(mItemClickListener);
1035 holder.addOnKeyListener(mSelectionManager);
1036 return holder;
Jeff Sharkey20b32272013-09-03 15:25:52 -07001037 }
1038
Ben Kwa60d91e42015-10-20 15:02:33 -07001039 /**
1040 * Deal with selection changed events by using a custom ItemAnimator that just changes the
1041 * background color. This works around focus issues (otherwise items lose focus when their
1042 * selection state changes) but also optimizes change animations for selection.
1043 */
1044 @Override
1045 public void onBindViewHolder(DocumentHolder holder, int position, List<Object> payload) {
1046 final View itemView = holder.itemView;
1047
1048 if (payload.contains(MultiSelectManager.SELECTION_CHANGED_MARKER)) {
Ben Kwad72a1da2015-12-01 19:56:57 -08001049 final boolean selected = isSelected(mModelIds.get(position));
Ben Kwa60d91e42015-10-20 15:02:33 -07001050 itemView.setActivated(selected);
1051 return;
1052 } else {
1053 onBindViewHolder(holder, position);
1054 }
1055 }
1056
Steve McKayef280152015-06-11 10:10:49 -07001057 @Override
1058 public void onBindViewHolder(DocumentHolder holder, int position) {
Steve McKayef280152015-06-11 10:10:49 -07001059 final Context context = getContext();
Steve McKayb85f90c2015-11-09 17:38:35 +09001060 final State state = getDisplayState();
Jeff Sharkey873daa32013-08-18 17:38:20 -07001061 final RootsCache roots = DocumentsApplication.getRootsCache(context);
1062 final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache(
1063 context, mThumbSize);
1064
Ben Kwab8a5e082015-12-07 13:25:27 -08001065 holder.modelId = mModelIds.get(position);
1066 final Cursor cursor = mModel.getItem(holder.modelId);
Steve McKayef280152015-06-11 10:10:49 -07001067 checkNotNull(cursor, "Cursor cannot be null.");
Jeff Sharkeyac9e6272013-08-31 21:27:44 -07001068
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -07001069 final String docAuthority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY);
1070 final String docRootId = getCursorString(cursor, RootCursorWrapper.COLUMN_ROOT_ID);
Jeff Sharkeyac9e6272013-08-31 21:27:44 -07001071 final String docId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
1072 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
1073 final String docDisplayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
1074 final long docLastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED);
1075 final int docIcon = getCursorInt(cursor, Document.COLUMN_ICON);
1076 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
1077 final String docSummary = getCursorString(cursor, Document.COLUMN_SUMMARY);
1078 final long docSize = getCursorLong(cursor, Document.COLUMN_SIZE);
Jeff Sharkeye22d02e2013-04-26 16:54:55 -07001079
Ben Kwa60d91e42015-10-20 15:02:33 -07001080 final View itemView = holder.itemView;
1081
Ben Kwab8a5e082015-12-07 13:25:27 -08001082 holder.setSelected(isSelected(holder.modelId));
Jeff Sharkey9656a532013-09-13 13:42:19 -07001083
Steve McKayef280152015-06-11 10:10:49 -07001084 final ImageView iconMime = (ImageView) itemView.findViewById(R.id.icon_mime);
1085 final ImageView iconThumb = (ImageView) itemView.findViewById(R.id.icon_thumb);
1086 final TextView title = (TextView) itemView.findViewById(android.R.id.title);
1087 final ImageView icon1 = (ImageView) itemView.findViewById(android.R.id.icon1);
Steve McKayef280152015-06-11 10:10:49 -07001088 final TextView summary = (TextView) itemView.findViewById(android.R.id.summary);
1089 final TextView date = (TextView) itemView.findViewById(R.id.date);
1090 final TextView size = (TextView) itemView.findViewById(R.id.size);
Jeff Sharkeye22d02e2013-04-26 16:54:55 -07001091
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -07001092 final ThumbnailAsyncTask oldTask = (ThumbnailAsyncTask) iconThumb.getTag();
Jeff Sharkey8a8fb672013-05-07 12:41:33 -07001093 if (oldTask != null) {
Jeff Sharkey753a3ae2013-10-22 17:09:44 -07001094 oldTask.preempt();
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -07001095 iconThumb.setTag(null);
Jeff Sharkey8a8fb672013-05-07 12:41:33 -07001096 }
1097
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -07001098 iconMime.animate().cancel();
1099 iconThumb.animate().cancel();
1100
Jeff Sharkeyaed873d2013-09-09 16:51:06 -07001101 final boolean supportsThumbnail = (docFlags & Document.FLAG_SUPPORTS_THUMBNAIL) != 0;
Jeff Sharkeyd10f0492013-09-09 17:35:46 -07001102 final boolean allowThumbnail = (state.derivedMode == MODE_GRID)
Jeff Sharkey9656a532013-09-13 13:42:19 -07001103 || MimePredicate.mimeMatches(MimePredicate.VISUAL_MIMES, docMimeType);
Jeff Sharkeyf63b7772013-10-01 17:57:41 -07001104 final boolean showThumbnail = supportsThumbnail && allowThumbnail && !mSvelteRecents;
Jeff Sharkeyaed873d2013-09-09 16:51:06 -07001105
Steve McKayb85f90c2015-11-09 17:38:35 +09001106 final boolean enabled = mTuner.isDocumentEnabled(docMimeType, docFlags);
Jeff Sharkey7e544612014-08-29 15:38:27 -07001107 final float iconAlpha = (state.derivedMode == MODE_LIST && !enabled) ? 0.5f : 1f;
1108
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -07001109 boolean cacheHit = false;
Jeff Sharkey9656a532013-09-13 13:42:19 -07001110 if (showThumbnail) {
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -07001111 final Uri uri = DocumentsContract.buildDocumentUri(docAuthority, docId);
Jeff Sharkeyac9e6272013-08-31 21:27:44 -07001112 final Bitmap cachedResult = thumbs.get(uri);
Jeff Sharkey8a8fb672013-05-07 12:41:33 -07001113 if (cachedResult != null) {
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -07001114 iconThumb.setImageBitmap(cachedResult);
1115 cacheHit = true;
Jeff Sharkey8a8fb672013-05-07 12:41:33 -07001116 } else {
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -07001117 iconThumb.setImageDrawable(null);
Steve McKayef280152015-06-11 10:10:49 -07001118 // TODO: Hang this off DocumentHolder?
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -07001119 final ThumbnailAsyncTask task = new ThumbnailAsyncTask(
Jeff Sharkey7e544612014-08-29 15:38:27 -07001120 uri, iconMime, iconThumb, mThumbSize, iconAlpha);
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -07001121 iconThumb.setTag(task);
Jeff Sharkey753a3ae2013-10-22 17:09:44 -07001122 ProviderExecutor.forAuthority(docAuthority).execute(task);
Jeff Sharkey8a8fb672013-05-07 12:41:33 -07001123 }
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -07001124 }
1125
1126 // Always throw MIME icon into place, even when a thumbnail is being
1127 // loaded in background.
1128 if (cacheHit) {
1129 iconMime.setAlpha(0f);
Jeff Sharkey9dd02622013-09-27 16:44:11 -07001130 iconMime.setImageDrawable(null);
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -07001131 iconThumb.setAlpha(1f);
Jeff Sharkeye22d02e2013-04-26 16:54:55 -07001132 } else {
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -07001133 iconMime.setAlpha(1f);
1134 iconThumb.setAlpha(0f);
Jeff Sharkey9dd02622013-09-27 16:44:11 -07001135 iconThumb.setImageDrawable(null);
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001136 iconMime.setImageDrawable(
Steve McKayef280152015-06-11 10:10:49 -07001137 getDocumentIcon(mContext, docAuthority, docId, docMimeType, docIcon, state));
Jeff Sharkeye22d02e2013-04-26 16:54:55 -07001138 }
1139
Ben Kwa65d7c042015-11-10 21:02:47 -08001140 if ((state.derivedMode == MODE_GRID) && mHideGridTitles) {
1141 title.setVisibility(View.GONE);
1142 } else {
Jeff Sharkey9656a532013-09-13 13:42:19 -07001143 title.setText(docDisplayName);
Ben Kwa3e4db1a2015-11-06 11:07:22 -08001144 title.setVisibility(View.VISIBLE);
Jeff Sharkey9656a532013-09-13 13:42:19 -07001145 }
1146
1147 Drawable iconDrawable = null;
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -07001148 if (mType == TYPE_RECENT_OPEN) {
Jeff Sharkey8b997042013-09-19 15:25:56 -07001149 // We've already had to enumerate roots before any results can
1150 // be shown, so this will never block.
1151 final RootInfo root = roots.getRootBlocking(docAuthority, docRootId);
Ben Kwa3e4db1a2015-11-06 11:07:22 -08001152 iconDrawable = root.loadIcon(mContext);
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -07001153
Jeff Sharkey7d58fc62013-09-12 16:25:02 -07001154 if (summary != null) {
1155 final boolean alwaysShowSummary = getResources()
1156 .getBoolean(R.bool.always_show_summary);
1157 if (alwaysShowSummary) {
1158 summary.setText(root.getDirectoryString());
1159 summary.setVisibility(View.VISIBLE);
Jeff Sharkey7d58fc62013-09-12 16:25:02 -07001160 } else {
Jeff Sharkey8b997042013-09-19 15:25:56 -07001161 if (iconDrawable != null && roots.isIconUniqueBlocking(root)) {
Jeff Sharkey7d58fc62013-09-12 16:25:02 -07001162 // No summary needed if icon speaks for itself
1163 summary.setVisibility(View.INVISIBLE);
1164 } else {
1165 summary.setText(root.getDirectoryString());
1166 summary.setVisibility(View.VISIBLE);
1167 summary.setTextAlignment(TextView.TEXT_ALIGNMENT_TEXT_END);
Jeff Sharkey7d58fc62013-09-12 16:25:02 -07001168 }
1169 }
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -07001170 }
Jeff Sharkeyac9e6272013-08-31 21:27:44 -07001171 } else {
Jeff Sharkey9656a532013-09-13 13:42:19 -07001172 // Directories showing thumbnails in grid mode get a little icon
1173 // hint to remind user they're a directory.
1174 if (Document.MIME_TYPE_DIR.equals(docMimeType) && state.derivedMode == MODE_GRID
1175 && showThumbnail) {
Steve McKayef280152015-06-11 10:10:49 -07001176 iconDrawable = IconUtils.applyTintAttr(mContext, R.drawable.ic_doc_folder,
Jeff Sharkey34c54092014-08-08 13:08:56 -07001177 android.R.attr.textColorPrimaryInverse);
Jeff Sharkey9656a532013-09-13 13:42:19 -07001178 }
1179
Jeff Sharkey7d58fc62013-09-12 16:25:02 -07001180 if (summary != null) {
1181 if (docSummary != null) {
1182 summary.setText(docSummary);
1183 summary.setVisibility(View.VISIBLE);
Jeff Sharkey7d58fc62013-09-12 16:25:02 -07001184 } else {
1185 summary.setVisibility(View.INVISIBLE);
1186 }
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -07001187 }
Jeff Sharkey2e694f82013-08-06 16:26:14 -07001188 }
1189
Jeff Sharkey9656a532013-09-13 13:42:19 -07001190 if (iconDrawable != null) {
Ben Kwa3e4db1a2015-11-06 11:07:22 -08001191 icon1.setVisibility(View.VISIBLE);
1192 icon1.setImageDrawable(iconDrawable);
Ben Kwa65d7c042015-11-10 21:02:47 -08001193 } else {
1194 icon1.setVisibility(View.GONE);
Jeff Sharkey9656a532013-09-13 13:42:19 -07001195 }
1196
Jeff Sharkeyac9e6272013-08-31 21:27:44 -07001197 if (docLastModified == -1) {
Jeff Sharkey8a8fb672013-05-07 12:41:33 -07001198 date.setText(null);
1199 } else {
Steve McKayef280152015-06-11 10:10:49 -07001200 date.setText(formatTime(mContext, docLastModified));
Jeff Sharkey8a8fb672013-05-07 12:41:33 -07001201 }
Jeff Sharkey2e694f82013-08-06 16:26:14 -07001202
Ben Kwa65d7c042015-11-10 21:02:47 -08001203 if (!state.showSize || Document.MIME_TYPE_DIR.equals(docMimeType) || docSize == -1) {
Jeff Sharkey2e694f82013-08-06 16:26:14 -07001204 size.setVisibility(View.GONE);
Ben Kwa65d7c042015-11-10 21:02:47 -08001205 } else {
1206 size.setVisibility(View.VISIBLE);
1207 size.setText(Formatter.formatFileSize(mContext, docSize));
Jeff Sharkey7d58fc62013-09-12 16:25:02 -07001208 }
Jeff Sharkey42d26792013-09-06 13:22:09 -07001209
Steve McKayef280152015-06-11 10:10:49 -07001210 setEnabledRecursive(itemView, enabled);
Jeff Sharkey7e544612014-08-29 15:38:27 -07001211
1212 iconMime.setAlpha(iconAlpha);
1213 iconThumb.setAlpha(iconAlpha);
Ben Kwa65d7c042015-11-10 21:02:47 -08001214 icon1.setAlpha(iconAlpha);
Jeff Sharkeyaed873d2013-09-09 16:51:06 -07001215
Steve McKay8e258c62015-05-06 14:27:57 -07001216 if (DEBUG_ENABLE_DND) {
Steve McKayef280152015-06-11 10:10:49 -07001217 setupDragAndDropOnDocumentView(itemView, cursor);
Steve McKay8e258c62015-05-06 14:27:57 -07001218 }
Jeff Sharkeye22d02e2013-04-26 16:54:55 -07001219 }
Jeff Sharkey09c10bf2013-06-30 20:02:59 -07001220
Steve McKay351a7492015-08-04 10:11:01 -07001221 @Override
Steve McKayef280152015-06-11 10:10:49 -07001222 public int getItemCount() {
Ben Kwad72a1da2015-12-01 19:56:57 -08001223 return mModelIds.size();
1224 }
1225
1226 @Override
1227 public void onModelUpdate(Model model) {
1228 // TODO(stable-id): Sort model IDs, categorize by dir/file, etc
Ben Kwab8a5e082015-12-07 13:25:27 -08001229 mModelIds = Lists.newArrayList(model.getModelIds());
Ben Kwad72a1da2015-12-01 19:56:57 -08001230 }
1231
1232 @Override
1233 public void onModelUpdateFailed(Exception e) {
1234 if (DEBUG) Log.d(TAG, "onModelUpdateFailed called ");
1235 mModelIds.clear();
1236 }
1237
1238 /**
1239 * @return The model ID of the item at the given adapter position.
1240 */
1241 public String getModelId(int adapterPosition) {
1242 return mModelIds.get(adapterPosition);
Jeff Sharkeyaed873d2013-09-09 16:51:06 -07001243 }
1244
Ben Kwada858bf2015-12-09 14:33:49 -08001245 /**
1246 * Hides a set of items from the associated RecyclerView.
1247 *
1248 * @param ids The Model IDs of the items to hide.
1249 * @return A SparseArray that maps the hidden IDs to their old positions. This can be used
1250 * to {@link #unhide} the items if necessary.
1251 */
1252 public SparseArray<String> hide(String... ids) {
1253 Set<String> toHide = Sets.newHashSet(ids);
1254
1255 // Proceed backwards through the list of items, because each removal causes the
1256 // positions of all subsequent items to change.
1257 SparseArray<String> hiddenItems = new SparseArray<>();
1258 for (int i = mModelIds.size() - 1; i >= 0; --i) {
1259 String id = mModelIds.get(i);
1260 if (toHide.contains(id)) {
1261 hiddenItems.put(i, mModelIds.remove(i));
1262 notifyItemRemoved(i);
1263 }
1264 }
1265
1266 return hiddenItems;
1267 }
1268
1269 /**
1270 * Unhides a set of previously hidden items.
1271 *
1272 * @param ids A sparse array of IDs from a previous call to {@link #hide}.
1273 */
1274 public void unhide(SparseArray<String> ids) {
1275 // Proceed backwards through the list of items, because each addition causes the
1276 // positions of all subsequent items to change.
1277 for (int i = ids.size() - 1; i >= 0; --i) {
1278 int pos = ids.keyAt(i);
1279 String id = ids.get(pos);
1280 mModelIds.add(pos, id);
1281 notifyItemInserted(pos);
1282 }
1283 }
Ben Kwab8a5e082015-12-07 13:25:27 -08001284
1285 /**
1286 * Returns a list of model IDs of items currently in the adapter. Excludes items that are
1287 * currently hidden (see {@link #hide(String...)}).
1288 *
1289 * @return A list of Model IDs.
1290 */
1291 public List<String> getModelIds() {
1292 return mModelIds;
1293 }
Jeff Sharkey8a8fb672013-05-07 12:41:33 -07001294 }
1295
1296 private static String formatTime(Context context, long when) {
1297 // TODO: DateUtils should make this easier
1298 Time then = new Time();
1299 then.set(when);
1300 Time now = new Time();
1301 now.setToNow();
1302
1303 int flags = DateUtils.FORMAT_NO_NOON | DateUtils.FORMAT_NO_MIDNIGHT
1304 | DateUtils.FORMAT_ABBREV_ALL;
1305
1306 if (then.year != now.year) {
1307 flags |= DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE;
1308 } else if (then.yearDay != now.yearDay) {
1309 flags |= DateUtils.FORMAT_SHOW_DATE;
1310 } else {
1311 flags |= DateUtils.FORMAT_SHOW_TIME;
1312 }
1313
1314 return DateUtils.formatDateTime(context, when, flags);
1315 }
Jeff Sharkeyb3620442013-09-01 18:41:04 -07001316
1317 private String findCommonMimeType(List<String> mimeTypes) {
1318 String[] commonType = mimeTypes.get(0).split("/");
1319 if (commonType.length != 2) {
1320 return "*/*";
1321 }
1322
1323 for (int i = 1; i < mimeTypes.size(); i++) {
1324 String[] type = mimeTypes.get(i).split("/");
1325 if (type.length != 2) continue;
1326
1327 if (!commonType[1].equals(type[1])) {
1328 commonType[1] = "*";
1329 }
1330
1331 if (!commonType[0].equals(type[0])) {
1332 commonType[0] = "*";
1333 commonType[1] = "*";
1334 break;
1335 }
1336 }
1337
1338 return commonType[0] + "/" + commonType[1];
1339 }
Jeff Sharkeyaed873d2013-09-09 16:51:06 -07001340
1341 private void setEnabledRecursive(View v, boolean enabled) {
Jeff Sharkey9656a532013-09-13 13:42:19 -07001342 if (v == null) return;
Jeff Sharkeyaed873d2013-09-09 16:51:06 -07001343 if (v.isEnabled() == enabled) return;
1344 v.setEnabled(enabled);
1345
1346 if (v instanceof ViewGroup) {
1347 final ViewGroup vg = (ViewGroup) v;
1348 for (int i = vg.getChildCount() - 1; i >= 0; i--) {
1349 setEnabledRecursive(vg.getChildAt(i), enabled);
1350 }
1351 }
1352 }
Jeff Sharkey7cf49032013-09-26 10:54:16 -07001353
Steve McKay1f199482015-05-20 15:58:42 -07001354 private void copyFromClipboard() {
1355 new AsyncTask<Void, Void, List<DocumentInfo>>() {
1356
1357 @Override
1358 protected List<DocumentInfo> doInBackground(Void... params) {
1359 return mClipper.getClippedDocuments();
1360 }
1361
1362 @Override
1363 protected void onPostExecute(List<DocumentInfo> docs) {
1364 DocumentInfo destination =
1365 ((BaseActivity) getActivity()).getCurrentDirectory();
1366 copyDocuments(docs, destination);
1367 }
1368 }.execute();
Steve McKay0599a442015-05-05 14:50:00 -07001369 }
1370
Steve McKay1f199482015-05-20 15:58:42 -07001371 private void copyFromClipData(final ClipData clipData, final DocumentInfo destination) {
Steve McKayef280152015-06-11 10:10:49 -07001372 checkNotNull(clipData);
Steve McKay1f199482015-05-20 15:58:42 -07001373 new AsyncTask<Void, Void, List<DocumentInfo>>() {
Vladislav Kaznacheev89b90332015-05-01 13:46:57 -07001374
Steve McKay1f199482015-05-20 15:58:42 -07001375 @Override
1376 protected List<DocumentInfo> doInBackground(Void... params) {
1377 return mClipper.getDocumentsFromClipData(clipData);
1378 }
1379
1380 @Override
1381 protected void onPostExecute(List<DocumentInfo> docs) {
1382 copyDocuments(docs, destination);
1383 }
1384 }.execute();
1385 }
1386
1387 private void copyDocuments(final List<DocumentInfo> docs, final DocumentInfo destination) {
1388 if (!canCopy(docs, destination)) {
Steve McKaye9809272015-10-01 11:39:24 -07001389 Snackbars.makeSnackbar(
Steve McKay1f199482015-05-20 15:58:42 -07001390 getActivity(),
Ben Kwac4693342015-09-30 10:00:10 -07001391 R.string.clipboard_files_cannot_paste,
1392 Snackbar.LENGTH_SHORT)
1393 .show();
Steve McKay0599a442015-05-05 14:50:00 -07001394 return;
1395 }
1396
Steve McKay1f199482015-05-20 15:58:42 -07001397 if (docs.isEmpty()) {
Vladislav Kaznacheev89b90332015-05-01 13:46:57 -07001398 return;
Vladislav Kaznacheeve16887a2015-05-05 12:09:47 -07001399 }
Vladislav Kaznacheev89b90332015-05-01 13:46:57 -07001400
Steve McKayb85f90c2015-11-09 17:38:35 +09001401 final DocumentStack curStack = getDisplayState().stack;
Vladislav Kaznacheev89b90332015-05-01 13:46:57 -07001402 DocumentStack tmpStack = new DocumentStack();
Steve McKay1f199482015-05-20 15:58:42 -07001403 if (destination != null) {
1404 tmpStack.push(destination);
Vladislav Kaznacheev89b90332015-05-01 13:46:57 -07001405 tmpStack.addAll(curStack);
1406 } else {
1407 tmpStack = curStack;
1408 }
1409
Steve McKay1f199482015-05-20 15:58:42 -07001410 CopyService.start(getActivity(), docs, tmpStack, CopyService.TRANSFER_MODE_COPY);
Vladislav Kaznacheev89b90332015-05-01 13:46:57 -07001411 }
1412
1413 private ClipData getClipDataFromDocuments(List<DocumentInfo> docs) {
1414 Context context = getActivity();
1415 final ContentResolver resolver = context.getContentResolver();
1416 ClipData clipData = null;
1417 for (DocumentInfo doc : docs) {
1418 final Uri uri = DocumentsContract.buildDocumentUri(doc.authority, doc.documentId);
1419 if (clipData == null) {
Vladislav Kaznacheeve16887a2015-05-05 12:09:47 -07001420 // TODO: figure out what this string should be.
1421 // Currently it is not displayed anywhere in the UI, but this might change.
1422 final String label = "";
1423 clipData = ClipData.newUri(resolver, label, uri);
Vladislav Kaznacheev89b90332015-05-01 13:46:57 -07001424 } else {
1425 // TODO: update list of mime types in ClipData.
1426 clipData.addItem(new ClipData.Item(uri));
1427 }
1428 }
1429 return clipData;
1430 }
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001431
Steve McKayf68210e2015-11-03 15:23:16 -08001432 public void copySelectedToClipboard() {
Steve McKaye11ef242015-12-07 16:31:42 -08001433 Selection selection = mSelectionManager.getSelection(new Selection());
1434 if (!selection.isEmpty()) {
1435 copySelectionToClipboard(selection);
1436 }
Steve McKay9276f3b2015-05-27 16:11:42 -07001437 }
Steve McKay0599a442015-05-05 14:50:00 -07001438
Steve McKaye11ef242015-12-07 16:31:42 -08001439 void copySelectionToClipboard(Selection selection) {
1440 checkArgument(!selection.isEmpty());
Steve McKay9276f3b2015-05-27 16:11:42 -07001441 new GetDocumentsTask() {
1442 @Override
1443 void onDocumentsReady(List<DocumentInfo> docs) {
1444 mClipper.clipDocuments(docs);
Steve McKay1f199482015-05-20 15:58:42 -07001445 Activity activity = getActivity();
Steve McKaye9809272015-10-01 11:39:24 -07001446 Snackbars.makeSnackbar(activity,
Steve McKay1f199482015-05-20 15:58:42 -07001447 activity.getResources().getQuantityString(
1448 R.plurals.clipboard_files_clipped, docs.size(), docs.size()),
Ben Kwab8a5e082015-12-07 13:25:27 -08001449 Snackbar.LENGTH_SHORT).show();
Steve McKay9276f3b2015-05-27 16:11:42 -07001450 }
Steve McKaye11ef242015-12-07 16:31:42 -08001451 }.execute(selection);
Steve McKay0599a442015-05-05 14:50:00 -07001452 }
1453
Steve McKayf68210e2015-11-03 15:23:16 -08001454 public void pasteFromClipboard() {
Steve McKay1f199482015-05-20 15:58:42 -07001455 copyFromClipboard();
1456 getActivity().invalidateOptionsMenu();
Steve McKay0599a442015-05-05 14:50:00 -07001457 }
1458
Steve McKay0599a442015-05-05 14:50:00 -07001459 /**
1460 * Returns true if the list of files can be copied to destination. Note that this
1461 * is a policy check only. Currently the method does not attempt to verify
1462 * available space or any other environmental aspects possibly resulting in
1463 * failure to copy.
1464 *
1465 * @return true if the list of files can be copied to destination.
1466 */
1467 boolean canCopy(List<DocumentInfo> files, DocumentInfo dest) {
Ben Kwa91923182015-08-27 16:06:33 -07001468 BaseActivity activity = (BaseActivity) getActivity();
Steve McKay0599a442015-05-05 14:50:00 -07001469
1470 final RootInfo root = activity.getCurrentRoot();
1471
1472 // Can't copy folders to Downloads.
1473 if (root.isDownloads()) {
1474 for (DocumentInfo docs : files) {
1475 if (docs.isDirectory()) {
1476 return false;
1477 }
1478 }
1479 }
1480
1481 return dest != null && dest.isDirectory() && dest.isCreateSupported();
1482 }
1483
Steve McKayf68210e2015-11-03 15:23:16 -08001484 public void selectAllFiles() {
Ben Kwab8a5e082015-12-07 13:25:27 -08001485 // Only select things currently visible in the adapter.
1486 boolean changed = mSelectionManager.setItemsSelected(mAdapter.getModelIds(), true);
Steve McKay9459a7c2015-07-24 13:14:20 -07001487 if (changed) {
1488 updateDisplayState();
1489 }
Steve McKay0599a442015-05-05 14:50:00 -07001490 }
1491
Steve McKayef280152015-06-11 10:10:49 -07001492 private void setupDragAndDropOnDirectoryView(View view) {
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001493 // Listen for drops on non-directory items and empty space.
1494 view.setOnDragListener(mOnDragListener);
1495 }
1496
1497 private void setupDragAndDropOnDocumentView(View view, Cursor cursor) {
1498 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
1499 if (Document.MIME_TYPE_DIR.equals(docMimeType)) {
1500 // Make a directory item a drop target. Drop on non-directories and empty space
1501 // is handled at the list/grid view level.
1502 view.setOnDragListener(mOnDragListener);
1503 }
1504
1505 // Temporary: attaching the listener to the title only.
1506 // Attaching to the entire item conflicts with the item long click handler responsible
1507 // for item selection.
1508 final View title = view.findViewById(android.R.id.title);
1509 title.setOnLongClickListener(mLongClickListener);
1510 }
1511
1512 private View.OnDragListener mOnDragListener = new View.OnDragListener() {
1513 @Override
1514 public boolean onDrag(View v, DragEvent event) {
1515 switch (event.getAction()) {
1516 case DragEvent.ACTION_DRAG_STARTED:
1517 // TODO: Check if the event contains droppable data.
1518 return true;
1519
1520 // TODO: Highlight potential drop target directory?
1521 // TODO: Expand drop target directory on hover?
1522 case DragEvent.ACTION_DRAG_ENTERED:
1523 case DragEvent.ACTION_DRAG_LOCATION:
1524 case DragEvent.ACTION_DRAG_EXITED:
1525 case DragEvent.ACTION_DRAG_ENDED:
1526 return true;
1527
1528 case DragEvent.ACTION_DROP:
Ben Kwa0497da82015-11-30 23:00:02 -08001529 String dstId = getModelId(v);
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001530 DocumentInfo dstDir = null;
Ben Kwa0497da82015-11-30 23:00:02 -08001531 if (dstId != null) {
1532 Cursor dstCursor = mModel.getItem(dstId);
Steve McKayef280152015-06-11 10:10:49 -07001533 checkNotNull(dstCursor, "Cursor cannot be null.");
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001534 dstDir = DocumentInfo.fromDirectoryCursor(dstCursor);
1535 // TODO: Do not drop into the directory where the documents came from.
1536 }
1537 copyFromClipData(event.getClipData(), dstDir);
1538 return true;
1539 }
1540 return false;
1541 }
1542 };
1543
Ben Kwa0497da82015-11-30 23:00:02 -08001544 /**
1545 * Gets the model ID for a given motion event (using the event position)
1546 */
1547 private String getModelId(MotionEvent e) {
1548 View view = mRecView.findChildViewUnder(e.getX(), e.getY());
1549 if (view == null) {
1550 return null;
1551 }
1552 RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(view);
1553 if (vh instanceof DocumentHolder) {
1554 return ((DocumentHolder) vh).modelId;
1555 } else {
1556 return null;
1557 }
1558 }
1559
1560 /**
1561 * Gets the model ID for a given RecyclerView item.
1562 * @param view A View that is a document item view, or a child of a document item view.
1563 * @return The Model ID for the given document, or null if the given view is not associated with
1564 * a document item view.
1565 */
1566 private String getModelId(View view) {
Vladislav Kaznacheev9400b892015-09-04 09:17:37 -07001567 while (true) {
1568 if (view.getLayoutParams() instanceof RecyclerView.LayoutParams) {
Ben Kwa0497da82015-11-30 23:00:02 -08001569 RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(view);
1570 if (vh instanceof DocumentHolder) {
1571 return ((DocumentHolder) vh).modelId;
1572 } else {
1573 return null;
1574 }
Vladislav Kaznacheev9400b892015-09-04 09:17:37 -07001575 }
1576 ViewParent parent = view.getParent();
1577 if (parent == null || !(parent instanceof View)) {
1578 return null;
1579 }
1580 view = (View) parent;
1581 }
1582 }
1583
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001584 private View.OnLongClickListener mLongClickListener = new View.OnLongClickListener() {
1585 @Override
1586 public boolean onLongClick(View v) {
1587 final List<DocumentInfo> docs = getDraggableDocuments(v);
1588 if (docs.isEmpty()) {
1589 return false;
1590 }
1591 v.startDrag(
1592 getClipDataFromDocuments(docs),
1593 new DrawableShadowBuilder(getDragShadowIcon(docs)),
1594 null,
Vladislav Kaznacheeve3ce8a92015-07-15 18:04:04 -07001595 View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ |
1596 View.DRAG_FLAG_GLOBAL_URI_WRITE
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001597 );
1598 return true;
1599 }
1600 };
1601
1602 private List<DocumentInfo> getDraggableDocuments(View currentItemView) {
Ben Kwa0497da82015-11-30 23:00:02 -08001603 String modelId = getModelId(currentItemView);
1604 if (modelId == null) {
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001605 return Collections.EMPTY_LIST;
1606 }
1607
Ben Kwaa017bc12015-10-07 14:15:12 -07001608 final List<DocumentInfo> selectedDocs =
1609 mModel.getDocuments(mSelectionManager.getSelection());
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001610 if (!selectedDocs.isEmpty()) {
Ben Kwa0497da82015-11-30 23:00:02 -08001611 if (!isSelected(modelId)) {
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001612 // There is a selection that does not include the current item, drag nothing.
1613 return Collections.EMPTY_LIST;
1614 }
1615 return selectedDocs;
1616 }
1617
Ben Kwa0497da82015-11-30 23:00:02 -08001618 final Cursor cursor = mModel.getItem(modelId);
Steve McKayef280152015-06-11 10:10:49 -07001619 checkNotNull(cursor, "Cursor cannot be null.");
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001620 final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
Steve McKayfefcd702015-08-20 16:19:38 +00001621
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001622 return Lists.newArrayList(doc);
1623 }
1624
1625 private Drawable getDragShadowIcon(List<DocumentInfo> docs) {
1626 if (docs.size() == 1) {
1627 final DocumentInfo doc = docs.get(0);
1628 return getDocumentIcon(getActivity(), doc.authority, doc.documentId,
Steve McKayb85f90c2015-11-09 17:38:35 +09001629 doc.mimeType, doc.icon, getDisplayState());
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001630 }
1631 return getActivity().getDrawable(R.drawable.ic_doc_generic);
1632 }
1633
1634 public static Drawable getDocumentIcon(Context context, String docAuthority, String docId,
1635 String docMimeType, int docIcon, State state) {
1636 if (docIcon != 0) {
1637 return IconUtils.loadPackageIcon(context, docAuthority, docIcon);
1638 } else {
1639 return IconUtils.loadMimeIcon(context, docMimeType, docAuthority, docId,
1640 state.derivedMode);
1641 }
1642 }
1643
Steve McKayef280152015-06-11 10:10:49 -07001644 private static class ThumbnailAsyncTask extends AsyncTask<Uri, Void, Bitmap>
1645 implements Preemptable {
1646 private final Uri mUri;
1647 private final ImageView mIconMime;
1648 private final ImageView mIconThumb;
1649 private final Point mThumbSize;
1650 private final float mTargetAlpha;
1651 private final CancellationSignal mSignal;
1652
1653 public ThumbnailAsyncTask(Uri uri, ImageView iconMime, ImageView iconThumb, Point thumbSize,
1654 float targetAlpha) {
1655 mUri = uri;
1656 mIconMime = iconMime;
1657 mIconThumb = iconThumb;
1658 mThumbSize = thumbSize;
1659 mTargetAlpha = targetAlpha;
1660 mSignal = new CancellationSignal();
1661 }
1662
1663 @Override
1664 public void preempt() {
1665 cancel(false);
1666 mSignal.cancel();
1667 }
1668
1669 @Override
1670 protected Bitmap doInBackground(Uri... params) {
1671 if (isCancelled()) return null;
1672
1673 final Context context = mIconThumb.getContext();
1674 final ContentResolver resolver = context.getContentResolver();
1675
1676 ContentProviderClient client = null;
1677 Bitmap result = null;
1678 try {
1679 client = DocumentsApplication.acquireUnstableProviderOrThrow(
1680 resolver, mUri.getAuthority());
1681 result = DocumentsContract.getDocumentThumbnail(client, mUri, mThumbSize, mSignal);
1682 if (result != null) {
1683 final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache(
1684 context, mThumbSize);
1685 thumbs.put(mUri, result);
1686 }
1687 } catch (Exception e) {
1688 if (!(e instanceof OperationCanceledException)) {
1689 Log.w(TAG, "Failed to load thumbnail for " + mUri + ": " + e);
1690 }
1691 } finally {
1692 ContentProviderClient.releaseQuietly(client);
1693 }
1694 return result;
1695 }
1696
1697 @Override
1698 protected void onPostExecute(Bitmap result) {
1699 if (mIconThumb.getTag() == this && result != null) {
1700 mIconThumb.setTag(null);
1701 mIconThumb.setImageBitmap(result);
1702
1703 mIconMime.setAlpha(mTargetAlpha);
1704 mIconMime.animate().alpha(0f).start();
1705 mIconThumb.setAlpha(0f);
1706 mIconThumb.animate().alpha(mTargetAlpha).start();
1707 }
1708 }
1709 }
1710
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001711 private class DrawableShadowBuilder extends View.DragShadowBuilder {
1712
1713 private final Drawable mShadow;
1714
1715 private final int mShadowDimension;
1716
1717 public DrawableShadowBuilder(Drawable shadow) {
1718 mShadow = shadow;
1719 mShadowDimension = getResources().getDimensionPixelSize(
1720 R.dimen.drag_shadow_size);
1721 mShadow.setBounds(0, 0, mShadowDimension, mShadowDimension);
1722 }
1723
Ben Kwa24be5d32015-08-27 16:04:46 -07001724 @Override
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001725 public void onProvideShadowMetrics(
1726 Point shadowSize, Point shadowTouchPoint) {
1727 shadowSize.set(mShadowDimension, mShadowDimension);
1728 shadowTouchPoint.set(mShadowDimension / 2, mShadowDimension / 2);
1729 }
1730
Ben Kwa24be5d32015-08-27 16:04:46 -07001731 @Override
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001732 public void onDrawShadow(Canvas canvas) {
1733 mShadow.draw(canvas);
1734 }
1735 }
Steve McKay1f199482015-05-20 15:58:42 -07001736
Steve McKay1f199482015-05-20 15:58:42 -07001737 /**
Steve McKay9276f3b2015-05-27 16:11:42 -07001738 * Abstract task providing support for loading documents *off*
1739 * the main thread. And if it isn't obvious, creating a list
1740 * of documents (especially large lists) can be pretty expensive.
1741 */
1742 private abstract class GetDocumentsTask
Steve McKayef280152015-06-11 10:10:49 -07001743 extends AsyncTask<Selection, Void, List<DocumentInfo>> {
Steve McKay9276f3b2015-05-27 16:11:42 -07001744 @Override
Steve McKayef280152015-06-11 10:10:49 -07001745 protected final List<DocumentInfo> doInBackground(Selection... selected) {
Ben Kwa24be5d32015-08-27 16:04:46 -07001746 return mModel.getDocuments(selected[0]);
Steve McKay9276f3b2015-05-27 16:11:42 -07001747 }
1748
1749 @Override
1750 protected final void onPostExecute(List<DocumentInfo> docs) {
1751 onDocumentsReady(docs);
1752 }
1753
1754 abstract void onDocumentsReady(List<DocumentInfo> docs);
1755 }
1756
Ben Kwa0497da82015-11-30 23:00:02 -08001757 boolean isSelected(String modelId) {
Ben Kwad72a1da2015-12-01 19:56:57 -08001758 return mSelectionManager.getSelection().contains(modelId);
Ben Kwa379e1762015-09-21 10:49:52 -07001759 }
Ben Kwa7461a952015-09-01 11:03:01 -07001760
Ben Kwa60d91e42015-10-20 15:02:33 -07001761 private class ItemClickListener implements ClickListener {
Ben Kwa98bb6cf2015-10-14 08:00:27 -07001762 @Override
1763 public void onClick(DocumentHolder doc) {
Ben Kwa98bb6cf2015-10-14 08:00:27 -07001764 if (mSelectionManager.hasSelection()) {
Ben Kwad72a1da2015-12-01 19:56:57 -08001765 mSelectionManager.toggleSelection(doc.modelId);
1766 mSelectionManager.setSelectionRangeBegin(doc.getAdapterPosition());
Ben Kwa98bb6cf2015-10-14 08:00:27 -07001767 } else {
Ben Kwa0497da82015-11-30 23:00:02 -08001768 handleViewItem(doc.modelId);
Ben Kwa98bb6cf2015-10-14 08:00:27 -07001769 }
1770 }
1771 }
1772
Ben Kwad72a1da2015-12-01 19:56:57 -08001773 private class ModelUpdateListener implements Model.UpdateListener {
Ben Kwa379e1762015-09-21 10:49:52 -07001774 @Override
1775 public void onModelUpdate(Model model) {
1776 if (model.info != null || model.error != null) {
1777 mMessageBar.setInfo(model.info);
1778 mMessageBar.setError(model.error);
1779 mMessageBar.show();
1780 }
Ben Kwa7461a952015-09-01 11:03:01 -07001781
Ben Kwa379e1762015-09-21 10:49:52 -07001782 mProgressBar.setVisibility(model.isLoading() ? View.VISIBLE : View.GONE);
1783
1784 if (model.isEmpty()) {
Ben Kwa2f975262015-09-16 13:15:38 -07001785 showEmptyView();
Ben Kwa379e1762015-09-21 10:49:52 -07001786 } else {
Ben Kwa2f975262015-09-16 13:15:38 -07001787 showRecyclerView();
1788 mAdapter.notifyDataSetChanged();
Ben Kwa379e1762015-09-21 10:49:52 -07001789 }
Ben Kwa379e1762015-09-21 10:49:52 -07001790 }
1791
1792 @Override
1793 public void onModelUpdateFailed(Exception e) {
Ben Kwa2f975262015-09-16 13:15:38 -07001794 showErrorView();
Ben Kwa7461a952015-09-01 11:03:01 -07001795 }
Ben Kwa24be5d32015-08-27 16:04:46 -07001796 }
Jeff Sharkeye22d02e2013-04-26 16:54:55 -07001797}