blob: 93921dd514904342645e993a794ef942c503140b [file] [log] [blame]
Jeff Sharkeye22d02e2013-04-26 16:54:55 -07001/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.documentsui;
18
Jeff Sharkey311a7d82015-04-11 21:27:21 -070019import static com.android.documentsui.BaseActivity.State.ACTION_BROWSE;
20import static com.android.documentsui.BaseActivity.State.ACTION_BROWSE_ALL;
Steve McKayd0a2a2c2015-03-25 14:35:33 -070021import static com.android.documentsui.BaseActivity.State.ACTION_CREATE;
22import static com.android.documentsui.BaseActivity.State.ACTION_MANAGE;
23import static com.android.documentsui.BaseActivity.State.MODE_GRID;
24import static com.android.documentsui.BaseActivity.State.MODE_LIST;
25import static com.android.documentsui.BaseActivity.State.MODE_UNKNOWN;
26import static com.android.documentsui.BaseActivity.State.SORT_ORDER_UNKNOWN;
Steve McKayfefcd702015-08-20 16:19:38 +000027import static com.android.documentsui.Shared.TAG;
Jeff Sharkeyac9e6272013-08-31 21:27:44 -070028import static com.android.documentsui.model.DocumentInfo.getCursorInt;
29import static com.android.documentsui.model.DocumentInfo.getCursorLong;
30import static com.android.documentsui.model.DocumentInfo.getCursorString;
Steve McKayef280152015-06-11 10:10:49 -070031import static com.android.internal.util.Preconditions.checkNotNull;
Steve McKayd57f5fa2015-07-23 16:33:41 -070032import static com.android.internal.util.Preconditions.checkState;
Steve McKay0599a442015-05-05 14:50:00 -070033
Ben Kwaf5858932015-04-07 15:43:39 -070034import android.app.Activity;
Jeff Sharkeyf63b7772013-10-01 17:57:41 -070035import android.app.ActivityManager;
Jeff Sharkey09c10bf2013-06-30 20:02:59 -070036import android.app.Fragment;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070037import android.app.FragmentManager;
38import android.app.FragmentTransaction;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070039import android.app.LoaderManager.LoaderCallbacks;
Vladislav Kaznacheev89b90332015-05-01 13:46:57 -070040import android.content.ClipData;
Jeff Sharkey3fd11772013-09-30 14:26:27 -070041import android.content.ContentProviderClient;
Jeff Sharkey873daa32013-08-18 17:38:20 -070042import android.content.ContentResolver;
Jeff Sharkeyd10f0492013-09-09 17:35:46 -070043import android.content.ContentValues;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070044import android.content.Context;
Jeff Sharkey873daa32013-08-18 17:38:20 -070045import android.content.Intent;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070046import android.content.Loader;
Jeff Sharkey083d7e12014-07-27 21:01:45 -070047import android.content.res.Resources;
Jeff Sharkeyac9e6272013-08-31 21:27:44 -070048import android.database.Cursor;
Jeff Sharkey8a8fb672013-05-07 12:41:33 -070049import android.graphics.Bitmap;
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -070050import android.graphics.Canvas;
Jeff Sharkey8a8fb672013-05-07 12:41:33 -070051import android.graphics.Point;
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -070052import android.graphics.drawable.Drawable;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070053import android.net.Uri;
Jeff Sharkey8a8fb672013-05-07 12:41:33 -070054import android.os.AsyncTask;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070055import android.os.Bundle;
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -070056import android.os.CancellationSignal;
Makoto Onuki77778752015-07-01 14:55:14 -070057import android.os.Handler;
58import android.os.Looper;
Jeff Sharkeye39a89b2013-10-29 11:56:37 -070059import android.os.OperationCanceledException;
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -070060import android.os.Parcelable;
Steve McKay8e258c62015-05-06 14:27:57 -070061import android.os.SystemProperties;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070062import android.provider.DocumentsContract;
Jeff Sharkeyac9e6272013-08-31 21:27:44 -070063import android.provider.DocumentsContract.Document;
Ben Kwa24be5d32015-08-27 16:04:46 -070064import android.support.annotation.Nullable;
Ben Kwa7461a952015-09-01 11:03:01 -070065import android.support.annotation.VisibleForTesting;
Ben Kwa91923182015-08-27 16:06:33 -070066import android.support.design.widget.Snackbar;
Steve McKayef280152015-06-11 10:10:49 -070067import android.support.v7.widget.GridLayoutManager;
68import android.support.v7.widget.LinearLayoutManager;
69import android.support.v7.widget.RecyclerView;
70import android.support.v7.widget.RecyclerView.LayoutManager;
Steve McKayef280152015-06-11 10:10:49 -070071import android.support.v7.widget.RecyclerView.RecyclerListener;
72import android.support.v7.widget.RecyclerView.ViewHolder;
Jeff Sharkey6d579272015-06-11 09:16:19 -070073import android.text.TextUtils;
Jeff Sharkey09c10bf2013-06-30 20:02:59 -070074import android.text.format.DateUtils;
Jeff Sharkey2e694f82013-08-06 16:26:14 -070075import android.text.format.Formatter;
Jeff Sharkey8a8fb672013-05-07 12:41:33 -070076import android.text.format.Time;
77import android.util.Log;
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -070078import android.util.SparseArray;
Ben Kwa91923182015-08-27 16:06:33 -070079import android.util.SparseBooleanArray;
Jeff Sharkeyc317af82013-07-01 16:56:54 -070080import android.view.ActionMode;
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -070081import android.view.DragEvent;
Steve McKayef280152015-06-11 10:10:49 -070082import android.view.GestureDetector;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070083import android.view.LayoutInflater;
Jeff Sharkey09c10bf2013-06-30 20:02:59 -070084import android.view.Menu;
Jeff Sharkey09c10bf2013-06-30 20:02:59 -070085import android.view.MenuItem;
Steve McKayef280152015-06-11 10:10:49 -070086import android.view.MotionEvent;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070087import android.view.View;
Steve McKayd57f5fa2015-07-23 16:33:41 -070088import android.view.View.OnLayoutChangeListener;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070089import android.view.ViewGroup;
Vladislav Kaznacheev9400b892015-09-04 09:17:37 -070090import android.view.ViewParent;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070091import android.widget.ImageView;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070092import android.widget.TextView;
Jeff Sharkey873daa32013-08-18 17:38:20 -070093import android.widget.Toast;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -070094
Steve McKay351a7492015-08-04 10:11:01 -070095import com.android.documentsui.BaseActivity.DocumentContext;
Steve McKayd0a2a2c2015-03-25 14:35:33 -070096import com.android.documentsui.BaseActivity.State;
Steve McKayef280152015-06-11 10:10:49 -070097import com.android.documentsui.MultiSelectManager.Selection;
Jeff Sharkey753a3ae2013-10-22 17:09:44 -070098import com.android.documentsui.ProviderExecutor.Preemptable;
Jeff Sharkeyd10f0492013-09-09 17:35:46 -070099import com.android.documentsui.RecentsProvider.StateColumns;
Jeff Sharkey724deeb2013-08-31 15:02:20 -0700100import com.android.documentsui.model.DocumentInfo;
Tomasz Mikolajewski9452c442015-04-14 16:32:41 +0900101import com.android.documentsui.model.DocumentStack;
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700102import com.android.documentsui.model.RootInfo;
Steve McKay1f199482015-05-20 15:58:42 -0700103import com.android.internal.util.Preconditions;
Steve McKayfefcd702015-08-20 16:19:38 +0000104import com.google.common.collect.Lists;
Jeff Sharkeyc317af82013-07-01 16:56:54 -0700105
106import java.util.ArrayList;
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -0700107import java.util.Collections;
Jeff Sharkeya5defe32013-08-05 17:56:48 -0700108import java.util.List;
Jeff Sharkey09c10bf2013-06-30 20:02:59 -0700109
110/**
111 * Display the documents inside a single directory.
112 */
113public class DirectoryFragment extends Fragment {
114
Jeff Sharkeya5defe32013-08-05 17:56:48 -0700115 public static final int TYPE_NORMAL = 1;
116 public static final int TYPE_SEARCH = 2;
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700117 public static final int TYPE_RECENT_OPEN = 3;
Jeff Sharkey5b535922013-08-02 15:55:26 -0700118
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700119 public static final int ANIM_NONE = 1;
120 public static final int ANIM_SIDE = 2;
121 public static final int ANIM_DOWN = 3;
122 public static final int ANIM_UP = 4;
123
Ben Kwaf5858932015-04-07 15:43:39 -0700124 public static final int REQUEST_COPY_DESTINATION = 1;
125
Steve McKayef280152015-06-11 10:10:49 -0700126 private static final int LOADER_ID = 42;
127 private static final boolean DEBUG = false;
Steve McKay8e258c62015-05-06 14:27:57 -0700128 private static final boolean DEBUG_ENABLE_DND = false;
129
Jeff Sharkey2e694f82013-08-06 16:26:14 -0700130 private static final String EXTRA_TYPE = "type";
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700131 private static final String EXTRA_ROOT = "root";
132 private static final String EXTRA_DOC = "doc";
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700133 private static final String EXTRA_QUERY = "query";
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700134 private static final String EXTRA_IGNORE_STATE = "ignoreState";
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700135
Ben Kwa7461a952015-09-01 11:03:01 -0700136 private Model mModel;
Ben Kwa24be5d32015-08-27 16:04:46 -0700137
Steve McKayef280152015-06-11 10:10:49 -0700138 private final Handler mHandler = new Handler(Looper.getMainLooper());
Jeff Sharkey2e694f82013-08-06 16:26:14 -0700139
Steve McKayef280152015-06-11 10:10:49 -0700140 private View mEmptyView;
141 private RecyclerView mRecView;
142
143 private int mType = TYPE_NORMAL;
144 private String mStateKey;
145
146 private int mLastMode = MODE_UNKNOWN;
147 private int mLastSortOrder = SORT_ORDER_UNKNOWN;
148 private boolean mLastShowSize;
149 private boolean mHideGridTitles;
150 private boolean mSvelteRecents;
151 private Point mThumbSize;
152 private DocumentsAdapter mAdapter;
153 private LoaderCallbacks<DirectoryResult> mCallbacks;
Steve McKay1f199482015-05-20 15:58:42 -0700154 private FragmentTuner mFragmentTuner;
155 private DocumentClipper mClipper;
Steve McKayef280152015-06-11 10:10:49 -0700156 // These are lazily initialized.
Steve McKayd57f5fa2015-07-23 16:33:41 -0700157 private LinearLayoutManager mListLayout;
158 private GridLayoutManager mGridLayout;
Steve McKayd57f5fa2015-07-23 16:33:41 -0700159 private int mColumnCount = 1; // This will get updated when layout changes.
Steve McKay1f199482015-05-20 15:58:42 -0700160
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700161 public static void showNormal(FragmentManager fm, RootInfo root, DocumentInfo doc, int anim) {
162 show(fm, TYPE_NORMAL, root, doc, null, anim);
Jeff Sharkey2e694f82013-08-06 16:26:14 -0700163 }
164
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700165 public static void showSearch(FragmentManager fm, RootInfo root, String query, int anim) {
166 show(fm, TYPE_SEARCH, root, null, query, anim);
Jeff Sharkey2e694f82013-08-06 16:26:14 -0700167 }
168
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700169 public static void showRecentsOpen(FragmentManager fm, int anim) {
170 show(fm, TYPE_RECENT_OPEN, null, null, null, anim);
Jeff Sharkey2e694f82013-08-06 16:26:14 -0700171 }
172
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700173 private static void show(FragmentManager fm, int type, RootInfo root, DocumentInfo doc,
174 String query, int anim) {
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700175 final Bundle args = new Bundle();
Jeff Sharkey2e694f82013-08-06 16:26:14 -0700176 args.putInt(EXTRA_TYPE, type);
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700177 args.putParcelable(EXTRA_ROOT, root);
178 args.putParcelable(EXTRA_DOC, doc);
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700179 args.putString(EXTRA_QUERY, query);
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700180
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700181 final FragmentTransaction ft = fm.beginTransaction();
182 switch (anim) {
183 case ANIM_SIDE:
184 args.putBoolean(EXTRA_IGNORE_STATE, true);
185 break;
186 case ANIM_DOWN:
187 args.putBoolean(EXTRA_IGNORE_STATE, true);
188 ft.setCustomAnimations(R.animator.dir_down, R.animator.dir_frozen);
189 break;
190 case ANIM_UP:
191 ft.setCustomAnimations(R.animator.dir_frozen, R.animator.dir_up);
192 break;
193 }
194
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700195 final DirectoryFragment fragment = new DirectoryFragment();
196 fragment.setArguments(args);
197
Jeff Sharkey76112212013-08-06 11:26:10 -0700198 ft.replace(R.id.container_directory, fragment);
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700199 ft.commitAllowingStateLoss();
200 }
201
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700202 private static String buildStateKey(RootInfo root, DocumentInfo doc) {
203 final StringBuilder builder = new StringBuilder();
204 builder.append(root != null ? root.authority : "null").append(';');
205 builder.append(root != null ? root.rootId : "null").append(';');
206 builder.append(doc != null ? doc.documentId : "null");
207 return builder.toString();
208 }
209
Jeff Sharkeya5defe32013-08-05 17:56:48 -0700210 public static DirectoryFragment get(FragmentManager fm) {
211 // TODO: deal with multiple directories shown at once
Jeff Sharkey76112212013-08-06 11:26:10 -0700212 return (DirectoryFragment) fm.findFragmentById(R.id.container_directory);
Jeff Sharkeya5defe32013-08-05 17:56:48 -0700213 }
214
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700215 @Override
216 public View onCreateView(
217 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
218 final Context context = inflater.getContext();
Jeff Sharkey083d7e12014-07-27 21:01:45 -0700219 final Resources res = context.getResources();
Jeff Sharkey09c10bf2013-06-30 20:02:59 -0700220 final View view = inflater.inflate(R.layout.fragment_directory, container, false);
221
Jeff Sharkeyc6cbdf12013-08-07 16:22:02 -0700222 mEmptyView = view.findViewById(android.R.id.empty);
223
Steve McKayef280152015-06-11 10:10:49 -0700224 mRecView = (RecyclerView) view.findViewById(R.id.recyclerView);
225 mRecView.setRecyclerListener(
226 new RecyclerListener() {
227 @Override
228 public void onViewRecycled(ViewHolder holder) {
229 cancelThumbnailTask(holder.itemView);
230 }
231 });
Steve McKay8e258c62015-05-06 14:27:57 -0700232
Steve McKayd57f5fa2015-07-23 16:33:41 -0700233 // TODO: Rather than update columns on layout changes, push this
234 // code (or something like it) into GridLayoutManager.
235 mRecView.addOnLayoutChangeListener(
236 new OnLayoutChangeListener() {
237
238 @Override
239 public void onLayoutChange(
240 View v, int left, int top, int right, int bottom, int oldLeft,
241 int oldTop, int oldRight, int oldBottom) {
Steve McKayfefcd702015-08-20 16:19:38 +0000242 mColumnCount = calculateColumnCount();
Steve McKayd57f5fa2015-07-23 16:33:41 -0700243 if (mGridLayout != null) {
244 mGridLayout.setSpanCount(mColumnCount);
245 }
246 }
247 });
248
249 // TODO: Add a divider between views (which might use RecyclerView.ItemDecoration).
Steve McKay8e258c62015-05-06 14:27:57 -0700250 if (DEBUG_ENABLE_DND) {
Steve McKayef280152015-06-11 10:10:49 -0700251 setupDragAndDropOnDirectoryView(mRecView);
Steve McKay8e258c62015-05-06 14:27:57 -0700252 }
Jeff Sharkey09c10bf2013-06-30 20:02:59 -0700253
Jeff Sharkeyac9e6272013-08-31 21:27:44 -0700254 return view;
255 }
Jeff Sharkey09c10bf2013-06-30 20:02:59 -0700256
Jeff Sharkeyac9e6272013-08-31 21:27:44 -0700257 @Override
Jeff Sharkeyf63b7772013-10-01 17:57:41 -0700258 public void onDestroyView() {
259 super.onDestroyView();
260
261 // Cancel any outstanding thumbnail requests
Steve McKayef280152015-06-11 10:10:49 -0700262 final int count = mRecView.getChildCount();
Jeff Sharkeyf63b7772013-10-01 17:57:41 -0700263 for (int i = 0; i < count; i++) {
Steve McKayef280152015-06-11 10:10:49 -0700264 final View view = mRecView.getChildAt(i);
265 cancelThumbnailTask(view);
Jeff Sharkeyf63b7772013-10-01 17:57:41 -0700266 }
Jeff Sharkeyfaaeb392013-10-04 14:44:56 -0700267
Steve McKayef280152015-06-11 10:10:49 -0700268 // Clear any outstanding selection
Ben Kwa24be5d32015-08-27 16:04:46 -0700269 mModel.clearSelection();
Jeff Sharkeyf63b7772013-10-01 17:57:41 -0700270 }
271
272 @Override
Jeff Sharkeyac9e6272013-08-31 21:27:44 -0700273 public void onActivityCreated(Bundle savedInstanceState) {
274 super.onActivityCreated(savedInstanceState);
275
276 final Context context = getActivity();
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700277 final State state = getDisplayState(DirectoryFragment.this);
Jeff Sharkeyac9e6272013-08-31 21:27:44 -0700278
Jeff Sharkey9656a532013-09-13 13:42:19 -0700279 final RootInfo root = getArguments().getParcelable(EXTRA_ROOT);
280 final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC);
281
Steve McKayef280152015-06-11 10:10:49 -0700282 mAdapter = new DocumentsAdapter(context);
283 mRecView.setAdapter(mAdapter);
284
285 GestureDetector.SimpleOnGestureListener listener =
286 new GestureDetector.SimpleOnGestureListener() {
287 @Override
288 public boolean onSingleTapUp(MotionEvent e) {
289 return DirectoryFragment.this.onSingleTapUp(e);
290 }
Steve McKay93d8ef42015-07-30 12:27:44 -0700291 @Override
292 public boolean onDoubleTap(MotionEvent e) {
293 Log.d(TAG, "Handling double tap.");
294 return DirectoryFragment.this.onDoubleTap(e);
295 }
Steve McKayef280152015-06-11 10:10:49 -0700296 };
297
Ben Kwa24be5d32015-08-27 16:04:46 -0700298 // TODO: instead of inserting the view into the constructor, extract listener-creation code
299 // and set the listener on the view after the fact. Then the view doesn't need to be passed
300 // into the selection manager which is passed into the model.
301 MultiSelectManager selMgr= new MultiSelectManager(
Steve McKay57394872015-08-12 14:48:34 -0700302 mRecView,
303 listener,
304 state.allowMultiple
305 ? MultiSelectManager.MODE_MULTIPLE
306 : MultiSelectManager.MODE_SINGLE);
Ben Kwa24be5d32015-08-27 16:04:46 -0700307 selMgr.addCallback(new SelectionModeListener());
Ben Kwa7461a952015-09-01 11:03:01 -0700308
309 mModel = new Model(context, selMgr);
Ben Kwa24be5d32015-08-27 16:04:46 -0700310 mModel.setSelectionManager(selMgr);
Ben Kwa7461a952015-09-01 11:03:01 -0700311 mModel.addUpdateListener(mAdapter);
Steve McKayef280152015-06-11 10:10:49 -0700312
Jeff Sharkey2e694f82013-08-06 16:26:14 -0700313 mType = getArguments().getInt(EXTRA_TYPE);
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700314 mStateKey = buildStateKey(root, doc);
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700315
Steve McKay1f199482015-05-20 15:58:42 -0700316 mFragmentTuner = pickFragmentTuner(state);
317 mClipper = new DocumentClipper(context);
318
Jeff Sharkey9656a532013-09-13 13:42:19 -0700319 if (mType == TYPE_RECENT_OPEN) {
320 // Hide titles when showing recents for picking images/videos
321 mHideGridTitles = MimePredicate.mimeMatches(
322 MimePredicate.VISUAL_MIMES, state.acceptMimes);
323 } else {
324 mHideGridTitles = (doc != null) && doc.isGridTitlesHidden();
325 }
326
Jeff Sharkeyf63b7772013-10-01 17:57:41 -0700327 final ActivityManager am = (ActivityManager) context.getSystemService(
328 Context.ACTIVITY_SERVICE);
329 mSvelteRecents = am.isLowRamDevice() && (mType == TYPE_RECENT_OPEN);
330
Jeff Sharkey46899c82013-08-18 22:26:48 -0700331 mCallbacks = new LoaderCallbacks<DirectoryResult>() {
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700332 @Override
Jeff Sharkey46899c82013-08-18 22:26:48 -0700333 public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) {
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700334 final String query = getArguments().getString(EXTRA_QUERY);
Jeff Sharkey46165b52013-07-31 20:53:22 -0700335
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700336 Uri contentsUri;
337 switch (mType) {
338 case TYPE_NORMAL:
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700339 contentsUri = DocumentsContract.buildChildDocumentsUri(
340 doc.authority, doc.documentId);
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -0700341 if (state.action == ACTION_MANAGE) {
342 contentsUri = DocumentsContract.setManageMode(contentsUri);
343 }
Jeff Sharkeyd10f0492013-09-09 17:35:46 -0700344 return new DirectoryLoader(
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -0700345 context, mType, root, doc, contentsUri, state.userSortOrder);
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700346 case TYPE_SEARCH:
347 contentsUri = DocumentsContract.buildSearchDocumentsUri(
Jeff Sharkey0e8c8712013-09-12 21:59:06 -0700348 root.authority, root.rootId, query);
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -0700349 if (state.action == ACTION_MANAGE) {
350 contentsUri = DocumentsContract.setManageMode(contentsUri);
351 }
Jeff Sharkeyd10f0492013-09-09 17:35:46 -0700352 return new DirectoryLoader(
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -0700353 context, mType, root, doc, contentsUri, state.userSortOrder);
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700354 case TYPE_RECENT_OPEN:
Jeff Sharkey1c903cc2013-09-02 17:19:40 -0700355 final RootsCache roots = DocumentsApplication.getRootsCache(context);
Jeff Sharkey8b997042013-09-19 15:25:56 -0700356 return new RecentLoader(context, roots, state);
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700357 default:
358 throw new IllegalStateException("Unknown type " + mType);
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -0700359 }
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700360 }
361
362 @Override
Jeff Sharkey46899c82013-08-18 22:26:48 -0700363 public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
Makoto Onuki77778752015-07-01 14:55:14 -0700364 if (result == null || result.exception != null) {
365 // onBackPressed does a fragment transaction, which can't be done inside
366 // onLoadFinished
367 mHandler.post(new Runnable() {
368 @Override
369 public void run() {
370 final Activity activity = getActivity();
371 if (activity != null) {
372 activity.onBackPressed();
373 }
374 }
375 });
376 return;
377 }
378
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700379 if (!isAdded()) return;
380
Ben Kwa24be5d32015-08-27 16:04:46 -0700381 mModel.update(result);
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700382
383 // Push latest state up to UI
384 // TODO: if mode change was racing with us, don't overwrite it
Jeff Sharkey7d58fc62013-09-12 16:25:02 -0700385 if (result.mode != MODE_UNKNOWN) {
386 state.derivedMode = result.mode;
387 }
Jeff Sharkeyd10f0492013-09-09 17:35:46 -0700388 state.derivedSortOrder = result.sortOrder;
Steve McKayd0a2a2c2015-03-25 14:35:33 -0700389 ((BaseActivity) context).onStateChanged();
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700390
391 updateDisplayState();
392
Jeff Sharkey25f10b32013-10-07 14:08:17 -0700393 // When launched into empty recents, show drawer
Ben Kwa24be5d32015-08-27 16:04:46 -0700394 if (mType == TYPE_RECENT_OPEN && mModel.isEmpty() && !state.stackTouched &&
Steve McKayb68dd222015-04-20 17:18:15 -0700395 context instanceof DocumentsActivity) {
396 ((DocumentsActivity) context).setRootsDrawerOpen(true);
Jeff Sharkey25f10b32013-10-07 14:08:17 -0700397 }
398
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700399 // Restore any previous instance state
400 final SparseArray<Parcelable> container = state.dirState.remove(mStateKey);
401 if (container != null && !getArguments().getBoolean(EXTRA_IGNORE_STATE, false)) {
402 getView().restoreHierarchyState(container);
403 } else if (mLastSortOrder != state.derivedSortOrder) {
Steve McKayef280152015-06-11 10:10:49 -0700404 mRecView.smoothScrollToPosition(0);
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700405 }
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700406
407 mLastSortOrder = state.derivedSortOrder;
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700408 }
409
410 @Override
Jeff Sharkey46899c82013-08-18 22:26:48 -0700411 public void onLoaderReset(Loader<DirectoryResult> loader) {
Ben Kwa24be5d32015-08-27 16:04:46 -0700412 mModel.update(null);
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700413 }
414 };
415
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700416 // Kick off loader at least once
Steve McKayef280152015-06-11 10:10:49 -0700417 getLoaderManager().restartLoader(LOADER_ID, null, mCallbacks);
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700418
Kyle Horimoto426bd0d2015-07-29 15:33:49 -0700419 mFragmentTuner.afterActivityCreated(this);
Jeff Sharkey2e694f82013-08-06 16:26:14 -0700420 updateDisplayState();
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700421 }
422
Jeff Sharkey42d26792013-09-06 13:22:09 -0700423 @Override
Ben Kwaf5858932015-04-07 15:43:39 -0700424 public void onActivityResult(int requestCode, int resultCode, Intent data) {
Ben Kwaf5858932015-04-07 15:43:39 -0700425 // There's only one request code right now. Replace this with a switch statement or
426 // something more scalable when more codes are added.
427 if (requestCode != REQUEST_COPY_DESTINATION) {
428 return;
429 }
430 if (resultCode == Activity.RESULT_CANCELED || data == null) {
431 // User pressed the back button or otherwise cancelled the destination pick. Don't
432 // proceed with the copy.
433 return;
434 }
435
Tomasz Mikolajewski9452c442015-04-14 16:32:41 +0900436 CopyService.start(getActivity(), getDisplayState(this).selectedDocumentsForCopy,
Ben Kwacb4461f2015-05-05 11:50:11 -0700437 (DocumentStack) data.getParcelableExtra(CopyService.EXTRA_STACK),
438 data.getIntExtra(CopyService.EXTRA_TRANSFER_MODE, CopyService.TRANSFER_MODE_NONE));
Ben Kwaf5858932015-04-07 15:43:39 -0700439 }
440
Steve McKayef280152015-06-11 10:10:49 -0700441 private int getEventAdapterPosition(MotionEvent e) {
442 View view = mRecView.findChildViewUnder(e.getX(), e.getY());
443 return view != null ? mRecView.getChildAdapterPosition(view) : RecyclerView.NO_POSITION;
444 }
445
446 private boolean onSingleTapUp(MotionEvent e) {
Ben Kwa24be5d32015-08-27 16:04:46 -0700447 if (Events.isTouchEvent(e) && mModel.getSelection().isEmpty()) {
Steve McKay93d8ef42015-07-30 12:27:44 -0700448 int position = getEventAdapterPosition(e);
449 if (position != RecyclerView.NO_POSITION) {
450 return handleViewItem(position);
Steve McKayef280152015-06-11 10:10:49 -0700451 }
452 }
Steve McKay93d8ef42015-07-30 12:27:44 -0700453 return false;
454 }
Steve McKayef280152015-06-11 10:10:49 -0700455
Steve McKay93d8ef42015-07-30 12:27:44 -0700456 protected boolean onDoubleTap(MotionEvent e) {
457 if (Events.isMouseEvent(e)) {
458 Log.d(TAG, "Handling double tap from mouse.");
459 int position = getEventAdapterPosition(e);
460 if (position != RecyclerView.NO_POSITION) {
461 return handleViewItem(position);
462 }
463 }
464 return false;
465 }
466
467 private boolean handleViewItem(int position) {
Ben Kwa24be5d32015-08-27 16:04:46 -0700468 final Cursor cursor = mModel.getItem(position);
Steve McKay93d8ef42015-07-30 12:27:44 -0700469 checkNotNull(cursor, "Cursor cannot be null.");
470 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
471 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
472 if (isDocumentEnabled(docMimeType, docFlags)) {
473 final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
Ben Kwa24be5d32015-08-27 16:04:46 -0700474 ((BaseActivity) getActivity()).onDocumentPicked(doc, mModel);
475 mModel.clearSelection();
Steve McKay93d8ef42015-07-30 12:27:44 -0700476 return true;
477 }
Steve McKayef280152015-06-11 10:10:49 -0700478 return false;
479 }
480
Ben Kwaf5858932015-04-07 15:43:39 -0700481 @Override
Jeff Sharkeyc8ae7a52013-09-18 16:26:49 -0700482 public void onStop() {
483 super.onStop();
484
485 // Remember last scroll location
486 final SparseArray<Parcelable> container = new SparseArray<Parcelable>();
487 getView().saveHierarchyState(container);
488 final State state = getDisplayState(this);
489 state.dirState.put(mStateKey, container);
490 }
491
492 @Override
Jeff Sharkey7d58fc62013-09-12 16:25:02 -0700493 public void onResume() {
494 super.onResume();
Jeff Sharkey42d26792013-09-06 13:22:09 -0700495 updateDisplayState();
496 }
497
Jeff Sharkeye8d13ea2014-08-08 15:10:03 -0700498 public void onDisplayStateChanged() {
499 updateDisplayState();
500 }
501
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700502 public void onUserSortOrderChanged() {
Jeff Sharkeyd10f0492013-09-09 17:35:46 -0700503 // Sort order change always triggers reload; we'll trigger state change
504 // on the flip side.
Steve McKayef280152015-06-11 10:10:49 -0700505 getLoaderManager().restartLoader(LOADER_ID, null, mCallbacks);
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700506 }
507
508 public void onUserModeChanged() {
Jeff Sharkeyd10f0492013-09-09 17:35:46 -0700509 final ContentResolver resolver = getActivity().getContentResolver();
510 final State state = getDisplayState(this);
511
512 final RootInfo root = getArguments().getParcelable(EXTRA_ROOT);
513 final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC);
514
Jeff Sharkey0e8c8712013-09-12 21:59:06 -0700515 if (root != null && doc != null) {
Jeff Sharkey7d58fc62013-09-12 16:25:02 -0700516 final Uri stateUri = RecentsProvider.buildState(
517 root.authority, root.rootId, doc.documentId);
518 final ContentValues values = new ContentValues();
519 values.put(StateColumns.MODE, state.userMode);
Jeff Sharkeyd10f0492013-09-09 17:35:46 -0700520
Jeff Sharkey7d58fc62013-09-12 16:25:02 -0700521 new AsyncTask<Void, Void, Void>() {
522 @Override
523 protected Void doInBackground(Void... params) {
524 resolver.insert(stateUri, values);
525 return null;
526 }
527 }.execute();
528 }
Jeff Sharkeyd10f0492013-09-09 17:35:46 -0700529
530 // Mode change is just visual change; no need to kick loader, and
531 // deliver change event immediately.
532 state.derivedMode = state.userMode;
Steve McKayd0a2a2c2015-03-25 14:35:33 -0700533 ((BaseActivity) getActivity()).onStateChanged();
Jeff Sharkeyd10f0492013-09-09 17:35:46 -0700534
Jeff Sharkeya4d1f222013-09-07 14:45:03 -0700535 updateDisplayState();
536 }
537
538 private void updateDisplayState() {
Jeff Sharkeyb3620442013-09-01 18:41:04 -0700539 final State state = getDisplayState(this);
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700540
Jeff Sharkey5e1884d2013-09-10 17:56:39 -0700541 if (mLastMode == state.derivedMode && mLastShowSize == state.showSize) return;
Jeff Sharkeyd10f0492013-09-09 17:35:46 -0700542 mLastMode = state.derivedMode;
Jeff Sharkey5e1884d2013-09-10 17:56:39 -0700543 mLastShowSize = state.showSize;
Jeff Sharkey09c10bf2013-06-30 20:02:59 -0700544
Steve McKayef280152015-06-11 10:10:49 -0700545 updateLayout(state.derivedMode);
Jeff Sharkey09c10bf2013-06-30 20:02:59 -0700546
Steve McKayef280152015-06-11 10:10:49 -0700547 mRecView.setAdapter(mAdapter);
548 }
549
550 /**
551 * Returns a {@code LayoutManager} for {@code mode}, lazily initializing
552 * classes as needed.
553 */
554 private void updateLayout(int mode) {
Jeff Sharkey8a8fb672013-05-07 12:41:33 -0700555 final int thumbSize;
Steve McKayef280152015-06-11 10:10:49 -0700556
557 final LayoutManager layout;
558 switch (mode) {
559 case MODE_GRID:
Steve McKayef280152015-06-11 10:10:49 -0700560 thumbSize = getResources().getDimensionPixelSize(R.dimen.grid_width);
Steve McKaya9be7182015-07-22 16:03:35 -0700561 if (mGridLayout == null) {
Steve McKayd57f5fa2015-07-23 16:33:41 -0700562 mGridLayout = new GridLayoutManager(getContext(), mColumnCount );
Steve McKaya9be7182015-07-22 16:03:35 -0700563 }
Steve McKayef280152015-06-11 10:10:49 -0700564 layout = mGridLayout;
565 break;
566 case MODE_LIST:
Steve McKaya9be7182015-07-22 16:03:35 -0700567 thumbSize = getResources().getDimensionPixelSize(R.dimen.icon_size);
Steve McKayef280152015-06-11 10:10:49 -0700568 if (mListLayout == null) {
569 mListLayout = new LinearLayoutManager(getContext());
570 }
Steve McKayef280152015-06-11 10:10:49 -0700571 layout = mListLayout;
572 break;
573 case MODE_UNKNOWN:
574 default:
575 throw new IllegalArgumentException("Unsupported layout mode: " + mode);
Jeff Sharkey09c10bf2013-06-30 20:02:59 -0700576 }
Jeff Sharkey8a8fb672013-05-07 12:41:33 -0700577
Steve McKayef280152015-06-11 10:10:49 -0700578 mRecView.setLayoutManager(layout);
Kyle Horimoto2da6e4a2015-08-27 16:44:00 -0700579 // TODO: Once b/23691541 is resolved, use a listener within MultiSelectManager instead of
580 // imperatively calling this function.
Steve McKay9058e042015-09-01 12:31:24 -0700581 mModel.mSelectionManager.handleLayoutChanged();
Steve McKayef280152015-06-11 10:10:49 -0700582 // setting layout manager automatically invalidates existing ViewHolders.
Jeff Sharkey8a8fb672013-05-07 12:41:33 -0700583 mThumbSize = new Point(thumbSize, thumbSize);
Jeff Sharkey09c10bf2013-06-30 20:02:59 -0700584 }
585
Steve McKayfefcd702015-08-20 16:19:38 +0000586 private int calculateColumnCount() {
587 int cellWidth = getResources().getDimensionPixelSize(R.dimen.grid_width);
588 int cellMargin = 2 * getResources().getDimensionPixelSize(R.dimen.grid_item_margin);
Steve McKayd57f5fa2015-07-23 16:33:41 -0700589 int viewPadding = mRecView.getPaddingLeft() + mRecView.getPaddingRight();
Steve McKayfefcd702015-08-20 16:19:38 +0000590
Steve McKayd57f5fa2015-07-23 16:33:41 -0700591 checkState(mRecView.getWidth() > 0);
592 int columnCount = Math.max(1,
Steve McKayfefcd702015-08-20 16:19:38 +0000593 (mRecView.getWidth() - viewPadding) / (cellWidth + cellMargin));
594
Steve McKayd57f5fa2015-07-23 16:33:41 -0700595 return columnCount;
596 }
597
Steve McKayef280152015-06-11 10:10:49 -0700598 /**
599 * Manages the integration between our ActionMode and MultiSelectManager, initiating
600 * ActionMode when there is a selection, canceling it when there is no selection,
601 * and clearing selection when action mode is explicitly exited by the user.
602 */
603 private final class SelectionModeListener
604 implements MultiSelectManager.Callback, ActionMode.Callback {
605
606 private Selection mSelected = new Selection();
607 private ActionMode mActionMode;
Steve McKay4f4232d2015-07-22 12:13:46 -0700608 private int mNoDeleteCount = 0;
609 private Menu mMenu;
Steve McKayef280152015-06-11 10:10:49 -0700610
Jeff Sharkey09c10bf2013-06-30 20:02:59 -0700611 @Override
Steve McKayef280152015-06-11 10:10:49 -0700612 public boolean onBeforeItemStateChange(int position, boolean selected) {
613 // Directories and footer items cannot be checked
614 if (selected) {
Ben Kwa24be5d32015-08-27 16:04:46 -0700615 final Cursor cursor = mModel.getItem(position);
Steve McKayef280152015-06-11 10:10:49 -0700616 checkNotNull(cursor, "Cursor cannot be null.");
Jeff Sharkey7cf49032013-09-26 10:54:16 -0700617 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
618 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
Steve McKayef280152015-06-11 10:10:49 -0700619 return isDocumentEnabled(docMimeType, docFlags);
620 }
621 return true;
622 }
623
624 @Override
625 public void onItemStateChanged(int position, boolean selected) {
Steve McKay4f4232d2015-07-22 12:13:46 -0700626
Ben Kwa24be5d32015-08-27 16:04:46 -0700627 final Cursor cursor = mModel.getItem(position);
Steve McKay4f4232d2015-07-22 12:13:46 -0700628 checkNotNull(cursor, "Cursor cannot be null.");
629
630 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
631 if ((docFlags & Document.FLAG_SUPPORTS_DELETE) == 0) {
632 mNoDeleteCount += selected ? 1 : -1;
633 }
Steve McKay57394872015-08-12 14:48:34 -0700634 }
Steve McKay4f4232d2015-07-22 12:13:46 -0700635
Steve McKay57394872015-08-12 14:48:34 -0700636 @Override
637 public void onSelectionChanged() {
Ben Kwa24be5d32015-08-27 16:04:46 -0700638 mModel.getSelection(mSelected);
Steve McKay4f4232d2015-07-22 12:13:46 -0700639 if (mSelected.size() > 0) {
Steve McKayef280152015-06-11 10:10:49 -0700640 if (DEBUG) Log.d(TAG, "Maybe starting action mode.");
641 if (mActionMode == null) {
642 if (DEBUG) Log.d(TAG, "Yeah. Starting action mode.");
643 mActionMode = getActivity().startActionMode(this);
Tomasz Mikolajewski2b6b0662015-07-28 14:59:00 +0900644 getActivity().getWindow().setStatusBarColor(
645 getResources().getColor(R.color.action_mode_status_bar_background));
Jeff Sharkeyaed873d2013-09-09 16:51:06 -0700646 }
Steve McKay4f4232d2015-07-22 12:13:46 -0700647 updateActionMenu();
648 } else {
649 if (DEBUG) Log.d(TAG, "Finishing action mode.");
650 if (mActionMode != null) {
651 mActionMode.finish();
652 }
Tomasz Mikolajewski2b6b0662015-07-28 14:59:00 +0900653 getActivity().getWindow().setStatusBarColor(
654 getResources().getColor(R.color.status_bar_background));
Jeff Sharkeyf339f252013-08-15 16:17:41 -0700655 }
Jeff Sharkey09c10bf2013-06-30 20:02:59 -0700656
Steve McKayef280152015-06-11 10:10:49 -0700657 if (mActionMode != null) {
658 mActionMode.setTitle(TextUtils.formatSelectedCount(mSelected.size()));
659 }
660 }
661
662 // Called when the user exits the action mode
663 @Override
664 public void onDestroyActionMode(ActionMode mode) {
665 if (DEBUG) Log.d(TAG, "Handling action mode destroyed.");
666 mActionMode = null;
667 // clear selection
Ben Kwa24be5d32015-08-27 16:04:46 -0700668 mModel.clearSelection();
Steve McKay4f4232d2015-07-22 12:13:46 -0700669 mSelected.clear();
670 mNoDeleteCount = 0;
Steve McKayef280152015-06-11 10:10:49 -0700671 }
672
Jeff Sharkeyc317af82013-07-01 16:56:54 -0700673 @Override
674 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
675 mode.getMenuInflater().inflate(R.menu.mode_directory, menu);
Ben Kwa24be5d32015-08-27 16:04:46 -0700676 mode.setTitle(TextUtils.formatSelectedCount(mModel.getSelection().size()));
677 return mModel.getSelection().size() > 0;
Jeff Sharkeyc317af82013-07-01 16:56:54 -0700678 }
679
680 @Override
681 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
Steve McKay4f4232d2015-07-22 12:13:46 -0700682 mMenu = menu;
683 updateActionMenu();
Jeff Sharkeyc317af82013-07-01 16:56:54 -0700684 return true;
685 }
686
Steve McKay4f4232d2015-07-22 12:13:46 -0700687 private void updateActionMenu() {
688 checkNotNull(mMenu);
689 // Delegate update logic to our owning action, since specialized logic is desired.
690 mFragmentTuner.updateActionMenu(mMenu, mType, mNoDeleteCount == 0);
691 }
692
Jeff Sharkeyc317af82013-07-01 16:56:54 -0700693 @Override
Steve McKayef280152015-06-11 10:10:49 -0700694 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
Steve McKay1f199482015-05-20 15:58:42 -0700695
Ben Kwa24be5d32015-08-27 16:04:46 -0700696 Selection selection = mModel.getSelection(new Selection());
Jeff Sharkeyc317af82013-07-01 16:56:54 -0700697
Jeff Sharkey873daa32013-08-18 17:38:20 -0700698 final int id = item.getItemId();
699 if (id == R.id.menu_open) {
Steve McKayef280152015-06-11 10:10:49 -0700700 openDocuments(selection);
Jeff Sharkeyb3620442013-09-01 18:41:04 -0700701 mode.finish();
Jeff Sharkeyc317af82013-07-01 16:56:54 -0700702 return true;
Jeff Sharkey873daa32013-08-18 17:38:20 -0700703
704 } else if (id == R.id.menu_share) {
Steve McKayef280152015-06-11 10:10:49 -0700705 shareDocuments(selection);
Jeff Sharkeyb3620442013-09-01 18:41:04 -0700706 mode.finish();
Jeff Sharkey873daa32013-08-18 17:38:20 -0700707 return true;
708
709 } else if (id == R.id.menu_delete) {
Steve McKayef280152015-06-11 10:10:49 -0700710 deleteDocuments(selection);
Jeff Sharkeyb3620442013-09-01 18:41:04 -0700711 mode.finish();
Jeff Sharkey873daa32013-08-18 17:38:20 -0700712 return true;
713
Steve McKay1f199482015-05-20 15:58:42 -0700714 } else if (id == R.id.menu_copy_to) {
Steve McKayef280152015-06-11 10:10:49 -0700715 transferDocuments(selection, CopyService.TRANSFER_MODE_COPY);
Ben Kwacb4461f2015-05-05 11:50:11 -0700716 mode.finish();
717 return true;
718
Steve McKay1f199482015-05-20 15:58:42 -0700719 } else if (id == R.id.menu_move_to) {
Steve McKayef280152015-06-11 10:10:49 -0700720 transferDocuments(selection, CopyService.TRANSFER_MODE_MOVE);
Ben Kwa41b26c12015-03-31 10:11:43 -0700721 mode.finish();
722 return true;
723
Steve McKay1f199482015-05-20 15:58:42 -0700724 } else if (id == R.id.menu_copy_to_clipboard) {
Steve McKayef280152015-06-11 10:10:49 -0700725 copySelectionToClipboard(selection);
Steve McKay1f199482015-05-20 15:58:42 -0700726 mode.finish();
727 return true;
728
Ben Kwa512a6ba2015-03-31 08:15:21 -0700729 } else if (id == R.id.menu_select_all) {
Steve McKay0599a442015-05-05 14:50:00 -0700730 selectAllFiles();
Ben Kwa512a6ba2015-03-31 08:15:21 -0700731 return true;
732
Jeff Sharkeyc317af82013-07-01 16:56:54 -0700733 } else {
734 return false;
735 }
736 }
Steve McKayef280152015-06-11 10:10:49 -0700737 }
Jeff Sharkeyc317af82013-07-01 16:56:54 -0700738
Steve McKayef280152015-06-11 10:10:49 -0700739 private static void cancelThumbnailTask(View view) {
740 final ImageView iconThumb = (ImageView) view.findViewById(R.id.icon_thumb);
741 if (iconThumb != null) {
742 final ThumbnailAsyncTask oldTask = (ThumbnailAsyncTask) iconThumb.getTag();
743 if (oldTask != null) {
744 oldTask.preempt();
745 iconThumb.setTag(null);
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -0700746 }
747 }
Steve McKayef280152015-06-11 10:10:49 -0700748 }
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -0700749
Steve McKayef280152015-06-11 10:10:49 -0700750 private void openDocuments(final Selection selected) {
Steve McKay9276f3b2015-05-27 16:11:42 -0700751 new GetDocumentsTask() {
752 @Override
753 void onDocumentsReady(List<DocumentInfo> docs) {
Steve McKay0fbfc652015-08-20 16:48:49 -0700754 // TODO: Implement support in Files activity for opening multiple docs.
Steve McKay9276f3b2015-05-27 16:11:42 -0700755 BaseActivity.get(DirectoryFragment.this).onDocumentsPicked(docs);
Ben Kwaf527c632015-04-08 15:03:35 -0700756 }
Steve McKay9276f3b2015-05-27 16:11:42 -0700757 }.execute(selected);
Jeff Sharkey873daa32013-08-18 17:38:20 -0700758 }
759
Steve McKayef280152015-06-11 10:10:49 -0700760 private void shareDocuments(final Selection selected) {
Steve McKay9276f3b2015-05-27 16:11:42 -0700761 new GetDocumentsTask() {
762 @Override
763 void onDocumentsReady(List<DocumentInfo> docs) {
764 Intent intent;
765
766 // Filter out directories - those can't be shared.
Steve McKayfefcd702015-08-20 16:19:38 +0000767 List<DocumentInfo> docsForSend = new ArrayList<>();
Steve McKay9276f3b2015-05-27 16:11:42 -0700768 for (DocumentInfo doc: docs) {
769 if (!Document.MIME_TYPE_DIR.equals(doc.mimeType)) {
770 docsForSend.add(doc);
771 }
772 }
773
774 if (docsForSend.size() == 1) {
775 final DocumentInfo doc = docsForSend.get(0);
776
777 intent = new Intent(Intent.ACTION_SEND);
778 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
779 intent.addCategory(Intent.CATEGORY_DEFAULT);
780 intent.setType(doc.mimeType);
781 intent.putExtra(Intent.EXTRA_STREAM, doc.derivedUri);
782
783 } else if (docsForSend.size() > 1) {
784 intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
785 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
786 intent.addCategory(Intent.CATEGORY_DEFAULT);
787
Steve McKayfefcd702015-08-20 16:19:38 +0000788 final ArrayList<String> mimeTypes = new ArrayList<>();
789 final ArrayList<Uri> uris = new ArrayList<>();
Steve McKay9276f3b2015-05-27 16:11:42 -0700790 for (DocumentInfo doc : docsForSend) {
791 mimeTypes.add(doc.mimeType);
792 uris.add(doc.derivedUri);
793 }
794
795 intent.setType(findCommonMimeType(mimeTypes));
796 intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
797
798 } else {
799 return;
800 }
801
802 intent = Intent.createChooser(intent, getActivity().getText(R.string.share_via));
803 startActivity(intent);
804 }
805 }.execute(selected);
806 }
807
Steve McKayef280152015-06-11 10:10:49 -0700808 private void deleteDocuments(final Selection selected) {
Ben Kwa91923182015-08-27 16:06:33 -0700809 Context context = getActivity();
810 ContentResolver resolver = context.getContentResolver();
811 String message = Shared.getQuantityString(context, R.plurals.deleting, selected.size());
Jeff Sharkey873daa32013-08-18 17:38:20 -0700812
Ben Kwa91923182015-08-27 16:06:33 -0700813 mModel.markForDeletion(selected);
Jeff Sharkey873daa32013-08-18 17:38:20 -0700814
Ben Kwa91923182015-08-27 16:06:33 -0700815 Activity activity = getActivity();
816 Snackbar.make(this.getView(), message, Snackbar.LENGTH_LONG)
817 .setAction(
818 R.string.undo,
819 new android.view.View.OnClickListener() {
820 @Override
821 public void onClick(View view) {}
822 })
823 .setCallback(
824 new Snackbar.Callback() {
825 @Override
826 public void onDismissed(Snackbar snackbar, int event) {
827 if (event == Snackbar.Callback.DISMISS_EVENT_ACTION) {
828 mModel.undoDeletion();
829 } else {
Ben Kwa7461a952015-09-01 11:03:01 -0700830 // TODO: Use a listener rather than pushing the view.
831 mModel.finalizeDeletion(DirectoryFragment.this.getView());
Ben Kwa91923182015-08-27 16:06:33 -0700832 }
Ben Kwa91923182015-08-27 16:06:33 -0700833 }
834 })
835 .show();
Jeff Sharkey873daa32013-08-18 17:38:20 -0700836 }
837
Steve McKayef280152015-06-11 10:10:49 -0700838 private void transferDocuments(final Selection selected, final int mode) {
Ben Kwaf5858932015-04-07 15:43:39 -0700839 // Pop up a dialog to pick a destination. This is inadequate but works for now.
840 // TODO: Implement a picker that is to spec.
Daichi Hironocaadd412015-04-10 15:50:38 +0900841 final Intent intent = new Intent(
Daichi Hirono22574ed2015-04-15 13:41:18 +0900842 BaseActivity.DocumentsIntent.ACTION_OPEN_COPY_DESTINATION,
Daichi Hironocaadd412015-04-10 15:50:38 +0900843 Uri.EMPTY,
844 getActivity(),
845 DocumentsActivity.class);
Steve McKay9276f3b2015-05-27 16:11:42 -0700846
847 new GetDocumentsTask() {
848 @Override
849 void onDocumentsReady(List<DocumentInfo> docs) {
850 getDisplayState(DirectoryFragment.this).selectedDocumentsForCopy = docs;
851
852 boolean directoryCopy = false;
853 for (DocumentInfo info : docs) {
854 if (Document.MIME_TYPE_DIR.equals(info.mimeType)) {
855 directoryCopy = true;
856 break;
857 }
858 }
859 intent.putExtra(BaseActivity.DocumentsIntent.EXTRA_DIRECTORY_COPY, directoryCopy);
860 intent.putExtra(CopyService.EXTRA_TRANSFER_MODE, mode);
861 startActivityForResult(intent, REQUEST_COPY_DESTINATION);
Daichi Hironof2a822d2015-04-14 17:12:54 +0900862 }
Steve McKay9276f3b2015-05-27 16:11:42 -0700863 }.execute(selected);
Ben Kwa41b26c12015-03-31 10:11:43 -0700864 }
865
Jeff Sharkeyb3620442013-09-01 18:41:04 -0700866 private static State getDisplayState(Fragment fragment) {
Steve McKayd0a2a2c2015-03-25 14:35:33 -0700867 return ((BaseActivity) fragment.getActivity()).getDisplayState();
Jeff Sharkey09c10bf2013-06-30 20:02:59 -0700868 }
869
Jeff Sharkeyaed873d2013-09-09 16:51:06 -0700870 private static abstract class Footer {
871 private final int mItemViewType;
872
873 public Footer(int itemViewType) {
874 mItemViewType = itemViewType;
875 }
876
877 public abstract View getView(View convertView, ViewGroup parent);
878
879 public int getItemViewType() {
880 return mItemViewType;
881 }
Jeff Sharkey20b32272013-09-03 15:25:52 -0700882 }
883
Jeff Sharkey5e1884d2013-09-10 17:56:39 -0700884 private class LoadingFooter extends Footer {
Jeff Sharkeyaed873d2013-09-09 16:51:06 -0700885 public LoadingFooter() {
886 super(1);
887 }
888
Jeff Sharkey20b32272013-09-03 15:25:52 -0700889 @Override
890 public View getView(View convertView, ViewGroup parent) {
891 final Context context = parent.getContext();
Jeff Sharkey5e1884d2013-09-10 17:56:39 -0700892 final State state = getDisplayState(DirectoryFragment.this);
893
Jeff Sharkey20b32272013-09-03 15:25:52 -0700894 if (convertView == null) {
895 final LayoutInflater inflater = LayoutInflater.from(context);
Jeff Sharkey5e1884d2013-09-10 17:56:39 -0700896 if (state.derivedMode == MODE_LIST) {
897 convertView = inflater.inflate(R.layout.item_loading_list, parent, false);
898 } else if (state.derivedMode == MODE_GRID) {
899 convertView = inflater.inflate(R.layout.item_loading_grid, parent, false);
900 } else {
901 throw new IllegalStateException();
902 }
Jeff Sharkey20b32272013-09-03 15:25:52 -0700903 }
Jeff Sharkey5e1884d2013-09-10 17:56:39 -0700904
Jeff Sharkey20b32272013-09-03 15:25:52 -0700905 return convertView;
906 }
907 }
908
Jeff Sharkeyaed873d2013-09-09 16:51:06 -0700909 private class MessageFooter extends Footer {
Jeff Sharkey20b32272013-09-03 15:25:52 -0700910 private final int mIcon;
911 private final String mMessage;
912
Jeff Sharkeyaed873d2013-09-09 16:51:06 -0700913 public MessageFooter(int itemViewType, int icon, String message) {
914 super(itemViewType);
Jeff Sharkey20b32272013-09-03 15:25:52 -0700915 mIcon = icon;
916 mMessage = message;
917 }
918
919 @Override
920 public View getView(View convertView, ViewGroup parent) {
921 final Context context = parent.getContext();
922 final State state = getDisplayState(DirectoryFragment.this);
923
924 if (convertView == null) {
925 final LayoutInflater inflater = LayoutInflater.from(context);
Jeff Sharkeyd10f0492013-09-09 17:35:46 -0700926 if (state.derivedMode == MODE_LIST) {
Jeff Sharkey20b32272013-09-03 15:25:52 -0700927 convertView = inflater.inflate(R.layout.item_message_list, parent, false);
Jeff Sharkeyd10f0492013-09-09 17:35:46 -0700928 } else if (state.derivedMode == MODE_GRID) {
Jeff Sharkey20b32272013-09-03 15:25:52 -0700929 convertView = inflater.inflate(R.layout.item_message_grid, parent, false);
930 } else {
931 throw new IllegalStateException();
932 }
933 }
934
935 final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon);
936 final TextView title = (TextView) convertView.findViewById(android.R.id.title);
937 icon.setImageResource(mIcon);
938 title.setText(mMessage);
939 return convertView;
940 }
941 }
942
Steve McKayef280152015-06-11 10:10:49 -0700943 // Provide a reference to the views for each data item
944 // Complex data items may need more than one view per item, and
945 // you provide access to all the views for a data item in a view holder
946 private static final class DocumentHolder extends RecyclerView.ViewHolder {
947 // each data item is just a string in this case
948 public View view;
949 public String docId; // The stable document id.
950 public DocumentHolder(View view) {
951 super(view);
952 this.view = view;
953 }
954 }
955
Ben Kwa7461a952015-09-01 11:03:01 -0700956 private final class DocumentsAdapter extends RecyclerView.Adapter<DocumentHolder>
957 implements Model.UpdateListener {
Steve McKayef280152015-06-11 10:10:49 -0700958
959 private final Context mContext;
960 private final LayoutInflater mInflater;
961 // TODO: Bring back support for footers.
Steve McKayfefcd702015-08-20 16:19:38 +0000962 private final List<Footer> mFooters = new ArrayList<>();
Steve McKayef280152015-06-11 10:10:49 -0700963
Steve McKayef280152015-06-11 10:10:49 -0700964 public DocumentsAdapter(Context context) {
965 mContext = context;
966 mInflater = LayoutInflater.from(context);
967 }
Jeff Sharkeyac9e6272013-08-31 21:27:44 -0700968
Ben Kwa7461a952015-09-01 11:03:01 -0700969 public void onModelUpdate(Model model) {
Jeff Sharkey20b32272013-09-03 15:25:52 -0700970 mFooters.clear();
Ben Kwa7461a952015-09-01 11:03:01 -0700971 if (model.info != null) {
972 mFooters.add(new MessageFooter(2, R.drawable.ic_dialog_info, model.info));
Ben Kwa24be5d32015-08-27 16:04:46 -0700973 }
Ben Kwa7461a952015-09-01 11:03:01 -0700974 if (model.error != null) {
975 mFooters.add(new MessageFooter(3, R.drawable.ic_dialog_alert, model.error));
Ben Kwa24be5d32015-08-27 16:04:46 -0700976 }
Ben Kwa7461a952015-09-01 11:03:01 -0700977 if (model.isLoading()) {
Ben Kwa24be5d32015-08-27 16:04:46 -0700978 mFooters.add(new LoadingFooter());
Jeff Sharkey20b32272013-09-03 15:25:52 -0700979 }
Jeff Sharkeyc6cbdf12013-08-07 16:22:02 -0700980
Ben Kwa7461a952015-09-01 11:03:01 -0700981 if (model.isEmpty()) {
Jeff Sharkeyc6cbdf12013-08-07 16:22:02 -0700982 mEmptyView.setVisibility(View.VISIBLE);
983 } else {
984 mEmptyView.setVisibility(View.GONE);
985 }
986
Jeff Sharkeya5defe32013-08-05 17:56:48 -0700987 notifyDataSetChanged();
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700988 }
989
Ben Kwa7461a952015-09-01 11:03:01 -0700990 public void onModelUpdateFailed(Exception e) {
991 String error = getString(R.string.query_error);
992 mFooters.add(new MessageFooter(3, R.drawable.ic_dialog_alert, error));
993 notifyDataSetChanged();
994 }
995
Jeff Sharkeye22d02e2013-04-26 16:54:55 -0700996 @Override
Steve McKayef280152015-06-11 10:10:49 -0700997 public DocumentHolder onCreateViewHolder(ViewGroup parent, int viewType) {
998 final State state = getDisplayState(DirectoryFragment.this);
999 final LayoutInflater inflater = LayoutInflater.from(getContext());
1000 switch (state.derivedMode) {
1001 case MODE_GRID:
1002 return new DocumentHolder(inflater.inflate(R.layout.item_doc_grid, parent, false));
1003 case MODE_LIST:
1004 return new DocumentHolder(inflater.inflate(R.layout.item_doc_list, parent, false));
1005 case MODE_UNKNOWN:
1006 default:
1007 throw new IllegalStateException("Unsupported layout mode.");
Jeff Sharkey20b32272013-09-03 15:25:52 -07001008 }
1009 }
1010
Steve McKayef280152015-06-11 10:10:49 -07001011 @Override
1012 public void onBindViewHolder(DocumentHolder holder, int position) {
1013
1014 final Context context = getContext();
Jeff Sharkeyb3620442013-09-01 18:41:04 -07001015 final State state = getDisplayState(DirectoryFragment.this);
Jeff Sharkey9656a532013-09-13 13:42:19 -07001016 final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC);
Jeff Sharkey873daa32013-08-18 17:38:20 -07001017 final RootsCache roots = DocumentsApplication.getRootsCache(context);
1018 final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache(
1019 context, mThumbSize);
1020
Ben Kwa24be5d32015-08-27 16:04:46 -07001021 final Cursor cursor = mModel.getItem(position);
Steve McKayef280152015-06-11 10:10:49 -07001022 checkNotNull(cursor, "Cursor cannot be null.");
Jeff Sharkeyac9e6272013-08-31 21:27:44 -07001023
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -07001024 final String docAuthority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY);
1025 final String docRootId = getCursorString(cursor, RootCursorWrapper.COLUMN_ROOT_ID);
Jeff Sharkeyac9e6272013-08-31 21:27:44 -07001026 final String docId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
1027 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
1028 final String docDisplayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
1029 final long docLastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED);
1030 final int docIcon = getCursorInt(cursor, Document.COLUMN_ICON);
1031 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
1032 final String docSummary = getCursorString(cursor, Document.COLUMN_SUMMARY);
1033 final long docSize = getCursorLong(cursor, Document.COLUMN_SIZE);
Jeff Sharkeye22d02e2013-04-26 16:54:55 -07001034
Steve McKayef280152015-06-11 10:10:49 -07001035 holder.docId = docId;
1036 final View itemView = holder.view;
Ben Kwa24be5d32015-08-27 16:04:46 -07001037 itemView.setActivated(mModel.isSelected(position));
Jeff Sharkey9656a532013-09-13 13:42:19 -07001038
Steve McKayef280152015-06-11 10:10:49 -07001039 final View line1 = itemView.findViewById(R.id.line1);
1040 final View line2 = itemView.findViewById(R.id.line2);
1041
1042 final ImageView iconMime = (ImageView) itemView.findViewById(R.id.icon_mime);
1043 final ImageView iconThumb = (ImageView) itemView.findViewById(R.id.icon_thumb);
1044 final TextView title = (TextView) itemView.findViewById(android.R.id.title);
1045 final ImageView icon1 = (ImageView) itemView.findViewById(android.R.id.icon1);
1046 final ImageView icon2 = (ImageView) itemView.findViewById(android.R.id.icon2);
1047 final TextView summary = (TextView) itemView.findViewById(android.R.id.summary);
1048 final TextView date = (TextView) itemView.findViewById(R.id.date);
1049 final TextView size = (TextView) itemView.findViewById(R.id.size);
Jeff Sharkeye22d02e2013-04-26 16:54:55 -07001050
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -07001051 final ThumbnailAsyncTask oldTask = (ThumbnailAsyncTask) iconThumb.getTag();
Jeff Sharkey8a8fb672013-05-07 12:41:33 -07001052 if (oldTask != null) {
Jeff Sharkey753a3ae2013-10-22 17:09:44 -07001053 oldTask.preempt();
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -07001054 iconThumb.setTag(null);
Jeff Sharkey8a8fb672013-05-07 12:41:33 -07001055 }
1056
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -07001057 iconMime.animate().cancel();
1058 iconThumb.animate().cancel();
1059
Jeff Sharkeyaed873d2013-09-09 16:51:06 -07001060 final boolean supportsThumbnail = (docFlags & Document.FLAG_SUPPORTS_THUMBNAIL) != 0;
Jeff Sharkeyd10f0492013-09-09 17:35:46 -07001061 final boolean allowThumbnail = (state.derivedMode == MODE_GRID)
Jeff Sharkey9656a532013-09-13 13:42:19 -07001062 || MimePredicate.mimeMatches(MimePredicate.VISUAL_MIMES, docMimeType);
Jeff Sharkeyf63b7772013-10-01 17:57:41 -07001063 final boolean showThumbnail = supportsThumbnail && allowThumbnail && !mSvelteRecents;
Jeff Sharkeyaed873d2013-09-09 16:51:06 -07001064
Jeff Sharkey7e544612014-08-29 15:38:27 -07001065 final boolean enabled = isDocumentEnabled(docMimeType, docFlags);
1066 final float iconAlpha = (state.derivedMode == MODE_LIST && !enabled) ? 0.5f : 1f;
1067
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -07001068 boolean cacheHit = false;
Jeff Sharkey9656a532013-09-13 13:42:19 -07001069 if (showThumbnail) {
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -07001070 final Uri uri = DocumentsContract.buildDocumentUri(docAuthority, docId);
Jeff Sharkeyac9e6272013-08-31 21:27:44 -07001071 final Bitmap cachedResult = thumbs.get(uri);
Jeff Sharkey8a8fb672013-05-07 12:41:33 -07001072 if (cachedResult != null) {
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -07001073 iconThumb.setImageBitmap(cachedResult);
1074 cacheHit = true;
Jeff Sharkey8a8fb672013-05-07 12:41:33 -07001075 } else {
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -07001076 iconThumb.setImageDrawable(null);
Steve McKayef280152015-06-11 10:10:49 -07001077 // TODO: Hang this off DocumentHolder?
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -07001078 final ThumbnailAsyncTask task = new ThumbnailAsyncTask(
Jeff Sharkey7e544612014-08-29 15:38:27 -07001079 uri, iconMime, iconThumb, mThumbSize, iconAlpha);
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -07001080 iconThumb.setTag(task);
Jeff Sharkey753a3ae2013-10-22 17:09:44 -07001081 ProviderExecutor.forAuthority(docAuthority).execute(task);
Jeff Sharkey8a8fb672013-05-07 12:41:33 -07001082 }
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -07001083 }
1084
1085 // Always throw MIME icon into place, even when a thumbnail is being
1086 // loaded in background.
1087 if (cacheHit) {
1088 iconMime.setAlpha(0f);
Jeff Sharkey9dd02622013-09-27 16:44:11 -07001089 iconMime.setImageDrawable(null);
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -07001090 iconThumb.setAlpha(1f);
Jeff Sharkeye22d02e2013-04-26 16:54:55 -07001091 } else {
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -07001092 iconMime.setAlpha(1f);
1093 iconThumb.setAlpha(0f);
Jeff Sharkey9dd02622013-09-27 16:44:11 -07001094 iconThumb.setImageDrawable(null);
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001095 iconMime.setImageDrawable(
Steve McKayef280152015-06-11 10:10:49 -07001096 getDocumentIcon(mContext, docAuthority, docId, docMimeType, docIcon, state));
Jeff Sharkeye22d02e2013-04-26 16:54:55 -07001097 }
1098
Jeff Sharkey9656a532013-09-13 13:42:19 -07001099 boolean hasLine1 = false;
Jeff Sharkey42d26792013-09-06 13:22:09 -07001100 boolean hasLine2 = false;
1101
Jeff Sharkey9656a532013-09-13 13:42:19 -07001102 final boolean hideTitle = (state.derivedMode == MODE_GRID) && mHideGridTitles;
1103 if (!hideTitle) {
1104 title.setText(docDisplayName);
1105 hasLine1 = true;
1106 }
1107
1108 Drawable iconDrawable = null;
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -07001109 if (mType == TYPE_RECENT_OPEN) {
Jeff Sharkey8b997042013-09-19 15:25:56 -07001110 // We've already had to enumerate roots before any results can
1111 // be shown, so this will never block.
1112 final RootInfo root = roots.getRootBlocking(docAuthority, docRootId);
Jeff Sharkey93cdbc22014-07-29 17:33:36 -07001113 if (state.derivedMode == MODE_GRID) {
Steve McKayef280152015-06-11 10:10:49 -07001114 iconDrawable = root.loadGridIcon(mContext);
Jeff Sharkey93cdbc22014-07-29 17:33:36 -07001115 } else {
Steve McKayef280152015-06-11 10:10:49 -07001116 iconDrawable = root.loadIcon(mContext);
Jeff Sharkey93cdbc22014-07-29 17:33:36 -07001117 }
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -07001118
Jeff Sharkey7d58fc62013-09-12 16:25:02 -07001119 if (summary != null) {
1120 final boolean alwaysShowSummary = getResources()
1121 .getBoolean(R.bool.always_show_summary);
1122 if (alwaysShowSummary) {
1123 summary.setText(root.getDirectoryString());
1124 summary.setVisibility(View.VISIBLE);
1125 hasLine2 = true;
1126 } else {
Jeff Sharkey8b997042013-09-19 15:25:56 -07001127 if (iconDrawable != null && roots.isIconUniqueBlocking(root)) {
Jeff Sharkey7d58fc62013-09-12 16:25:02 -07001128 // No summary needed if icon speaks for itself
1129 summary.setVisibility(View.INVISIBLE);
1130 } else {
1131 summary.setText(root.getDirectoryString());
1132 summary.setVisibility(View.VISIBLE);
1133 summary.setTextAlignment(TextView.TEXT_ALIGNMENT_TEXT_END);
1134 hasLine2 = true;
1135 }
1136 }
Jeff Sharkeya35ac2d2013-09-10 12:04:26 -07001137 }
Jeff Sharkeyac9e6272013-08-31 21:27:44 -07001138 } else {
Jeff Sharkey9656a532013-09-13 13:42:19 -07001139 // Directories showing thumbnails in grid mode get a little icon
1140 // hint to remind user they're a directory.
1141 if (Document.MIME_TYPE_DIR.equals(docMimeType) && state.derivedMode == MODE_GRID
1142 && showThumbnail) {
Steve McKayef280152015-06-11 10:10:49 -07001143 iconDrawable = IconUtils.applyTintAttr(mContext, R.drawable.ic_doc_folder,
Jeff Sharkey34c54092014-08-08 13:08:56 -07001144 android.R.attr.textColorPrimaryInverse);
Jeff Sharkey9656a532013-09-13 13:42:19 -07001145 }
1146
Jeff Sharkey7d58fc62013-09-12 16:25:02 -07001147 if (summary != null) {
1148 if (docSummary != null) {
1149 summary.setText(docSummary);
1150 summary.setVisibility(View.VISIBLE);
1151 hasLine2 = true;
1152 } else {
1153 summary.setVisibility(View.INVISIBLE);
1154 }
Jeff Sharkeyd82b26b2013-09-02 15:07:28 -07001155 }
Jeff Sharkey2e694f82013-08-06 16:26:14 -07001156 }
1157
Jeff Sharkey9656a532013-09-13 13:42:19 -07001158 if (icon1 != null) icon1.setVisibility(View.GONE);
1159 if (icon2 != null) icon2.setVisibility(View.GONE);
1160
1161 if (iconDrawable != null) {
1162 if (hasLine1) {
1163 icon1.setVisibility(View.VISIBLE);
1164 icon1.setImageDrawable(iconDrawable);
1165 } else {
1166 icon2.setVisibility(View.VISIBLE);
1167 icon2.setImageDrawable(iconDrawable);
1168 }
1169 }
1170
Jeff Sharkeyac9e6272013-08-31 21:27:44 -07001171 if (docLastModified == -1) {
Jeff Sharkey8a8fb672013-05-07 12:41:33 -07001172 date.setText(null);
1173 } else {
Steve McKayef280152015-06-11 10:10:49 -07001174 date.setText(formatTime(mContext, docLastModified));
Jeff Sharkey42d26792013-09-06 13:22:09 -07001175 hasLine2 = true;
Jeff Sharkey8a8fb672013-05-07 12:41:33 -07001176 }
Jeff Sharkey2e694f82013-08-06 16:26:14 -07001177
1178 if (state.showSize) {
1179 size.setVisibility(View.VISIBLE);
Jeff Sharkeyac9e6272013-08-31 21:27:44 -07001180 if (Document.MIME_TYPE_DIR.equals(docMimeType) || docSize == -1) {
Jeff Sharkey2e694f82013-08-06 16:26:14 -07001181 size.setText(null);
1182 } else {
Steve McKayef280152015-06-11 10:10:49 -07001183 size.setText(Formatter.formatFileSize(mContext, docSize));
Jeff Sharkey42d26792013-09-06 13:22:09 -07001184 hasLine2 = true;
Jeff Sharkey2e694f82013-08-06 16:26:14 -07001185 }
1186 } else {
1187 size.setVisibility(View.GONE);
Jeff Sharkey09c10bf2013-06-30 20:02:59 -07001188 }
Jeff Sharkeya5defe32013-08-05 17:56:48 -07001189
Jeff Sharkey9656a532013-09-13 13:42:19 -07001190 if (line1 != null) {
1191 line1.setVisibility(hasLine1 ? View.VISIBLE : View.GONE);
1192 }
Jeff Sharkey7d58fc62013-09-12 16:25:02 -07001193 if (line2 != null) {
1194 line2.setVisibility(hasLine2 ? View.VISIBLE : View.GONE);
1195 }
Jeff Sharkey42d26792013-09-06 13:22:09 -07001196
Steve McKayef280152015-06-11 10:10:49 -07001197 setEnabledRecursive(itemView, enabled);
Jeff Sharkey7e544612014-08-29 15:38:27 -07001198
1199 iconMime.setAlpha(iconAlpha);
1200 iconThumb.setAlpha(iconAlpha);
1201 if (icon1 != null) icon1.setAlpha(iconAlpha);
1202 if (icon2 != null) icon2.setAlpha(iconAlpha);
Jeff Sharkeyaed873d2013-09-09 16:51:06 -07001203
Steve McKay8e258c62015-05-06 14:27:57 -07001204 if (DEBUG_ENABLE_DND) {
Steve McKayef280152015-06-11 10:10:49 -07001205 setupDragAndDropOnDocumentView(itemView, cursor);
Steve McKay8e258c62015-05-06 14:27:57 -07001206 }
Jeff Sharkeye22d02e2013-04-26 16:54:55 -07001207 }
Jeff Sharkey09c10bf2013-06-30 20:02:59 -07001208
Steve McKay351a7492015-08-04 10:11:01 -07001209 @Override
Steve McKayef280152015-06-11 10:10:49 -07001210 public int getItemCount() {
Ben Kwa24be5d32015-08-27 16:04:46 -07001211 return mModel.getItemCount();
Steve McKayef280152015-06-11 10:10:49 -07001212 // return mCursorCount + mFooters.size();
Jeff Sharkeyaed873d2013-09-09 16:51:06 -07001213 }
1214
1215 @Override
Jeff Sharkey20b32272013-09-03 15:25:52 -07001216 public int getItemViewType(int position) {
Ben Kwa24be5d32015-08-27 16:04:46 -07001217 final int itemCount = mModel.getItemCount();
1218 if (position < itemCount) {
Jeff Sharkey20b32272013-09-03 15:25:52 -07001219 return 0;
1220 } else {
Ben Kwa24be5d32015-08-27 16:04:46 -07001221 position -= itemCount;
Jeff Sharkeyaed873d2013-09-09 16:51:06 -07001222 return mFooters.get(position).getItemViewType();
Jeff Sharkey20b32272013-09-03 15:25:52 -07001223 }
1224 }
Jeff Sharkey8a8fb672013-05-07 12:41:33 -07001225 }
1226
1227 private static String formatTime(Context context, long when) {
1228 // TODO: DateUtils should make this easier
1229 Time then = new Time();
1230 then.set(when);
1231 Time now = new Time();
1232 now.setToNow();
1233
1234 int flags = DateUtils.FORMAT_NO_NOON | DateUtils.FORMAT_NO_MIDNIGHT
1235 | DateUtils.FORMAT_ABBREV_ALL;
1236
1237 if (then.year != now.year) {
1238 flags |= DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE;
1239 } else if (then.yearDay != now.yearDay) {
1240 flags |= DateUtils.FORMAT_SHOW_DATE;
1241 } else {
1242 flags |= DateUtils.FORMAT_SHOW_TIME;
1243 }
1244
1245 return DateUtils.formatDateTime(context, when, flags);
1246 }
Jeff Sharkeyb3620442013-09-01 18:41:04 -07001247
1248 private String findCommonMimeType(List<String> mimeTypes) {
1249 String[] commonType = mimeTypes.get(0).split("/");
1250 if (commonType.length != 2) {
1251 return "*/*";
1252 }
1253
1254 for (int i = 1; i < mimeTypes.size(); i++) {
1255 String[] type = mimeTypes.get(i).split("/");
1256 if (type.length != 2) continue;
1257
1258 if (!commonType[1].equals(type[1])) {
1259 commonType[1] = "*";
1260 }
1261
1262 if (!commonType[0].equals(type[0])) {
1263 commonType[0] = "*";
1264 commonType[1] = "*";
1265 break;
1266 }
1267 }
1268
1269 return commonType[0] + "/" + commonType[1];
1270 }
Jeff Sharkeyaed873d2013-09-09 16:51:06 -07001271
1272 private void setEnabledRecursive(View v, boolean enabled) {
Jeff Sharkey9656a532013-09-13 13:42:19 -07001273 if (v == null) return;
Jeff Sharkeyaed873d2013-09-09 16:51:06 -07001274 if (v.isEnabled() == enabled) return;
1275 v.setEnabled(enabled);
1276
1277 if (v instanceof ViewGroup) {
1278 final ViewGroup vg = (ViewGroup) v;
1279 for (int i = vg.getChildCount() - 1; i >= 0; i--) {
1280 setEnabledRecursive(vg.getChildAt(i), enabled);
1281 }
1282 }
1283 }
Jeff Sharkey7cf49032013-09-26 10:54:16 -07001284
1285 private boolean isDocumentEnabled(String docMimeType, int docFlags) {
1286 final State state = getDisplayState(DirectoryFragment.this);
1287
Jeff Sharkey7cf49032013-09-26 10:54:16 -07001288 // Directories are always enabled
1289 if (Document.MIME_TYPE_DIR.equals(docMimeType)) {
1290 return true;
1291 }
1292
Jeff Sharkey783ebc22013-09-26 19:42:52 -07001293 // Read-only files are disabled when creating
1294 if (state.action == ACTION_CREATE && (docFlags & Document.FLAG_SUPPORTS_WRITE) == 0) {
1295 return false;
1296 }
1297
Jeff Sharkey7cf49032013-09-26 10:54:16 -07001298 return MimePredicate.mimeMatches(state.acceptMimes, docMimeType);
1299 }
Vladislav Kaznacheev89b90332015-05-01 13:46:57 -07001300
Steve McKay1f199482015-05-20 15:58:42 -07001301 private void copyFromClipboard() {
1302 new AsyncTask<Void, Void, List<DocumentInfo>>() {
1303
1304 @Override
1305 protected List<DocumentInfo> doInBackground(Void... params) {
1306 return mClipper.getClippedDocuments();
1307 }
1308
1309 @Override
1310 protected void onPostExecute(List<DocumentInfo> docs) {
1311 DocumentInfo destination =
1312 ((BaseActivity) getActivity()).getCurrentDirectory();
1313 copyDocuments(docs, destination);
1314 }
1315 }.execute();
Steve McKay0599a442015-05-05 14:50:00 -07001316 }
1317
Steve McKay1f199482015-05-20 15:58:42 -07001318 private void copyFromClipData(final ClipData clipData, final DocumentInfo destination) {
Steve McKayef280152015-06-11 10:10:49 -07001319 checkNotNull(clipData);
Steve McKay1f199482015-05-20 15:58:42 -07001320 new AsyncTask<Void, Void, List<DocumentInfo>>() {
Vladislav Kaznacheev89b90332015-05-01 13:46:57 -07001321
Steve McKay1f199482015-05-20 15:58:42 -07001322 @Override
1323 protected List<DocumentInfo> doInBackground(Void... params) {
1324 return mClipper.getDocumentsFromClipData(clipData);
1325 }
1326
1327 @Override
1328 protected void onPostExecute(List<DocumentInfo> docs) {
1329 copyDocuments(docs, destination);
1330 }
1331 }.execute();
1332 }
1333
1334 private void copyDocuments(final List<DocumentInfo> docs, final DocumentInfo destination) {
1335 if (!canCopy(docs, destination)) {
1336 Toast.makeText(
1337 getActivity(),
1338 R.string.clipboard_files_cannot_paste, Toast.LENGTH_SHORT).show();
Steve McKay0599a442015-05-05 14:50:00 -07001339 return;
1340 }
1341
Steve McKay1f199482015-05-20 15:58:42 -07001342 if (docs.isEmpty()) {
Vladislav Kaznacheev89b90332015-05-01 13:46:57 -07001343 return;
Vladislav Kaznacheeve16887a2015-05-05 12:09:47 -07001344 }
Vladislav Kaznacheev89b90332015-05-01 13:46:57 -07001345
Steve McKay1f199482015-05-20 15:58:42 -07001346 final DocumentStack curStack = getDisplayState(DirectoryFragment.this).stack;
Vladislav Kaznacheev89b90332015-05-01 13:46:57 -07001347 DocumentStack tmpStack = new DocumentStack();
Steve McKay1f199482015-05-20 15:58:42 -07001348 if (destination != null) {
1349 tmpStack.push(destination);
Vladislav Kaznacheev89b90332015-05-01 13:46:57 -07001350 tmpStack.addAll(curStack);
1351 } else {
1352 tmpStack = curStack;
1353 }
1354
Steve McKay1f199482015-05-20 15:58:42 -07001355 CopyService.start(getActivity(), docs, tmpStack, CopyService.TRANSFER_MODE_COPY);
Vladislav Kaznacheev89b90332015-05-01 13:46:57 -07001356 }
1357
1358 private ClipData getClipDataFromDocuments(List<DocumentInfo> docs) {
1359 Context context = getActivity();
1360 final ContentResolver resolver = context.getContentResolver();
1361 ClipData clipData = null;
1362 for (DocumentInfo doc : docs) {
1363 final Uri uri = DocumentsContract.buildDocumentUri(doc.authority, doc.documentId);
1364 if (clipData == null) {
Vladislav Kaznacheeve16887a2015-05-05 12:09:47 -07001365 // TODO: figure out what this string should be.
1366 // Currently it is not displayed anywhere in the UI, but this might change.
1367 final String label = "";
1368 clipData = ClipData.newUri(resolver, label, uri);
Vladislav Kaznacheev89b90332015-05-01 13:46:57 -07001369 } else {
1370 // TODO: update list of mime types in ClipData.
1371 clipData.addItem(new ClipData.Item(uri));
1372 }
1373 }
1374 return clipData;
1375 }
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001376
Steve McKay1f199482015-05-20 15:58:42 -07001377 void copySelectedToClipboard() {
Ben Kwa24be5d32015-08-27 16:04:46 -07001378 Selection sel = mModel.getSelection(new Selection());
Steve McKayef280152015-06-11 10:10:49 -07001379 copySelectionToClipboard(sel);
Steve McKay9276f3b2015-05-27 16:11:42 -07001380 }
Steve McKay0599a442015-05-05 14:50:00 -07001381
Steve McKayef280152015-06-11 10:10:49 -07001382 void copySelectionToClipboard(Selection items) {
Steve McKay9276f3b2015-05-27 16:11:42 -07001383 new GetDocumentsTask() {
1384 @Override
1385 void onDocumentsReady(List<DocumentInfo> docs) {
1386 mClipper.clipDocuments(docs);
Steve McKay1f199482015-05-20 15:58:42 -07001387 Activity activity = getActivity();
1388 Toast.makeText(activity,
1389 activity.getResources().getQuantityString(
1390 R.plurals.clipboard_files_clipped, docs.size(), docs.size()),
1391 Toast.LENGTH_SHORT).show();
Steve McKay9276f3b2015-05-27 16:11:42 -07001392 }
Steve McKayef280152015-06-11 10:10:49 -07001393 }.execute(items);
Steve McKay0599a442015-05-05 14:50:00 -07001394 }
1395
1396 void pasteFromClipboard() {
Steve McKay1f199482015-05-20 15:58:42 -07001397 copyFromClipboard();
1398 getActivity().invalidateOptionsMenu();
Steve McKay0599a442015-05-05 14:50:00 -07001399 }
1400
Steve McKay0599a442015-05-05 14:50:00 -07001401 /**
1402 * Returns true if the list of files can be copied to destination. Note that this
1403 * is a policy check only. Currently the method does not attempt to verify
1404 * available space or any other environmental aspects possibly resulting in
1405 * failure to copy.
1406 *
1407 * @return true if the list of files can be copied to destination.
1408 */
1409 boolean canCopy(List<DocumentInfo> files, DocumentInfo dest) {
Ben Kwa91923182015-08-27 16:06:33 -07001410 BaseActivity activity = (BaseActivity) getActivity();
Steve McKay0599a442015-05-05 14:50:00 -07001411
1412 final RootInfo root = activity.getCurrentRoot();
1413
1414 // Can't copy folders to Downloads.
1415 if (root.isDownloads()) {
1416 for (DocumentInfo docs : files) {
1417 if (docs.isDirectory()) {
1418 return false;
1419 }
1420 }
1421 }
1422
1423 return dest != null && dest.isDirectory() && dest.isCreateSupported();
1424 }
1425
1426 void selectAllFiles() {
Ben Kwa24be5d32015-08-27 16:04:46 -07001427 boolean changed = mModel.selectAll();
Steve McKay9459a7c2015-07-24 13:14:20 -07001428 if (changed) {
1429 updateDisplayState();
1430 }
Steve McKay0599a442015-05-05 14:50:00 -07001431 }
1432
Steve McKayef280152015-06-11 10:10:49 -07001433 private void setupDragAndDropOnDirectoryView(View view) {
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001434 // Listen for drops on non-directory items and empty space.
1435 view.setOnDragListener(mOnDragListener);
1436 }
1437
1438 private void setupDragAndDropOnDocumentView(View view, Cursor cursor) {
1439 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
1440 if (Document.MIME_TYPE_DIR.equals(docMimeType)) {
1441 // Make a directory item a drop target. Drop on non-directories and empty space
1442 // is handled at the list/grid view level.
1443 view.setOnDragListener(mOnDragListener);
1444 }
1445
1446 // Temporary: attaching the listener to the title only.
1447 // Attaching to the entire item conflicts with the item long click handler responsible
1448 // for item selection.
1449 final View title = view.findViewById(android.R.id.title);
1450 title.setOnLongClickListener(mLongClickListener);
1451 }
1452
1453 private View.OnDragListener mOnDragListener = new View.OnDragListener() {
1454 @Override
1455 public boolean onDrag(View v, DragEvent event) {
1456 switch (event.getAction()) {
1457 case DragEvent.ACTION_DRAG_STARTED:
1458 // TODO: Check if the event contains droppable data.
1459 return true;
1460
1461 // TODO: Highlight potential drop target directory?
1462 // TODO: Expand drop target directory on hover?
1463 case DragEvent.ACTION_DRAG_ENTERED:
1464 case DragEvent.ACTION_DRAG_LOCATION:
1465 case DragEvent.ACTION_DRAG_EXITED:
1466 case DragEvent.ACTION_DRAG_ENDED:
1467 return true;
1468
1469 case DragEvent.ACTION_DROP:
Vladislav Kaznacheev9400b892015-09-04 09:17:37 -07001470 int dstPosition = mRecView.getChildAdapterPosition(getContainingItemView(v));
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001471 DocumentInfo dstDir = null;
1472 if (dstPosition != android.widget.AdapterView.INVALID_POSITION) {
Ben Kwa24be5d32015-08-27 16:04:46 -07001473 Cursor dstCursor = mModel.getItem(dstPosition);
Steve McKayef280152015-06-11 10:10:49 -07001474 checkNotNull(dstCursor, "Cursor cannot be null.");
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001475 dstDir = DocumentInfo.fromDirectoryCursor(dstCursor);
1476 // TODO: Do not drop into the directory where the documents came from.
1477 }
1478 copyFromClipData(event.getClipData(), dstDir);
1479 return true;
1480 }
1481 return false;
1482 }
1483 };
1484
Vladislav Kaznacheev9400b892015-09-04 09:17:37 -07001485 private View getContainingItemView(View view) {
1486 while (true) {
1487 if (view.getLayoutParams() instanceof RecyclerView.LayoutParams) {
1488 return view;
1489 }
1490 ViewParent parent = view.getParent();
1491 if (parent == null || !(parent instanceof View)) {
1492 return null;
1493 }
1494 view = (View) parent;
1495 }
1496 }
1497
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001498 private View.OnLongClickListener mLongClickListener = new View.OnLongClickListener() {
1499 @Override
1500 public boolean onLongClick(View v) {
1501 final List<DocumentInfo> docs = getDraggableDocuments(v);
1502 if (docs.isEmpty()) {
1503 return false;
1504 }
1505 v.startDrag(
1506 getClipDataFromDocuments(docs),
1507 new DrawableShadowBuilder(getDragShadowIcon(docs)),
1508 null,
1509 View.DRAG_FLAG_GLOBAL
1510 );
1511 return true;
1512 }
1513 };
1514
1515 private List<DocumentInfo> getDraggableDocuments(View currentItemView) {
Vladislav Kaznacheev9400b892015-09-04 09:17:37 -07001516 int position = mRecView.getChildAdapterPosition(getContainingItemView(currentItemView));
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001517 if (position == android.widget.AdapterView.INVALID_POSITION) {
1518 return Collections.EMPTY_LIST;
1519 }
1520
Ben Kwa24be5d32015-08-27 16:04:46 -07001521 final List<DocumentInfo> selectedDocs = mModel.getSelectedDocuments();
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001522 if (!selectedDocs.isEmpty()) {
Ben Kwa24be5d32015-08-27 16:04:46 -07001523 if (!mModel.isSelected(position)) {
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001524 // There is a selection that does not include the current item, drag nothing.
1525 return Collections.EMPTY_LIST;
1526 }
1527 return selectedDocs;
1528 }
1529
Ben Kwa24be5d32015-08-27 16:04:46 -07001530 final Cursor cursor = mModel.getItem(position);
Steve McKayef280152015-06-11 10:10:49 -07001531 checkNotNull(cursor, "Cursor cannot be null.");
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001532 final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
Steve McKayfefcd702015-08-20 16:19:38 +00001533
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001534 return Lists.newArrayList(doc);
1535 }
1536
1537 private Drawable getDragShadowIcon(List<DocumentInfo> docs) {
1538 if (docs.size() == 1) {
1539 final DocumentInfo doc = docs.get(0);
1540 return getDocumentIcon(getActivity(), doc.authority, doc.documentId,
1541 doc.mimeType, doc.icon, getDisplayState(this));
1542 }
1543 return getActivity().getDrawable(R.drawable.ic_doc_generic);
1544 }
1545
1546 public static Drawable getDocumentIcon(Context context, String docAuthority, String docId,
1547 String docMimeType, int docIcon, State state) {
1548 if (docIcon != 0) {
1549 return IconUtils.loadPackageIcon(context, docAuthority, docIcon);
1550 } else {
1551 return IconUtils.loadMimeIcon(context, docMimeType, docAuthority, docId,
1552 state.derivedMode);
1553 }
1554 }
1555
Steve McKayef280152015-06-11 10:10:49 -07001556 private static class ThumbnailAsyncTask extends AsyncTask<Uri, Void, Bitmap>
1557 implements Preemptable {
1558 private final Uri mUri;
1559 private final ImageView mIconMime;
1560 private final ImageView mIconThumb;
1561 private final Point mThumbSize;
1562 private final float mTargetAlpha;
1563 private final CancellationSignal mSignal;
1564
1565 public ThumbnailAsyncTask(Uri uri, ImageView iconMime, ImageView iconThumb, Point thumbSize,
1566 float targetAlpha) {
1567 mUri = uri;
1568 mIconMime = iconMime;
1569 mIconThumb = iconThumb;
1570 mThumbSize = thumbSize;
1571 mTargetAlpha = targetAlpha;
1572 mSignal = new CancellationSignal();
1573 }
1574
1575 @Override
1576 public void preempt() {
1577 cancel(false);
1578 mSignal.cancel();
1579 }
1580
1581 @Override
1582 protected Bitmap doInBackground(Uri... params) {
1583 if (isCancelled()) return null;
1584
1585 final Context context = mIconThumb.getContext();
1586 final ContentResolver resolver = context.getContentResolver();
1587
1588 ContentProviderClient client = null;
1589 Bitmap result = null;
1590 try {
1591 client = DocumentsApplication.acquireUnstableProviderOrThrow(
1592 resolver, mUri.getAuthority());
1593 result = DocumentsContract.getDocumentThumbnail(client, mUri, mThumbSize, mSignal);
1594 if (result != null) {
1595 final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache(
1596 context, mThumbSize);
1597 thumbs.put(mUri, result);
1598 }
1599 } catch (Exception e) {
1600 if (!(e instanceof OperationCanceledException)) {
1601 Log.w(TAG, "Failed to load thumbnail for " + mUri + ": " + e);
1602 }
1603 } finally {
1604 ContentProviderClient.releaseQuietly(client);
1605 }
1606 return result;
1607 }
1608
1609 @Override
1610 protected void onPostExecute(Bitmap result) {
1611 if (mIconThumb.getTag() == this && result != null) {
1612 mIconThumb.setTag(null);
1613 mIconThumb.setImageBitmap(result);
1614
1615 mIconMime.setAlpha(mTargetAlpha);
1616 mIconMime.animate().alpha(0f).start();
1617 mIconThumb.setAlpha(0f);
1618 mIconThumb.animate().alpha(mTargetAlpha).start();
1619 }
1620 }
1621 }
1622
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001623 private class DrawableShadowBuilder extends View.DragShadowBuilder {
1624
1625 private final Drawable mShadow;
1626
1627 private final int mShadowDimension;
1628
1629 public DrawableShadowBuilder(Drawable shadow) {
1630 mShadow = shadow;
1631 mShadowDimension = getResources().getDimensionPixelSize(
1632 R.dimen.drag_shadow_size);
1633 mShadow.setBounds(0, 0, mShadowDimension, mShadowDimension);
1634 }
1635
Ben Kwa24be5d32015-08-27 16:04:46 -07001636 @Override
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001637 public void onProvideShadowMetrics(
1638 Point shadowSize, Point shadowTouchPoint) {
1639 shadowSize.set(mShadowDimension, mShadowDimension);
1640 shadowTouchPoint.set(mShadowDimension / 2, mShadowDimension / 2);
1641 }
1642
Ben Kwa24be5d32015-08-27 16:04:46 -07001643 @Override
Vladislav Kaznacheevb6da7222015-05-01 14:18:57 -07001644 public void onDrawShadow(Canvas canvas) {
1645 mShadow.draw(canvas);
1646 }
1647 }
Steve McKay1f199482015-05-20 15:58:42 -07001648
1649 private FragmentTuner pickFragmentTuner(final State state) {
1650 return state.action == ACTION_BROWSE_ALL
Steve McKay0fbfc652015-08-20 16:48:49 -07001651 ? new FilesTuner()
Steve McKay1f199482015-05-20 15:58:42 -07001652 : new DefaultTuner(state);
1653 }
1654
1655 /**
1656 * Interface for specializing the Fragment for the "host" Activity.
1657 * Feel free to expand the role of this class to handle other specializations.
1658 */
1659 private interface FragmentTuner {
Steve McKay4f4232d2015-07-22 12:13:46 -07001660 void updateActionMenu(Menu menu, int dirType, boolean canDelete);
Kyle Horimoto426bd0d2015-07-29 15:33:49 -07001661 void afterActivityCreated(DirectoryFragment fragment);
Steve McKay1f199482015-05-20 15:58:42 -07001662 }
1663
1664 /**
Steve McKay9276f3b2015-05-27 16:11:42 -07001665 * Abstract task providing support for loading documents *off*
1666 * the main thread. And if it isn't obvious, creating a list
1667 * of documents (especially large lists) can be pretty expensive.
1668 */
1669 private abstract class GetDocumentsTask
Steve McKayef280152015-06-11 10:10:49 -07001670 extends AsyncTask<Selection, Void, List<DocumentInfo>> {
Steve McKay9276f3b2015-05-27 16:11:42 -07001671 @Override
Steve McKayef280152015-06-11 10:10:49 -07001672 protected final List<DocumentInfo> doInBackground(Selection... selected) {
Ben Kwa24be5d32015-08-27 16:04:46 -07001673 return mModel.getDocuments(selected[0]);
Steve McKay9276f3b2015-05-27 16:11:42 -07001674 }
1675
1676 @Override
1677 protected final void onPostExecute(List<DocumentInfo> docs) {
1678 onDocumentsReady(docs);
1679 }
1680
1681 abstract void onDocumentsReady(List<DocumentInfo> docs);
1682 }
1683
1684 /**
Steve McKay1f199482015-05-20 15:58:42 -07001685 * Provides support for Platform specific specializations of DirectoryFragment.
1686 */
1687 private static final class DefaultTuner implements FragmentTuner {
1688
1689 private final State mState;
1690
1691 public DefaultTuner(State state) {
1692 mState = state;
1693 }
1694
1695 @Override
Steve McKay4f4232d2015-07-22 12:13:46 -07001696 public void updateActionMenu(Menu menu, int dirType, boolean canDelete) {
Steve McKay1f199482015-05-20 15:58:42 -07001697 Preconditions.checkState(mState.action != ACTION_BROWSE_ALL);
1698
1699 final MenuItem open = menu.findItem(R.id.menu_open);
1700 final MenuItem share = menu.findItem(R.id.menu_share);
1701 final MenuItem delete = menu.findItem(R.id.menu_delete);
1702 final MenuItem copyTo = menu.findItem(R.id.menu_copy_to);
1703 final MenuItem moveTo = menu.findItem(R.id.menu_move_to);
1704 final MenuItem copyToClipboard = menu.findItem(R.id.menu_copy_to_clipboard);
1705
1706 final boolean manageOrBrowse = (mState.action == ACTION_MANAGE
1707 || mState.action == ACTION_BROWSE);
1708
1709 open.setVisible(!manageOrBrowse);
1710 share.setVisible(manageOrBrowse);
Steve McKay4f4232d2015-07-22 12:13:46 -07001711 delete.setVisible(manageOrBrowse && canDelete);
Steve McKay1f199482015-05-20 15:58:42 -07001712 // Disable copying from the Recents view.
1713 copyTo.setVisible(manageOrBrowse && dirType != TYPE_RECENT_OPEN);
1714 moveTo.setVisible(SystemProperties.getBoolean("debug.documentsui.enable_move", false));
1715
Steve McKay0fbfc652015-08-20 16:48:49 -07001716 // Only shown in files mode.
Steve McKay1f199482015-05-20 15:58:42 -07001717 copyToClipboard.setVisible(false);
1718 }
Kyle Horimoto426bd0d2015-07-29 15:33:49 -07001719
1720 @Override
1721 public void afterActivityCreated(DirectoryFragment fragment) {}
Steve McKay1f199482015-05-20 15:58:42 -07001722 }
1723
1724 /**
Steve McKay0fbfc652015-08-20 16:48:49 -07001725 * Provides support for Files activity specific specializations of DirectoryFragment.
Steve McKay1f199482015-05-20 15:58:42 -07001726 */
Steve McKay0fbfc652015-08-20 16:48:49 -07001727 private static final class FilesTuner implements FragmentTuner {
Steve McKay1f199482015-05-20 15:58:42 -07001728 @Override
Steve McKay4f4232d2015-07-22 12:13:46 -07001729 public void updateActionMenu(Menu menu, int dirType, boolean canDelete) {
Steve McKay1f199482015-05-20 15:58:42 -07001730 menu.findItem(R.id.menu_share).setVisible(true);
Steve McKay4f4232d2015-07-22 12:13:46 -07001731 menu.findItem(R.id.menu_delete).setVisible(canDelete);
Steve McKay1f199482015-05-20 15:58:42 -07001732 menu.findItem(R.id.menu_copy_to_clipboard).setVisible(true);
1733
1734 menu.findItem(R.id.menu_open).setVisible(false);
1735 menu.findItem(R.id.menu_copy_to).setVisible(false);
1736 menu.findItem(R.id.menu_move_to).setVisible(false);
1737 }
Kyle Horimoto426bd0d2015-07-29 15:33:49 -07001738
1739 @Override
Kyle Horimoto62a7fd02015-08-18 13:25:29 -07001740 public void afterActivityCreated(DirectoryFragment fragment) {}
Steve McKay1f199482015-05-20 15:58:42 -07001741 }
Ben Kwa24be5d32015-08-27 16:04:46 -07001742
1743 /**
1744 * The data model for the current loaded directory.
1745 */
Ben Kwa7461a952015-09-01 11:03:01 -07001746 @VisibleForTesting
1747 public static final class Model implements DocumentContext {
Ben Kwa24be5d32015-08-27 16:04:46 -07001748 private MultiSelectManager mSelectionManager;
Ben Kwa7461a952015-09-01 11:03:01 -07001749 private Context mContext;
Ben Kwa24be5d32015-08-27 16:04:46 -07001750 private int mCursorCount;
1751 private boolean mIsLoading;
Ben Kwa7461a952015-09-01 11:03:01 -07001752 private SparseBooleanArray mMarkedForDeletion = new SparseBooleanArray();
1753 private UpdateListener mUpdateListener;
Ben Kwa24be5d32015-08-27 16:04:46 -07001754 @Nullable private Cursor mCursor;
1755 @Nullable private String info;
1756 @Nullable private String error;
Ben Kwa7461a952015-09-01 11:03:01 -07001757
1758 Model(Context context, MultiSelectManager selectionManager) {
1759 mContext = context;
1760 mSelectionManager = selectionManager;
1761 }
Ben Kwa24be5d32015-08-27 16:04:46 -07001762
1763 /**
1764 * Sets the selection manager used by the model.
1765 * TODO: the model should instantiate the selection manager. See onActivityCreated.
1766 */
1767 void setSelectionManager(MultiSelectManager mgr) {
1768 mSelectionManager = mgr;
1769 }
1770
1771 /**
1772 * Selects all files in the current directory.
1773 * @return true if the selection state changed for any files.
1774 */
1775 boolean selectAll() {
1776 return mSelectionManager.setItemsSelected(0, mCursorCount, true);
1777 }
1778
1779 /**
1780 * Clones the current selection into the given Selection object.
1781 * @param selection
1782 * @return The selection that was passed in, for convenience.
1783 */
1784 Selection getSelection(Selection selection) {
1785 return mSelectionManager.getSelection(selection);
1786 }
1787
1788 /**
1789 * @return The current selection (the live instance, not a copy).
1790 */
1791 Selection getSelection() {
1792 return mSelectionManager.getSelection();
1793 }
1794
1795 boolean isSelected(int position) {
1796 return mSelectionManager.getSelection().contains(position);
1797 }
1798
1799 void clearSelection() {
1800 mSelectionManager.clearSelection();
1801 }
1802
1803 void update(DirectoryResult result) {
1804 if (DEBUG) Log.i(TAG, "Updating model with new result set.");
1805
1806 if (result == null) {
1807 mCursor = null;
1808 mCursorCount = 0;
1809 info = null;
1810 error = null;
1811 mIsLoading = false;
Ben Kwa7461a952015-09-01 11:03:01 -07001812 if (mUpdateListener != null) mUpdateListener.onModelUpdate(this);
Ben Kwa24be5d32015-08-27 16:04:46 -07001813 return;
1814 }
1815
1816 if (result.exception != null) {
1817 Log.e(TAG, "Error while loading directory contents", result.exception);
Ben Kwa7461a952015-09-01 11:03:01 -07001818 if (mUpdateListener != null) mUpdateListener.onModelUpdateFailed(result.exception);
Ben Kwa24be5d32015-08-27 16:04:46 -07001819 return;
1820 }
1821
1822 mCursor = result.cursor;
1823 mCursorCount = mCursor.getCount();
1824
1825 final Bundle extras = mCursor.getExtras();
1826 if (extras != null) {
1827 info = extras.getString(DocumentsContract.EXTRA_INFO);
1828 error = extras.getString(DocumentsContract.EXTRA_ERROR);
1829 mIsLoading = extras.getBoolean(DocumentsContract.EXTRA_LOADING, false);
1830 }
Ben Kwa7461a952015-09-01 11:03:01 -07001831
1832 if (mUpdateListener != null) mUpdateListener.onModelUpdate(this);
Ben Kwa24be5d32015-08-27 16:04:46 -07001833 }
1834
Ben Kwa7461a952015-09-01 11:03:01 -07001835 int getItemCount() {
Ben Kwa91923182015-08-27 16:06:33 -07001836 return mCursorCount - mMarkedForDeletion.size();
Ben Kwa24be5d32015-08-27 16:04:46 -07001837 }
1838
Ben Kwa7461a952015-09-01 11:03:01 -07001839 Cursor getItem(int position) {
Ben Kwa91923182015-08-27 16:06:33 -07001840 // Items marked for deletion are masked out of the UI. To do this, for every marked
1841 // item whose position is less than the requested item position, advance the requested
1842 // position by 1.
1843 final int originalPos = position;
1844 final int size = mMarkedForDeletion.size();
1845 for (int i = 0; i <= size; ++i) {
1846 // It'd be more concise, but less efficient, to iterate over positions while calling
1847 // mMarkedForDeletion.get. Instead, iterate over deleted entries.
1848 if (mMarkedForDeletion.keyAt(i) <= position && mMarkedForDeletion.valueAt(i)) {
1849 ++position;
1850 }
1851 }
1852
1853 if (DEBUG) {
1854 Log.d(TAG, "Item position adjusted for deletion. Original: " + originalPos
1855 + " Adjusted: " + position);
1856 }
1857
Ben Kwa24be5d32015-08-27 16:04:46 -07001858 if (position >= mCursorCount) {
1859 throw new IndexOutOfBoundsException("Attempt to retrieve " + position + " of " +
1860 mCursorCount + " items");
1861 }
1862
1863 mCursor.moveToPosition(position);
1864 return mCursor;
1865 }
1866
1867 private boolean isEmpty() {
1868 return mCursorCount == 0;
1869 }
1870
1871 private boolean isLoading() {
1872 return mIsLoading;
1873 }
1874
1875 private List<DocumentInfo> getSelectedDocuments() {
1876 Selection sel = getSelection(new Selection());
1877 return getDocuments(sel);
1878 }
1879
Ben Kwa7461a952015-09-01 11:03:01 -07001880 List<DocumentInfo> getDocuments(Selection items) {
Ben Kwa91923182015-08-27 16:06:33 -07001881 final int size = (items != null) ? items.size() : 0;
Ben Kwa24be5d32015-08-27 16:04:46 -07001882
Ben Kwa91923182015-08-27 16:06:33 -07001883 final List<DocumentInfo> docs = new ArrayList<>(size);
Ben Kwa24be5d32015-08-27 16:04:46 -07001884 for (int i = 0; i < size; i++) {
1885 final Cursor cursor = getItem(items.get(i));
1886 checkNotNull(cursor, "Cursor cannot be null.");
1887 final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
1888 docs.add(doc);
1889 }
1890 return docs;
1891 }
1892
1893 @Override
1894 public Cursor getCursor() {
1895 if (Looper.myLooper() != Looper.getMainLooper()) {
1896 throw new IllegalStateException("Can't call getCursor from non-main thread.");
1897 }
1898 return mCursor;
1899 }
Ben Kwa91923182015-08-27 16:06:33 -07001900
Ben Kwa7461a952015-09-01 11:03:01 -07001901 List<DocumentInfo> getDocumentsMarkedForDeletion() {
Ben Kwa91923182015-08-27 16:06:33 -07001902 final int size = mMarkedForDeletion.size();
1903 List<DocumentInfo> docs = new ArrayList<>(size);
1904
1905 for (int i = 0; i < size; ++i) {
1906 final int position = mMarkedForDeletion.keyAt(i);
1907 checkState(position < mCursorCount);
1908 mCursor.moveToPosition(position);
1909 final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(mCursor);
1910 docs.add(doc);
1911 }
1912 return docs;
1913 }
1914
1915 /**
1916 * Marks the given files for deletion. This will remove them from the UI. Clients must then
1917 * call either {@link #undoDeletion()} or {@link #finalizeDeletion()} to cancel or confirm
1918 * the deletion, respectively. Only one deletion operation is allowed at a time.
1919 *
1920 * @param selected A selection representing the files to delete.
1921 */
Ben Kwa7461a952015-09-01 11:03:01 -07001922 void markForDeletion(Selection selected) {
Ben Kwa91923182015-08-27 16:06:33 -07001923 // Only one deletion operation at a time.
1924 checkState(mMarkedForDeletion.size() == 0);
1925 // There should never be more to delete than what exists.
1926 checkState(mCursorCount >= selected.size());
1927
1928 final int size = selected.size();
1929 for (int i = 0; i < size; ++i) {
1930 int position = selected.get(i);
1931 if (DEBUG) Log.d(TAG, "Marked position " + position + " for deletion");
1932 mMarkedForDeletion.append(position, true);
Ben Kwa7461a952015-09-01 11:03:01 -07001933 if (mUpdateListener != null) mUpdateListener.notifyItemRemoved(position);
Ben Kwa91923182015-08-27 16:06:33 -07001934 }
1935 }
1936
1937 /**
1938 * Cancels an ongoing deletion operation. All files currently marked for deletion will be
1939 * unmarked, and restored in the UI. See {@link #markForDeletion(Selection)}.
1940 */
Ben Kwa7461a952015-09-01 11:03:01 -07001941 void undoDeletion() {
Ben Kwa91923182015-08-27 16:06:33 -07001942 // Iterate over deleted items, temporarily marking them false in the deletion list, and
1943 // re-adding them to the UI.
1944 final int size = mMarkedForDeletion.size();
1945 for (int i = 0; i < size; ++i) {
1946 final int position = mMarkedForDeletion.keyAt(i);
1947 mMarkedForDeletion.put(position, false);
Ben Kwa7461a952015-09-01 11:03:01 -07001948 if (mUpdateListener != null) mUpdateListener.notifyItemInserted(position);
Ben Kwa91923182015-08-27 16:06:33 -07001949 }
1950
1951 // Then, clear the deletion list.
1952 mMarkedForDeletion.clear();
1953 }
1954
1955 /**
1956 * Finalizes an ongoing deletion operation. All files currently marked for deletion will be
1957 * deleted. See {@link #markForDeletion(Selection)}.
Ben Kwa7461a952015-09-01 11:03:01 -07001958 *
1959 * @param view The view which will be used to interact with the user (e.g. surfacing
1960 * snackbars) for errors, info, etc.
Ben Kwa91923182015-08-27 16:06:33 -07001961 */
Ben Kwa7461a952015-09-01 11:03:01 -07001962 void finalizeDeletion(final View view) {
1963 final ContentResolver resolver = mContext.getContentResolver();
1964 DeleteFilesTask task = new DeleteFilesTask(
1965 resolver,
1966 new Runnable() {
1967 @Override
1968 public void run() {
1969 Snackbar.make(
1970 view,
1971 R.string.toast_failed_delete,
1972 Snackbar.LENGTH_LONG)
1973 .show();
1974
1975 }
1976 });
1977 task.execute();
Ben Kwa91923182015-08-27 16:06:33 -07001978 }
1979
1980 /**
1981 * A Task which collects the DocumentInfo for documents that have been marked for deletion,
1982 * and actually deletes them.
1983 */
1984 private class DeleteFilesTask extends AsyncTask<Void, Void, List<DocumentInfo>> {
1985 private ContentResolver mResolver;
Ben Kwa7461a952015-09-01 11:03:01 -07001986 private Runnable mErrorCallback;
Ben Kwa91923182015-08-27 16:06:33 -07001987
Ben Kwa7461a952015-09-01 11:03:01 -07001988 /**
1989 * @param resolver A ContentResolver for performing the actual file deletions.
1990 * @param errorCallback A Runnable that is executed in the event that one or more errors
1991 * occured while copying files. Execution will occur on the UI thread.
1992 */
1993 public DeleteFilesTask(ContentResolver resolver, Runnable errorCallback) {
Ben Kwa91923182015-08-27 16:06:33 -07001994 mResolver = resolver;
Ben Kwa7461a952015-09-01 11:03:01 -07001995 mErrorCallback = errorCallback;
Ben Kwa91923182015-08-27 16:06:33 -07001996 }
1997
1998 @Override
1999 protected List<DocumentInfo> doInBackground(Void... params) {
2000 return getDocumentsMarkedForDeletion();
2001 }
2002
2003 @Override
2004 protected void onPostExecute(List<DocumentInfo> docs) {
2005 boolean hadTrouble = false;
2006 for (DocumentInfo doc : docs) {
2007 if (!doc.isDeleteSupported()) {
2008 Log.w(TAG, doc + " could not be deleted. Skipping...");
2009 hadTrouble = true;
2010 continue;
2011 }
2012
2013 ContentProviderClient client = null;
2014 try {
2015 if (DEBUG) Log.d(TAG, "Deleting: " + doc.displayName);
2016 client = DocumentsApplication.acquireUnstableProviderOrThrow(
2017 mResolver, doc.derivedUri.getAuthority());
2018 DocumentsContract.deleteDocument(client, doc.derivedUri);
2019 } catch (Exception e) {
2020 Log.w(TAG, "Failed to delete " + doc);
2021 hadTrouble = true;
2022 } finally {
2023 ContentProviderClient.releaseQuietly(client);
2024 }
2025 }
2026
2027 if (hadTrouble) {
Ben Kwa7461a952015-09-01 11:03:01 -07002028 // TODO show which files failed? b/23720103
2029 mErrorCallback.run();
Ben Kwa91923182015-08-27 16:06:33 -07002030 if (DEBUG) Log.d(TAG, "Deletion task completed. Some deletions failed.");
2031 } else {
2032 if (DEBUG) Log.d(TAG, "Deletion task completed successfully.");
2033 }
2034 mMarkedForDeletion.clear();
2035 }
2036 }
Ben Kwa7461a952015-09-01 11:03:01 -07002037
2038 void addUpdateListener(UpdateListener listener) {
2039 checkState(mUpdateListener == null);
2040 mUpdateListener = listener;
2041 }
2042
2043 interface UpdateListener {
2044 /**
2045 * Called when a successful update has occurred.
2046 */
2047 void onModelUpdate(Model model);
2048
2049 /**
2050 * Called when an update has been attempted but failed.
2051 */
2052 void onModelUpdateFailed(Exception e);
2053
2054 /**
2055 * Called when an item has been removed from the model.
2056 */
2057 void notifyItemRemoved(int position);
2058
2059 /**
2060 * Called when an item has been added to the model.
2061 */
2062 void notifyItemInserted(int position);
2063 }
Ben Kwa24be5d32015-08-27 16:04:46 -07002064 }
Jeff Sharkeye22d02e2013-04-26 16:54:55 -07002065}