blob: d41c52100a9c19be0a8a09a16933d3a72a5acea6 [file] [log] [blame]
Steve McKayd0a2a2c2015-03-25 14:35:33 -07001/*
2 * Copyright (C) 2015 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
Steve McKayd9caa6a2016-09-15 16:36:45 -070019import static com.android.documentsui.base.Shared.DEBUG;
20import static com.android.documentsui.base.Shared.EXTRA_BENCHMARK;
21import static com.android.documentsui.base.State.ACTION_CREATE;
22import static com.android.documentsui.base.State.ACTION_GET_CONTENT;
23import static com.android.documentsui.base.State.ACTION_OPEN;
24import static com.android.documentsui.base.State.ACTION_OPEN_TREE;
25import static com.android.documentsui.base.State.ACTION_PICK_COPY_DESTINATION;
26import static com.android.documentsui.base.State.MODE_GRID;
Steve McKayb68dd222015-04-20 17:18:15 -070027
Steve McKayd0a2a2c2015-03-25 14:35:33 -070028import android.app.Activity;
29import android.app.Fragment;
Aga Wronskaaf5ace52016-02-17 13:50:42 -080030import android.app.FragmentManager;
Steve McKayb68dd222015-04-20 17:18:15 -070031import android.content.Intent;
Ben Kwa0bcdec32015-05-29 15:40:31 -070032import android.content.pm.ApplicationInfo;
33import android.content.pm.PackageInfo;
34import android.content.pm.PackageManager;
35import android.content.pm.ProviderInfo;
Daichi Hirono6ff85c32016-04-19 17:56:13 +090036import android.database.ContentObserver;
Steve McKayb68dd222015-04-20 17:18:15 -070037import android.net.Uri;
38import android.os.AsyncTask;
39import android.os.Bundle;
Tomasz Mikolajewskib8373c22016-03-15 17:41:31 +090040import android.os.Handler;
Tomasz Mikolajewskib8373c22016-03-15 17:41:31 +090041import android.os.MessageQueue.IdleHandler;
Steve McKayb68dd222015-04-20 17:18:15 -070042import android.provider.DocumentsContract;
43import android.provider.DocumentsContract.Root;
Steve McKayd4800812016-02-02 11:41:03 -080044import android.support.annotation.CallSuper;
Steve McKay0fbfc652015-08-20 16:48:49 -070045import android.support.annotation.LayoutRes;
Steve McKayfefcd702015-08-20 16:19:38 +000046import android.support.annotation.Nullable;
Steve McKayd9caa6a2016-09-15 16:36:45 -070047import android.support.annotation.VisibleForTesting;
Steve McKayb68dd222015-04-20 17:18:15 -070048import android.util.Log;
Ben Kwa74e5d412016-02-10 07:46:35 -080049import android.view.KeyEvent;
Steve McKayb68dd222015-04-20 17:18:15 -070050import android.view.Menu;
51import android.view.MenuItem;
Garfield, Tan171e6f52016-07-29 14:44:58 -070052import android.view.View;
Steve McKayd0a2a2c2015-03-25 14:35:33 -070053
Ben Linebf2a172016-06-03 13:46:52 -070054import com.android.documentsui.MenuManager.DirectoryDetails;
Ben Linb8c54e72016-06-10 12:13:27 -070055import com.android.documentsui.NavigationViewManager.Breadcrumb;
Aga Wronskaaf5ace52016-02-17 13:50:42 -080056import com.android.documentsui.SearchViewManager.SearchManagerListener;
Steve McKayd0805062016-09-15 14:30:38 -070057import com.android.documentsui.base.DocumentInfo;
58import com.android.documentsui.base.DocumentStack;
Steve McKayd9caa6a2016-09-15 16:36:45 -070059import com.android.documentsui.base.Events;
60import com.android.documentsui.base.LocalPreferences;
Steve McKayd0805062016-09-15 14:30:38 -070061import com.android.documentsui.base.PairedTask;
62import com.android.documentsui.base.RootInfo;
Steve McKayd9caa6a2016-09-15 16:36:45 -070063import com.android.documentsui.base.Shared;
64import com.android.documentsui.base.State;
65import com.android.documentsui.base.State.ViewMode;
Steve McKayfb4fd2f2016-03-11 10:49:32 -080066import com.android.documentsui.dirlist.AnimationView;
Steve McKayf68210e2015-11-03 15:23:16 -080067import com.android.documentsui.dirlist.DirectoryFragment;
Ben Lin7c35b032016-05-31 13:24:01 -070068import com.android.documentsui.dirlist.FragmentTuner;
Tomasz Mikolajewskid71bd612016-02-16 12:28:43 +090069import com.android.documentsui.dirlist.Model;
Steve McKay990f76e2016-09-16 12:36:58 -070070import com.android.documentsui.dirlist.MultiSelectManager;
Steve McKayd9caa6a2016-09-15 16:36:45 -070071import com.android.documentsui.dirlist.MultiSelectManager.Selection;
72import com.android.documentsui.roots.LoadRootTask;
73import com.android.documentsui.roots.RootsCache;
Garfield, Tana5588b62016-07-13 09:23:04 -070074import com.android.documentsui.services.FileOperationService;
75import com.android.documentsui.services.FileOperations;
Steve McKayd0805062016-09-15 14:30:38 -070076import com.android.documentsui.sidebar.RootsFragment;
Garfield, Tan171e6f52016-07-29 14:44:58 -070077import com.android.documentsui.sorting.SortController;
78import com.android.documentsui.sorting.SortModel;
Steve McKay64ac2512015-05-12 12:49:58 -070079
Steve McKay64ac2512015-05-12 12:49:58 -070080import java.io.FileNotFoundException;
Steve McKay64ac2512015-05-12 12:49:58 -070081import java.util.ArrayList;
82import java.util.Collection;
Daichi Hirono320a08f2016-03-25 19:04:39 +090083import java.util.Date;
Steve McKay64ac2512015-05-12 12:49:58 -070084import java.util.List;
85import java.util.concurrent.Executor;
86
Steve McKay18d01e82016-02-03 11:15:57 -080087public abstract class BaseActivity extends Activity
Ben Linb8c54e72016-06-10 12:13:27 -070088 implements SearchManagerListener, NavigationViewManager.Environment {
Steve McKayb68dd222015-04-20 17:18:15 -070089
Garfield, Tana5588b62016-07-13 09:23:04 -070090 public final FileOperations.Callback fileOpCallback = (status, opType, docCount) -> {
91 if (status == FileOperations.Callback.STATUS_REJECTED) {
92 Snackbars.showPasteFailed(this);
93 return;
94 }
95
96 if (docCount == 0) {
97 // Nothing has been pasted, so there is no need to show a snackbar.
98 return;
99 }
100
101 switch (opType) {
102 case FileOperationService.OPERATION_MOVE:
103 Snackbars.showMove(this, docCount);
104 break;
105 case FileOperationService.OPERATION_COPY:
106 Snackbars.showCopy(this, docCount);
107 break;
108 case FileOperationService.OPERATION_DELETE:
109 // We don't show anything for deletion.
110 break;
111 default:
112 throw new UnsupportedOperationException("Unsupported Operation: " + opType);
113 }
114 };
115
Tomasz Mikolajewskib8373c22016-03-15 17:41:31 +0900116 private static final String BENCHMARK_TESTING_PACKAGE = "com.android.documentsui.appperftests";
117
Steve McKay16e0c1f2016-09-15 12:41:13 -0700118 protected SearchViewManager mSearchManager;
Steve McKayd9caa6a2016-09-15 16:36:45 -0700119 protected State mState;
Steve McKay16e0c1f2016-09-15 12:41:13 -0700120
121 protected @Nullable RetainedState mRetainedState;
122 protected RootsCache mRoots;
123 protected DrawerController mDrawer;
124 protected NavigationViewManager mNavigator;
Tomasz Mikolajewskie29e3412016-02-24 12:53:44 +0900125 List<EventListener> mEventListeners = new ArrayList<>();
Steve McKay16e0c1f2016-09-15 12:41:13 -0700126 protected SortController mSortController;
Steve McKay0269fb62015-04-22 15:55:34 -0700127
Steve McKayf2c8b0d2015-09-23 15:44:24 -0700128 private final String mTag;
Daichi Hirono6ff85c32016-04-19 17:56:13 +0900129 private final ContentObserver mRootsCacheObserver = new ContentObserver(new Handler()) {
130 @Override
131 public void onChange(boolean selfChange) {
132 new HandleRootsChangedTask(BaseActivity.this).execute(getCurrentRoot());
133 }
134 };
Steve McKay95c79f52016-02-04 19:40:45 -0800135
Steve McKay0fbfc652015-08-20 16:48:49 -0700136 @LayoutRes
137 private int mLayoutId;
Steve McKayb68dd222015-04-20 17:18:15 -0700138
Ben Kwa74e5d412016-02-10 07:46:35 -0800139 private boolean mNavDrawerHasFocus;
Daichi Hirono320a08f2016-03-25 19:04:39 +0900140 private long mStartTime;
Ben Kwa74e5d412016-02-10 07:46:35 -0800141
Steve McKay990f76e2016-09-16 12:36:58 -0700142 /**
143 * Provides Activity a means of injection into and specialization of
144 * DirectoryFragment.
145 */
146 public abstract FragmentTuner getFragmentTuner(
147 Model model, MultiSelectManager selectionMgr, boolean mSearchMode);
148
149 /**
150 * Provides Activity a means of injection into and specialization of
151 * DirectoryFragment hosted menus.
152 */
153 public abstract MenuManager getMenuManager();
154
155 /**
156 * Provides Activity a means of injection into and specialization of
157 * DirectoryFragment.
158 */
159 public abstract DirectoryDetails getDirectoryDetails();
Garfield, Tan171e6f52016-07-29 14:44:58 -0700160
Tomasz Mikolajewskid71bd612016-02-16 12:28:43 +0900161 public abstract void onDocumentPicked(DocumentInfo doc, Model model);
Steve McKayd0a2a2c2015-03-25 14:35:33 -0700162 public abstract void onDocumentsPicked(List<DocumentInfo> docs);
Steve McKay351a7492015-08-04 10:11:01 -0700163
Steve McKay16e0c1f2016-09-15 12:41:13 -0700164 protected abstract void onTaskFinished(Uri... uris);
165 protected abstract void refreshDirectory(int anim);
Steve McKayc7dc0cf2016-02-04 12:15:22 -0800166 /** Allows sub-classes to include information in a newly created State instance. */
Steve McKay16e0c1f2016-09-15 12:41:13 -0700167 protected abstract void includeState(State initialState);
168 protected abstract void onDirectoryCreated(DocumentInfo doc);
Steve McKayb68dd222015-04-20 17:18:15 -0700169
Steve McKay0fbfc652015-08-20 16:48:49 -0700170 public BaseActivity(@LayoutRes int layoutId, String tag) {
171 mLayoutId = layoutId;
Steve McKayb68dd222015-04-20 17:18:15 -0700172 mTag = tag;
173 }
174
Steve McKay18d01e82016-02-03 11:15:57 -0800175 @CallSuper
Steve McKayb68dd222015-04-20 17:18:15 -0700176 @Override
177 public void onCreate(Bundle icicle) {
Daichi Hirono320a08f2016-03-25 19:04:39 +0900178 // Record the time when onCreate is invoked for metric.
179 mStartTime = new Date().getTime();
180
Steve McKayb68dd222015-04-20 17:18:15 -0700181 super.onCreate(icicle);
Steve McKay0fbfc652015-08-20 16:48:49 -0700182
Tomasz Mikolajewskib8373c22016-03-15 17:41:31 +0900183 final Intent intent = getIntent();
184
Daichi Hirono320a08f2016-03-25 19:04:39 +0900185 addListenerForLaunchCompletion();
Tomasz Mikolajewskib8373c22016-03-15 17:41:31 +0900186
Steve McKay18d01e82016-02-03 11:15:57 -0800187 setContentView(mLayoutId);
188
189 mDrawer = DrawerController.create(this);
Steve McKayc7dc0cf2016-02-04 12:15:22 -0800190 mState = getState(icicle);
Tomasz Mikolajewskib8373c22016-03-15 17:41:31 +0900191 Metrics.logActivityLaunch(this, mState, intent);
Ben Kwa1c0a3892016-01-26 11:50:03 -0800192
Steve McKay2bab2f82016-06-03 09:23:39 -0700193 // we're really interested in retainining state in our very complex
194 // DirectoryFragment. So we do a little code yoga to extend
195 // support to that fragment.
196 mRetainedState = (RetainedState) getLastNonConfigurationInstance();
Steve McKayb68dd222015-04-20 17:18:15 -0700197 mRoots = DocumentsApplication.getRootsCache(this);
Steve McKay18d01e82016-02-03 11:15:57 -0800198
Daichi Hirono6ff85c32016-04-19 17:56:13 +0900199 getContentResolver().registerContentObserver(
200 RootsCache.sNotificationUri, false, mRootsCacheObserver);
Steve McKay18d01e82016-02-03 11:15:57 -0800201
Garfield, Tan11d23482016-08-05 09:33:29 -0700202 mSearchManager = new SearchViewManager(this, icicle, mState.sortModel);
Steve McKay0fbfc652015-08-20 16:48:49 -0700203
Steve McKay18d01e82016-02-03 11:15:57 -0800204 DocumentsToolbar toolbar = (DocumentsToolbar) findViewById(R.id.toolbar);
205 setActionBar(toolbar);
Steve McKay85ec0d62016-06-24 15:05:08 -0700206
207 Breadcrumb breadcrumb =
208 Shared.findView(this, R.id.dropdown_breadcrumb, R.id.horizontal_breadcrumb);
209 assert(breadcrumb != null);
210
Ben Linb8c54e72016-06-10 12:13:27 -0700211 mNavigator = new NavigationViewManager(mDrawer, toolbar, mState, this, breadcrumb);
Steve McKay18d01e82016-02-03 11:15:57 -0800212
Steve McKay9de0da62016-08-25 15:18:23 -0700213 mSortController = SortController.create(this, mState.derivedMode, mState.sortModel);
214
Garfield, Tan171e6f52016-07-29 14:44:58 -0700215
Steve McKay0fbfc652015-08-20 16:48:49 -0700216 // Base classes must update result in their onCreate.
217 setResult(Activity.RESULT_CANCELED);
Steve McKay0269fb62015-04-22 15:55:34 -0700218 }
219
220 @Override
221 public boolean onCreateOptionsMenu(Menu menu) {
222 boolean showMenu = super.onCreateOptionsMenu(menu);
223
224 getMenuInflater().inflate(R.menu.activity, menu);
Aga Wronskaaf5ace52016-02-17 13:50:42 -0800225 mNavigator.update();
Aga Wronska8e21daa2016-03-24 18:22:09 -0700226 boolean fullBarSearch = getResources().getBoolean(R.bool.full_bar_search_view);
227 mSearchManager.install((DocumentsToolbar) findViewById(R.id.toolbar), fullBarSearch);
Steve McKay0269fb62015-04-22 15:55:34 -0700228
229 return showMenu;
Steve McKayb68dd222015-04-20 17:18:15 -0700230 }
231
Steve McKay7bd32e12015-04-30 16:12:59 -0700232 @Override
Steve McKayd4800812016-02-02 11:41:03 -0800233 @CallSuper
Steve McKay7bd32e12015-04-30 16:12:59 -0700234 public boolean onPrepareOptionsMenu(Menu menu) {
Steve McKaye9809272015-10-01 11:39:24 -0700235 super.onPrepareOptionsMenu(menu);
Steve McKayd4800812016-02-02 11:41:03 -0800236 mSearchManager.showMenu(canSearchRoot());
Steve McKaye9809272015-10-01 11:39:24 -0700237 return true;
Steve McKay7bd32e12015-04-30 16:12:59 -0700238 }
239
Daichi Hironoe28c3c82016-01-13 13:19:02 +0900240 @Override
241 protected void onDestroy() {
Daichi Hirono6ff85c32016-04-19 17:56:13 +0900242 getContentResolver().unregisterContentObserver(mRootsCacheObserver);
Daichi Hironoe28c3c82016-01-13 13:19:02 +0900243 super.onDestroy();
244 }
245
Steve McKayc7dc0cf2016-02-04 12:15:22 -0800246 private State getState(@Nullable Bundle icicle) {
247 if (icicle != null) {
Aga Wronskaaf5ace52016-02-17 13:50:42 -0800248 State state = icicle.<State>getParcelable(Shared.EXTRA_STATE);
Steve McKayc7dc0cf2016-02-04 12:15:22 -0800249 if (DEBUG) Log.d(mTag, "Recovered existing state object: " + state);
250 return state;
251 }
252
Ben Kwa0574b182015-09-08 07:31:19 -0700253 State state = new State();
254
255 final Intent intent = getIntent();
Ben Kwa0574b182015-09-08 07:31:19 -0700256
Garfield, Tan171e6f52016-07-29 14:44:58 -0700257 state.sortModel = SortModel.createModel();
Ben Kwa0574b182015-09-08 07:31:19 -0700258 state.localOnly = intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false);
Steve McKay459bc2b2015-09-16 15:07:31 -0700259 state.initAcceptMimes(intent);
Ben Kwa0574b182015-09-08 07:31:19 -0700260 state.excludedAuthorities = getExcludedAuthorities();
261
Aga Wronskaceb990e2016-03-30 18:07:59 -0700262 includeState(state);
263
Steve McKay0a2c49a2016-05-04 14:55:15 -0700264 // Advanced roots are shown by default without menu option if forced by config or intent.
Steve McKay91226ee2016-06-20 11:46:56 -0700265 boolean forceAdvanced = Shared.shouldShowDeviceRoot(this, intent);
266 boolean chosenAdvanced = LocalPreferences.getShowDeviceRoot(this, state.action);
267 state.showAdvanced = forceAdvanced || chosenAdvanced;
268
Aga Wronskaceb990e2016-03-30 18:07:59 -0700269 // Menu option is shown for whitelisted intents if advanced roots are not shown by default.
Steve McKay91226ee2016-06-20 11:46:56 -0700270 state.showAdvancedOption = !forceAdvanced && (
Steve McKay24917422016-05-10 15:01:39 -0700271 Shared.shouldShowFancyFeatures(this)
272 || state.action == ACTION_OPEN
273 || state.action == ACTION_CREATE
274 || state.action == ACTION_OPEN_TREE
275 || state.action == ACTION_PICK_COPY_DESTINATION
276 || state.action == ACTION_GET_CONTENT);
Aga Wronskaceb990e2016-03-30 18:07:59 -0700277
278 if (DEBUG) Log.d(mTag, "Created new state object: " + state);
279
Ben Kwa0574b182015-09-08 07:31:19 -0700280 return state;
281 }
282
Steve McKaycb9eb422016-02-09 16:17:24 -0800283 public void setRootsDrawerOpen(boolean open) {
284 mNavigator.revealRootsDrawer(open);
285 }
Steve McKayb68dd222015-04-20 17:18:15 -0700286
Steve McKayd0805062016-09-15 14:30:38 -0700287 public void onRootPicked(RootInfo root) {
Aga Wronskaaf5ace52016-02-17 13:50:42 -0800288 // Clicking on the current root removes search
289 mSearchManager.cancelSearch();
290
Aga Wronskafa421722016-02-08 12:00:38 -0800291 // Skip refreshing if root nor directory didn't change
292 if (root.equals(getCurrentRoot()) && mState.stack.size() == 1) {
Aga Wronskafd26e8d2016-02-02 18:01:58 -0800293 return;
294 }
295
Steve McKay7776aa52016-01-25 19:00:22 -0800296 mState.derivedMode = LocalPreferences.getViewMode(this, root, MODE_GRID);
Steve McKay743bc1b2016-08-26 16:10:39 -0700297 mSortController.onViewModeChanged(mState.derivedMode);
Steve McKay7776aa52016-01-25 19:00:22 -0800298
Garfield, Tan171e6f52016-07-29 14:44:58 -0700299 // Set summary header's visibility. Only recents and downloads root may have summary in
300 // their docs.
301 mState.sortModel.setDimensionVisibility(
302 SortModel.SORT_DIMENSION_ID_SUMMARY,
303 root.isRecents() || root.isDownloads() ? View.VISIBLE : View.INVISIBLE);
304
Steve McKayb68dd222015-04-20 17:18:15 -0700305 // Clear entire backstack and start in new root
Daichi Hirono2806beb2016-01-07 15:29:12 +0900306 mState.onRootChanged(root);
Steve McKay0269fb62015-04-22 15:55:34 -0700307
308 // Recents is always in memory, so we just load it directly.
309 // Otherwise we delegate loading data from disk to a task
310 // to ensure a responsive ui.
311 if (mRoots.isRecentsRoot(root)) {
Steve McKayfb4fd2f2016-03-11 10:49:32 -0800312 refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
Steve McKay0269fb62015-04-22 15:55:34 -0700313 } else {
Steve McKayc7dc0cf2016-02-04 12:15:22 -0800314 new PickRootTask(this, root).executeOnExecutor(getExecutorForCurrentDirectory());
Daichi Hirono3067d0d2015-12-25 11:08:42 +0900315 }
316 }
317
Steve McKayb68dd222015-04-20 17:18:15 -0700318 @Override
319 public boolean onOptionsItemSelected(MenuItem item) {
Steve McKayb68dd222015-04-20 17:18:15 -0700320
Steve McKay6035b3c2015-12-04 11:19:09 -0800321 switch (item.getItemId()) {
322 case android.R.id.home:
323 onBackPressed();
324 return true;
325
326 case R.id.menu_create_dir:
327 showCreateDirectoryDialog();
328 return true;
329
330 case R.id.menu_search:
Steve McKayf2eb8d92016-03-08 14:01:47 -0800331 // SearchViewManager listens for this directly.
Steve McKay6035b3c2015-12-04 11:19:09 -0800332 return false;
333
Steve McKay6035b3c2015-12-04 11:19:09 -0800334 case R.id.menu_grid:
Steve McKay7776aa52016-01-25 19:00:22 -0800335 setViewMode(State.MODE_GRID);
Steve McKay6035b3c2015-12-04 11:19:09 -0800336 return true;
337
338 case R.id.menu_list:
Steve McKay7776aa52016-01-25 19:00:22 -0800339 setViewMode(State.MODE_LIST);
Steve McKay6035b3c2015-12-04 11:19:09 -0800340 return true;
341
Aga Wronskaceb990e2016-03-30 18:07:59 -0700342 case R.id.menu_advanced:
343 setDisplayAdvancedDevices(!mState.showAdvanced);
344 return true;
345
Steve McKay6035b3c2015-12-04 11:19:09 -0800346 default:
347 return super.onOptionsItemSelected(item);
348 }
Steve McKayb68dd222015-04-20 17:18:15 -0700349 }
350
Steve McKay16e0c1f2016-09-15 12:41:13 -0700351 protected final @Nullable DirectoryFragment getDirectoryFragment() {
Steve McKayd4800812016-02-02 11:41:03 -0800352 return DirectoryFragment.get(getFragmentManager());
353 }
354
Steve McKay16e0c1f2016-09-15 12:41:13 -0700355 protected void showCreateDirectoryDialog() {
Aga Wronska94e53e42016-04-07 13:09:58 -0700356 Metrics.logUserAction(this, Metrics.USER_ACTION_CREATE_DIR);
357
Steve McKayceeb3f72015-05-19 16:10:25 -0700358 CreateDirectoryFragment.show(getFragmentManager());
359 }
360
361 /**
362 * Returns true if a directory can be created in the current location.
363 * @return
364 */
Steve McKay16e0c1f2016-09-15 12:41:13 -0700365 protected boolean canCreateDirectory() {
Steve McKayceeb3f72015-05-19 16:10:25 -0700366 final RootInfo root = getCurrentRoot();
367 final DocumentInfo cwd = getCurrentDirectory();
368 return cwd != null
369 && cwd.isCreateSupported()
370 && !mSearchManager.isSearching()
Steve McKaye9809272015-10-01 11:39:24 -0700371 && !root.isRecents()
Steve McKayceeb3f72015-05-19 16:10:25 -0700372 && !root.isDownloads();
373 }
374
Steve McKay16e0c1f2016-09-15 12:41:13 -0700375 protected void openContainerDocument(DocumentInfo doc) {
Steve McKay0af8afd2016-02-25 13:34:03 -0800376 assert(doc.isContainer());
377
Tomasz Mikolajewskie29e3412016-02-24 12:53:44 +0900378 notifyDirectoryNavigated(doc.derivedUri);
379
Daichi Hirono2806beb2016-01-07 15:29:12 +0900380 mState.pushDocument(doc);
Tomasz Mikolajewski2b5170b2016-02-03 15:49:58 +0900381 // Show an opening animation only if pressing "back" would get us back to the
382 // previous directory. Especially after opening a root document, pressing
383 // back, wouldn't go to the previous root, but close the activity.
384 final int anim = (mState.hasLocationChanged() && mState.stack.size() > 1)
Steve McKayfb4fd2f2016-03-11 10:49:32 -0800385 ? AnimationView.ANIM_ENTER : AnimationView.ANIM_NONE;
Tomasz Mikolajewski2b5170b2016-02-03 15:49:58 +0900386 refreshCurrentRootAndDirectory(anim);
Steve McKay351a7492015-08-04 10:11:01 -0700387 }
388
Steve McKayb68dd222015-04-20 17:18:15 -0700389 /**
Aga Wronska8788dad2016-01-15 17:30:15 -0800390 * Refreshes the content of the director and the menu/action bar.
391 * The current directory name and selection will get updated.
Steve McKayb68dd222015-04-20 17:18:15 -0700392 * @param anim
393 */
Steve McKay18d01e82016-02-03 11:15:57 -0800394 @Override
395 public final void refreshCurrentRootAndDirectory(int anim) {
Aga Wronska8788dad2016-01-15 17:30:15 -0800396 mSearchManager.cancelSearch();
397
Aga Wronska8788dad2016-01-15 17:30:15 -0800398 refreshDirectory(anim);
Steve McKayb68dd222015-04-20 17:18:15 -0700399
400 final RootsFragment roots = RootsFragment.get(getFragmentManager());
401 if (roots != null) {
402 roots.onCurrentRootChanged();
403 }
404
Steve McKay18d01e82016-02-03 11:15:57 -0800405 mNavigator.update();
Aga Wronska8788dad2016-01-15 17:30:15 -0800406 invalidateOptionsMenu();
407 }
408
Steve McKay16e0c1f2016-09-15 12:41:13 -0700409 protected final void loadRoot(final Uri uri) {
Steve McKayd9caa6a2016-09-15 16:36:45 -0700410 new LoadRootTask(this, mRoots, mState, uri).executeOnExecutor(
Steve McKayfd8425a2016-02-23 14:34:50 -0800411 ProviderExecutor.forAuthority(uri.getAuthority()));
412 }
413
Aga Wronska8788dad2016-01-15 17:30:15 -0800414 /**
Garfield, Tan804133e2016-04-20 15:13:56 -0700415 * This is called when user hovers over a doc for enough time during a drag n' drop, to open a
416 * folder that accepts drop. We should only open a container that's not an archive.
417 */
418 public void springOpenDirectory(DocumentInfo doc) {
419 }
420
421 /**
Aga Wronska8788dad2016-01-15 17:30:15 -0800422 * Called when search results changed.
423 * Refreshes the content of the directory. It doesn't refresh elements on the action bar.
424 * e.g. The current directory name displayed on the action bar won't get updated.
425 */
426 @Override
Aga Wronskaaf5ace52016-02-17 13:50:42 -0800427 public void onSearchChanged(@Nullable String query) {
428 // We should not get here if root is not searchable
Steve McKay0af8afd2016-02-25 13:34:03 -0800429 assert(canSearchRoot());
Aga Wronskaaf5ace52016-02-17 13:50:42 -0800430 reloadSearch(query);
Aga Wronska8e21daa2016-03-24 18:22:09 -0700431 }
432
433 @Override
434 public void onSearchFinished() {
435 // Restores menu icons state
Steve McKayf2eb8d92016-03-08 14:01:47 -0800436 invalidateOptionsMenu();
Aga Wronska8788dad2016-01-15 17:30:15 -0800437 }
438
Aga Wronskaaf5ace52016-02-17 13:50:42 -0800439 private void reloadSearch(String query) {
440 FragmentManager fm = getFragmentManager();
441 RootInfo root = getCurrentRoot();
442 DocumentInfo cwd = getCurrentDirectory();
443
444 DirectoryFragment.reloadSearch(fm, root, cwd, query);
Steve McKayb68dd222015-04-20 17:18:15 -0700445 }
446
Steve McKay16e0c1f2016-09-15 12:41:13 -0700447 private final List<String> getExcludedAuthorities() {
Ben Kwa0bcdec32015-05-29 15:40:31 -0700448 List<String> authorities = new ArrayList<>();
449 if (getIntent().getBooleanExtra(DocumentsContract.EXTRA_EXCLUDE_SELF, false)) {
450 // Exclude roots provided by the calling package.
451 String packageName = getCallingPackageMaybeExtra();
452 try {
453 PackageInfo pkgInfo = getPackageManager().getPackageInfo(packageName,
454 PackageManager.GET_PROVIDERS);
455 for (ProviderInfo provider: pkgInfo.providers) {
456 authorities.add(provider.authority);
457 }
458 } catch (PackageManager.NameNotFoundException e) {
459 Log.e(mTag, "Calling package name does not resolve: " + packageName);
460 }
461 }
462 return authorities;
463 }
464
Aga Wronska654e25c2016-01-29 11:41:41 -0800465 boolean canSearchRoot() {
466 final RootInfo root = getCurrentRoot();
467 return (root.flags & Root.FLAG_SUPPORTS_SEARCH) != 0;
468 }
469
Steve McKay16e0c1f2016-09-15 12:41:13 -0700470 public final String getCallingPackageMaybeExtra() {
Ben Kwa0bcdec32015-05-29 15:40:31 -0700471 String callingPackage = getCallingPackage();
472 // System apps can set the calling package name using an extra.
473 try {
474 ApplicationInfo info = getPackageManager().getApplicationInfo(callingPackage, 0);
475 if (info.isSystemApp() || info.isUpdatedSystemApp()) {
476 final String extra = getIntent().getStringExtra(DocumentsContract.EXTRA_PACKAGE_NAME);
477 if (extra != null) {
478 callingPackage = extra;
479 }
480 }
481 } finally {
482 return callingPackage;
483 }
Steve McKayb68dd222015-04-20 17:18:15 -0700484 }
Steve McKayd0a2a2c2015-03-25 14:35:33 -0700485
486 public static BaseActivity get(Fragment fragment) {
487 return (BaseActivity) fragment.getActivity();
488 }
489
Ben Kwa0574b182015-09-08 07:31:19 -0700490 public State getDisplayState() {
491 return mState;
492 }
493
Aga Wronska64ae1f42016-03-22 14:18:43 -0700494 /*
495 * Get the default directory to be presented after starting the activity.
496 * Method can be overridden if the change of the behavior of the the child activity is needed.
497 */
498 public Uri getDefaultRoot() {
Steve McKay17f7e582016-04-04 15:26:48 -0700499 return Shared.shouldShowDocumentsRoot(this, getIntent())
500 ? DocumentsContract.buildHomeUri()
501 : DocumentsContract.buildRootUri(
502 "com.android.providers.downloads.documents", "downloads");
503 }
Aga Wronskaceb990e2016-03-30 18:07:59 -0700504
Aga Wronska94e53e42016-04-07 13:09:58 -0700505 /**
506 * Set internal storage visible based on explicit user action.
507 */
Aga Wronskaceb990e2016-03-30 18:07:59 -0700508 void setDisplayAdvancedDevices(boolean display) {
Aga Wronska94e53e42016-04-07 13:09:58 -0700509 Metrics.logUserAction(this,
510 display ? Metrics.USER_ACTION_SHOW_ADVANCED : Metrics.USER_ACTION_HIDE_ADVANCED);
511
Steve McKay91226ee2016-06-20 11:46:56 -0700512 LocalPreferences.setShowDeviceRoot(this, mState.action, display);
Aga Wronskaceb990e2016-03-30 18:07:59 -0700513 mState.showAdvanced = display;
514 RootsFragment.get(getFragmentManager()).onDisplayStateChanged();
515 invalidateOptionsMenu();
Aga Wronska64ae1f42016-03-22 14:18:43 -0700516 }
517
Aga Wronska94e53e42016-04-07 13:09:58 -0700518 /**
Steve McKay7776aa52016-01-25 19:00:22 -0800519 * Set mode based on explicit user action.
Steve McKayb68dd222015-04-20 17:18:15 -0700520 */
Steve McKay7776aa52016-01-25 19:00:22 -0800521 void setViewMode(@ViewMode int mode) {
Aga Wronska94e53e42016-04-07 13:09:58 -0700522 if (mode == State.MODE_GRID) {
523 Metrics.logUserAction(this, Metrics.USER_ACTION_GRID);
524 } else if (mode == State.MODE_LIST) {
525 Metrics.logUserAction(this, Metrics.USER_ACTION_LIST);
526 }
527
Steve McKay30aabb82016-02-17 11:00:55 -0800528 LocalPreferences.setViewMode(this, getCurrentRoot(), mode);
Steve McKay7776aa52016-01-25 19:00:22 -0800529 mState.derivedMode = mode;
530
531 // view icon needs to be updated, but we *could* do it
532 // in onOptionsItemSelected, and not do the full invalidation
533 // But! That's a larger refactoring we'll save for another day.
534 invalidateOptionsMenu();
Steve McKayd4800812016-02-02 11:41:03 -0800535 DirectoryFragment dir = getDirectoryFragment();
536 if (dir != null) {
537 dir.onViewModeChanged();
Ben Kwa65f393a2016-02-17 10:48:57 -0800538 }
Garfield, Tan171e6f52016-07-29 14:44:58 -0700539
540 mSortController.onViewModeChanged(mode);
Steve McKayb68dd222015-04-20 17:18:15 -0700541 }
542
Aga Wronska3c237182016-01-20 16:32:33 -0800543 public void setPending(boolean pending) {
Steve McKay16e0c1f2016-09-15 12:41:13 -0700544 // TODO: Isolate this behavior to PickActivity.
Steve McKayb68dd222015-04-20 17:18:15 -0700545 }
546
547 @Override
548 protected void onSaveInstanceState(Bundle state) {
549 super.onSaveInstanceState(state);
Aga Wronskaaf5ace52016-02-17 13:50:42 -0800550 state.putParcelable(Shared.EXTRA_STATE, mState);
551 mSearchManager.onSaveInstanceState(state);
Steve McKayb68dd222015-04-20 17:18:15 -0700552 }
553
554 @Override
555 protected void onRestoreInstanceState(Bundle state) {
556 super.onRestoreInstanceState(state);
557 }
558
Steve McKay2bab2f82016-06-03 09:23:39 -0700559 /**
560 * Delegate ths call to the current fragment so it can save selection.
561 * Feel free to expand on this with other useful state.
562 */
563 @Override
564 public RetainedState onRetainNonConfigurationInstance() {
565 RetainedState retained = new RetainedState();
566 DirectoryFragment fragment = DirectoryFragment.get(getFragmentManager());
567 if (fragment != null) {
568 fragment.retainState(retained);
569 }
570 return retained;
571 }
572
573 public @Nullable RetainedState getRetainedState() {
574 return mRetainedState;
575 }
576
Steve McKay18d01e82016-02-03 11:15:57 -0800577 @Override
578 public boolean isSearchExpanded() {
579 return mSearchManager.isExpanded();
580 }
581
582 @Override
Steve McKayf68210e2015-11-03 15:23:16 -0800583 public RootInfo getCurrentRoot() {
Steve McKay4d0255f2015-09-25 16:02:56 -0700584 if (mState.stack.root != null) {
585 return mState.stack.root;
Steve McKayb68dd222015-04-20 17:18:15 -0700586 } else {
587 return mRoots.getRecentsRoot();
588 }
589 }
590
591 public DocumentInfo getCurrentDirectory() {
Steve McKay4d0255f2015-09-25 16:02:56 -0700592 return mState.stack.peek();
Steve McKayb68dd222015-04-20 17:18:15 -0700593 }
594
Steve McKay459bc2b2015-09-16 15:07:31 -0700595 public Executor getExecutorForCurrentDirectory() {
Steve McKayb68dd222015-04-20 17:18:15 -0700596 final DocumentInfo cwd = getCurrentDirectory();
597 if (cwd != null && cwd.authority != null) {
598 return ProviderExecutor.forAuthority(cwd.authority);
599 } else {
600 return AsyncTask.THREAD_POOL_EXECUTOR;
601 }
602 }
603
Steve McKay0fbfc652015-08-20 16:48:49 -0700604 @Override
605 public void onBackPressed() {
606 // While action bar is expanded, the state stack UI is hidden.
607 if (mSearchManager.cancelSearch()) {
608 return;
609 }
610
Steve McKayd4800812016-02-02 11:41:03 -0800611 DirectoryFragment dir = getDirectoryFragment();
612 if (dir != null && dir.onBackPressed()) {
Steve McKay86c05762016-01-28 15:30:10 -0800613 return;
614 }
615
Daichi Hirono2806beb2016-01-07 15:29:12 +0900616 if (!mState.hasLocationChanged()) {
Steve McKay0fbfc652015-08-20 16:48:49 -0700617 super.onBackPressed();
618 return;
619 }
620
Steve McKayfd8425a2016-02-23 14:34:50 -0800621 if (onBeforePopDir() || popDir()) {
Steve McKay95c79f52016-02-04 19:40:45 -0800622 return;
Steve McKay0fbfc652015-08-20 16:48:49 -0700623 }
Steve McKay95c79f52016-02-04 19:40:45 -0800624
625 super.onBackPressed();
Steve McKay0fbfc652015-08-20 16:48:49 -0700626 }
627
Steve McKay16e0c1f2016-09-15 12:41:13 -0700628 protected boolean onBeforePopDir() {
Steve McKayfd8425a2016-02-23 14:34:50 -0800629 // Files app overrides this with some fancy logic.
630 return false;
631 }
632
Steve McKayb68dd222015-04-20 17:18:15 -0700633 public void onStackPicked(DocumentStack stack) {
634 try {
635 // Update the restored stack to ensure we have freshest data
636 stack.updateDocuments(getContentResolver());
Daichi Hirono2806beb2016-01-07 15:29:12 +0900637 mState.setStack(stack);
Steve McKayfb4fd2f2016-03-11 10:49:32 -0800638 refreshCurrentRootAndDirectory(AnimationView.ANIM_SIDE);
Steve McKayb68dd222015-04-20 17:18:15 -0700639
640 } catch (FileNotFoundException e) {
641 Log.w(mTag, "Failed to restore stack: " + e);
642 }
643 }
644
Ben Kwa74e5d412016-02-10 07:46:35 -0800645 /**
646 * Declare a global key handler to route key events when there isn't a specific focus view. This
647 * covers the scenario where a user opens DocumentsUI and just starts typing.
648 *
649 * @param keyCode
650 * @param event
651 * @return
652 */
653 @CallSuper
654 @Override
655 public boolean onKeyDown(int keyCode, KeyEvent event) {
656 if (Events.isNavigationKeyCode(keyCode)) {
657 // Forward all unclaimed navigation keystrokes to the DirectoryFragment. This causes any
658 // stray navigation keystrokes focus the content pane, which is probably what the user
659 // is trying to do.
660 DirectoryFragment df = DirectoryFragment.get(getFragmentManager());
661 if (df != null) {
662 df.requestFocus();
663 return true;
664 }
665 } else if (keyCode == KeyEvent.KEYCODE_TAB) {
Ben Kwa65f393a2016-02-17 10:48:57 -0800666 // Tab toggles focus on the navigation drawer.
Ben Kwa74e5d412016-02-10 07:46:35 -0800667 toggleNavDrawerFocus();
668 return true;
Ben Kwa65f393a2016-02-17 10:48:57 -0800669 } else if (keyCode == KeyEvent.KEYCODE_DEL) {
670 popDir();
671 return true;
Ben Kwa74e5d412016-02-10 07:46:35 -0800672 }
673 return super.onKeyDown(keyCode, event);
674 }
675
Tomasz Mikolajewskie29e3412016-02-24 12:53:44 +0900676 public void addEventListener(EventListener listener) {
677 mEventListeners.add(listener);
678 }
679
Tomasz Mikolajewskie29e3412016-02-24 12:53:44 +0900680 public void removeEventListener(EventListener listener) {
681 mEventListeners.remove(listener);
682 }
683
684 public void notifyDirectoryLoaded(Uri uri) {
685 for (EventListener listener : mEventListeners) {
686 listener.onDirectoryLoaded(uri);
687 }
688 }
689
690 void notifyDirectoryNavigated(Uri uri) {
691 for (EventListener listener : mEventListeners) {
692 listener.onDirectoryNavigated(uri);
693 }
694 }
695
Ben Kwa74e5d412016-02-10 07:46:35 -0800696 /**
697 * Toggles focus between the navigation drawer and the directory listing. If the drawer isn't
698 * locked, open/close it as appropriate.
699 */
700 void toggleNavDrawerFocus() {
701 if (mNavDrawerHasFocus) {
702 mDrawer.setOpen(false);
703 DirectoryFragment df = DirectoryFragment.get(getFragmentManager());
704 if (df != null) {
705 df.requestFocus();
706 }
707 } else {
708 mDrawer.setOpen(true);
709 RootsFragment rf = RootsFragment.get(getFragmentManager());
710 if (rf != null) {
711 rf.requestFocus();
712 }
713 }
714 mNavDrawerHasFocus = !mNavDrawerHasFocus;
715 }
716
Ben Kwa65f393a2016-02-17 10:48:57 -0800717 /**
718 * Pops the top entry off the directory stack, and returns the user to the previous directory.
719 * If the directory stack only contains one item, this method does nothing.
720 *
721 * @return Whether the stack was popped.
722 */
723 private boolean popDir() {
724 if (mState.stack.size() > 1) {
725 mState.stack.pop();
Steve McKayfb4fd2f2016-03-11 10:49:32 -0800726 refreshCurrentRootAndDirectory(AnimationView.ANIM_LEAVE);
Ben Kwa65f393a2016-02-17 10:48:57 -0800727 return true;
728 }
729 return false;
730 }
731
Daichi Hirono320a08f2016-03-25 19:04:39 +0900732 /**
733 * Closes the activity when it's idle.
734 */
735 private void addListenerForLaunchCompletion() {
736 addEventListener(new EventListener() {
737 @Override
738 public void onDirectoryNavigated(Uri uri) {
739 }
740
741 @Override
742 public void onDirectoryLoaded(Uri uri) {
743 removeEventListener(this);
744 getMainLooper().getQueue().addIdleHandler(new IdleHandler() {
745 @Override
746 public boolean queueIdle() {
747 // If startup benchmark is requested by a whitelisted testing package, then
748 // close the activity once idle, and notify the testing activity.
749 if (getIntent().getBooleanExtra(EXTRA_BENCHMARK, false) &&
750 BENCHMARK_TESTING_PACKAGE.equals(getCallingPackage())) {
751 setResult(RESULT_OK);
752 finish();
753 }
754
755 Metrics.logStartupMs(
756 BaseActivity.this, (int) (new Date().getTime() - mStartTime));
757
758 // Remove the idle handler.
759 return false;
760 }
761 });
762 new Handler().post(new Runnable() {
763 @Override public void run() {
764 }
765 });
766 }
767 });
768 }
769
Steve McKayc7dc0cf2016-02-04 12:15:22 -0800770 private static final class PickRootTask extends PairedTask<BaseActivity, Void, DocumentInfo> {
Steve McKayb68dd222015-04-20 17:18:15 -0700771 private RootInfo mRoot;
772
Steve McKayc7dc0cf2016-02-04 12:15:22 -0800773 public PickRootTask(BaseActivity activity, RootInfo root) {
774 super(activity);
Steve McKayb68dd222015-04-20 17:18:15 -0700775 mRoot = root;
776 }
777
778 @Override
Steve McKayc7dc0cf2016-02-04 12:15:22 -0800779 protected DocumentInfo run(Void... params) {
Garfield, Tana5588b62016-07-13 09:23:04 -0700780 return mRoot.getRootDocumentBlocking(mOwner);
Steve McKayb68dd222015-04-20 17:18:15 -0700781 }
782
783 @Override
Steve McKayc7dc0cf2016-02-04 12:15:22 -0800784 protected void finish(DocumentInfo result) {
785 if (result != null) {
786 mOwner.openContainerDocument(result);
Steve McKayb68dd222015-04-20 17:18:15 -0700787 }
788 }
789 }
790
Steve McKayc7dc0cf2016-02-04 12:15:22 -0800791 private static final class HandleRootsChangedTask
792 extends PairedTask<BaseActivity, RootInfo, RootInfo> {
Daichi Hirono6ff85c32016-04-19 17:56:13 +0900793 RootInfo mCurrentRoot;
Tomasz Mikolajewskic6a7faf2016-04-15 11:27:53 +0900794 DocumentInfo mDefaultRootDocument;
Daichi Hirono2806beb2016-01-07 15:29:12 +0900795
Steve McKayc7dc0cf2016-02-04 12:15:22 -0800796 public HandleRootsChangedTask(BaseActivity activity) {
797 super(activity);
798 }
799
Daichi Hirono3067d0d2015-12-25 11:08:42 +0900800 @Override
Steve McKayc7dc0cf2016-02-04 12:15:22 -0800801 protected RootInfo run(RootInfo... roots) {
Steve McKay0af8afd2016-02-25 13:34:03 -0800802 assert(roots.length == 1);
Daichi Hirono6ff85c32016-04-19 17:56:13 +0900803 mCurrentRoot = roots[0];
Steve McKayc7dc0cf2016-02-04 12:15:22 -0800804 final Collection<RootInfo> cachedRoots = mOwner.mRoots.getRootsBlocking();
Daichi Hirono3067d0d2015-12-25 11:08:42 +0900805 for (final RootInfo root : cachedRoots) {
Daichi Hirono6ff85c32016-04-19 17:56:13 +0900806 if (root.getUri().equals(mCurrentRoot.getUri())) {
Daichi Hirono3067d0d2015-12-25 11:08:42 +0900807 // We don't need to change the current root as the current root was not removed.
808 return null;
809 }
810 }
Tomasz Mikolajewskic6a7faf2016-04-15 11:27:53 +0900811
812 // Choose the default root.
813 final RootInfo defaultRoot = mOwner.mRoots.getDefaultRootBlocking(mOwner.mState);
814 assert(defaultRoot != null);
815 if (!defaultRoot.isRecents()) {
Garfield, Tana5588b62016-07-13 09:23:04 -0700816 mDefaultRootDocument = defaultRoot.getRootDocumentBlocking(mOwner);
Tomasz Mikolajewskic6a7faf2016-04-15 11:27:53 +0900817 }
818 return defaultRoot;
Daichi Hirono3067d0d2015-12-25 11:08:42 +0900819 }
820
821 @Override
Tomasz Mikolajewskic6a7faf2016-04-15 11:27:53 +0900822 protected void finish(RootInfo defaultRoot) {
823 if (defaultRoot == null) {
824 return;
825 }
826
Daichi Hirono6ff85c32016-04-19 17:56:13 +0900827 // If the activity has been launched for the specific root and it is removed, finish the
828 // activity.
829 final Uri uri = mOwner.getIntent().getData();
830 if (uri != null && uri.equals(mCurrentRoot.getUri())) {
831 mOwner.finish();
832 return;
833 }
834
Tomasz Mikolajewskic6a7faf2016-04-15 11:27:53 +0900835 // Clear entire backstack and start in new root.
836 mOwner.mState.onRootChanged(defaultRoot);
837 mOwner.mSearchManager.update(defaultRoot);
838
839 if (defaultRoot.isRecents()) {
840 mOwner.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
841 } else {
842 mOwner.openContainerDocument(mDefaultRootDocument);
Daichi Hirono3067d0d2015-12-25 11:08:42 +0900843 }
844 }
845 }
Steve McKayd9caa6a2016-09-15 16:36:45 -0700846
847 public final class RetainedState {
848 public @Nullable Selection selection;
849
850 public boolean hasSelection() {
851 return selection != null;
852 }
853 }
854
855 @VisibleForTesting
856 protected interface EventListener {
857 /**
858 * @param uri Uri navigated to. If recents, then null.
859 */
860 void onDirectoryNavigated(@Nullable Uri uri);
861
862 /**
863 * @param uri Uri of the loaded directory. If recents, then null.
864 */
865 void onDirectoryLoaded(@Nullable Uri uri);
866 }
Steve McKayd0a2a2c2015-03-25 14:35:33 -0700867}