blob: c96d6055840d7e80e639a484451609908483fce6 [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
Tomasz Mikolajewskid71bd612016-02-16 12:28:43 +0900155 public abstract void onDocumentPicked(DocumentInfo doc, Model model);
Steve McKayd0a2a2c2015-03-25 14:35:33 -0700156 public abstract void onDocumentsPicked(List<DocumentInfo> docs);
Steve McKay351a7492015-08-04 10:11:01 -0700157
Steve McKay16e0c1f2016-09-15 12:41:13 -0700158 protected abstract void onTaskFinished(Uri... uris);
159 protected abstract void refreshDirectory(int anim);
Steve McKayc7dc0cf2016-02-04 12:15:22 -0800160 /** Allows sub-classes to include information in a newly created State instance. */
Steve McKay16e0c1f2016-09-15 12:41:13 -0700161 protected abstract void includeState(State initialState);
162 protected abstract void onDirectoryCreated(DocumentInfo doc);
Steve McKayb68dd222015-04-20 17:18:15 -0700163
Steve McKay0fbfc652015-08-20 16:48:49 -0700164 public BaseActivity(@LayoutRes int layoutId, String tag) {
165 mLayoutId = layoutId;
Steve McKayb68dd222015-04-20 17:18:15 -0700166 mTag = tag;
167 }
168
Steve McKay18d01e82016-02-03 11:15:57 -0800169 @CallSuper
Steve McKayb68dd222015-04-20 17:18:15 -0700170 @Override
171 public void onCreate(Bundle icicle) {
Daichi Hirono320a08f2016-03-25 19:04:39 +0900172 // Record the time when onCreate is invoked for metric.
173 mStartTime = new Date().getTime();
174
Steve McKayb68dd222015-04-20 17:18:15 -0700175 super.onCreate(icicle);
Steve McKay0fbfc652015-08-20 16:48:49 -0700176
Tomasz Mikolajewskib8373c22016-03-15 17:41:31 +0900177 final Intent intent = getIntent();
178
Daichi Hirono320a08f2016-03-25 19:04:39 +0900179 addListenerForLaunchCompletion();
Tomasz Mikolajewskib8373c22016-03-15 17:41:31 +0900180
Steve McKay18d01e82016-02-03 11:15:57 -0800181 setContentView(mLayoutId);
182
183 mDrawer = DrawerController.create(this);
Steve McKayc7dc0cf2016-02-04 12:15:22 -0800184 mState = getState(icicle);
Tomasz Mikolajewskib8373c22016-03-15 17:41:31 +0900185 Metrics.logActivityLaunch(this, mState, intent);
Ben Kwa1c0a3892016-01-26 11:50:03 -0800186
Steve McKay2bab2f82016-06-03 09:23:39 -0700187 // we're really interested in retainining state in our very complex
188 // DirectoryFragment. So we do a little code yoga to extend
189 // support to that fragment.
190 mRetainedState = (RetainedState) getLastNonConfigurationInstance();
Steve McKayb68dd222015-04-20 17:18:15 -0700191 mRoots = DocumentsApplication.getRootsCache(this);
Steve McKay18d01e82016-02-03 11:15:57 -0800192
Daichi Hirono6ff85c32016-04-19 17:56:13 +0900193 getContentResolver().registerContentObserver(
194 RootsCache.sNotificationUri, false, mRootsCacheObserver);
Steve McKay18d01e82016-02-03 11:15:57 -0800195
Garfield, Tan11d23482016-08-05 09:33:29 -0700196 mSearchManager = new SearchViewManager(this, icicle, mState.sortModel);
Steve McKay0fbfc652015-08-20 16:48:49 -0700197
Steve McKay18d01e82016-02-03 11:15:57 -0800198 DocumentsToolbar toolbar = (DocumentsToolbar) findViewById(R.id.toolbar);
199 setActionBar(toolbar);
Steve McKay85ec0d62016-06-24 15:05:08 -0700200
201 Breadcrumb breadcrumb =
202 Shared.findView(this, R.id.dropdown_breadcrumb, R.id.horizontal_breadcrumb);
203 assert(breadcrumb != null);
204
Ben Linb8c54e72016-06-10 12:13:27 -0700205 mNavigator = new NavigationViewManager(mDrawer, toolbar, mState, this, breadcrumb);
Steve McKay18d01e82016-02-03 11:15:57 -0800206
Steve McKay9de0da62016-08-25 15:18:23 -0700207 mSortController = SortController.create(this, mState.derivedMode, mState.sortModel);
208
Garfield, Tan171e6f52016-07-29 14:44:58 -0700209
Steve McKay0fbfc652015-08-20 16:48:49 -0700210 // Base classes must update result in their onCreate.
211 setResult(Activity.RESULT_CANCELED);
Steve McKay0269fb62015-04-22 15:55:34 -0700212 }
213
214 @Override
215 public boolean onCreateOptionsMenu(Menu menu) {
216 boolean showMenu = super.onCreateOptionsMenu(menu);
217
218 getMenuInflater().inflate(R.menu.activity, menu);
Aga Wronskaaf5ace52016-02-17 13:50:42 -0800219 mNavigator.update();
Aga Wronska8e21daa2016-03-24 18:22:09 -0700220 boolean fullBarSearch = getResources().getBoolean(R.bool.full_bar_search_view);
221 mSearchManager.install((DocumentsToolbar) findViewById(R.id.toolbar), fullBarSearch);
Steve McKay0269fb62015-04-22 15:55:34 -0700222
223 return showMenu;
Steve McKayb68dd222015-04-20 17:18:15 -0700224 }
225
Steve McKay7bd32e12015-04-30 16:12:59 -0700226 @Override
Steve McKayd4800812016-02-02 11:41:03 -0800227 @CallSuper
Steve McKay7bd32e12015-04-30 16:12:59 -0700228 public boolean onPrepareOptionsMenu(Menu menu) {
Steve McKaye9809272015-10-01 11:39:24 -0700229 super.onPrepareOptionsMenu(menu);
Steve McKayd4800812016-02-02 11:41:03 -0800230 mSearchManager.showMenu(canSearchRoot());
Steve McKaye9809272015-10-01 11:39:24 -0700231 return true;
Steve McKay7bd32e12015-04-30 16:12:59 -0700232 }
233
Daichi Hironoe28c3c82016-01-13 13:19:02 +0900234 @Override
235 protected void onDestroy() {
Daichi Hirono6ff85c32016-04-19 17:56:13 +0900236 getContentResolver().unregisterContentObserver(mRootsCacheObserver);
Daichi Hironoe28c3c82016-01-13 13:19:02 +0900237 super.onDestroy();
238 }
239
Steve McKayc7dc0cf2016-02-04 12:15:22 -0800240 private State getState(@Nullable Bundle icicle) {
241 if (icicle != null) {
Aga Wronskaaf5ace52016-02-17 13:50:42 -0800242 State state = icicle.<State>getParcelable(Shared.EXTRA_STATE);
Steve McKayc7dc0cf2016-02-04 12:15:22 -0800243 if (DEBUG) Log.d(mTag, "Recovered existing state object: " + state);
244 return state;
245 }
246
Ben Kwa0574b182015-09-08 07:31:19 -0700247 State state = new State();
248
249 final Intent intent = getIntent();
Ben Kwa0574b182015-09-08 07:31:19 -0700250
Garfield, Tan171e6f52016-07-29 14:44:58 -0700251 state.sortModel = SortModel.createModel();
Ben Kwa0574b182015-09-08 07:31:19 -0700252 state.localOnly = intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false);
Steve McKay459bc2b2015-09-16 15:07:31 -0700253 state.initAcceptMimes(intent);
Ben Kwa0574b182015-09-08 07:31:19 -0700254 state.excludedAuthorities = getExcludedAuthorities();
255
Aga Wronskaceb990e2016-03-30 18:07:59 -0700256 includeState(state);
257
Steve McKay0a2c49a2016-05-04 14:55:15 -0700258 // Advanced roots are shown by default without menu option if forced by config or intent.
Steve McKay91226ee2016-06-20 11:46:56 -0700259 boolean forceAdvanced = Shared.shouldShowDeviceRoot(this, intent);
260 boolean chosenAdvanced = LocalPreferences.getShowDeviceRoot(this, state.action);
261 state.showAdvanced = forceAdvanced || chosenAdvanced;
262
Aga Wronskaceb990e2016-03-30 18:07:59 -0700263 // Menu option is shown for whitelisted intents if advanced roots are not shown by default.
Steve McKay91226ee2016-06-20 11:46:56 -0700264 state.showAdvancedOption = !forceAdvanced && (
Steve McKay24917422016-05-10 15:01:39 -0700265 Shared.shouldShowFancyFeatures(this)
266 || state.action == ACTION_OPEN
267 || state.action == ACTION_CREATE
268 || state.action == ACTION_OPEN_TREE
269 || state.action == ACTION_PICK_COPY_DESTINATION
270 || state.action == ACTION_GET_CONTENT);
Aga Wronskaceb990e2016-03-30 18:07:59 -0700271
272 if (DEBUG) Log.d(mTag, "Created new state object: " + state);
273
Ben Kwa0574b182015-09-08 07:31:19 -0700274 return state;
275 }
276
Steve McKaycb9eb422016-02-09 16:17:24 -0800277 public void setRootsDrawerOpen(boolean open) {
278 mNavigator.revealRootsDrawer(open);
279 }
Steve McKayb68dd222015-04-20 17:18:15 -0700280
Steve McKayd0805062016-09-15 14:30:38 -0700281 public void onRootPicked(RootInfo root) {
Aga Wronskaaf5ace52016-02-17 13:50:42 -0800282 // Clicking on the current root removes search
283 mSearchManager.cancelSearch();
284
Aga Wronskafa421722016-02-08 12:00:38 -0800285 // Skip refreshing if root nor directory didn't change
286 if (root.equals(getCurrentRoot()) && mState.stack.size() == 1) {
Aga Wronskafd26e8d2016-02-02 18:01:58 -0800287 return;
288 }
289
Steve McKay7776aa52016-01-25 19:00:22 -0800290 mState.derivedMode = LocalPreferences.getViewMode(this, root, MODE_GRID);
Steve McKay743bc1b2016-08-26 16:10:39 -0700291 mSortController.onViewModeChanged(mState.derivedMode);
Steve McKay7776aa52016-01-25 19:00:22 -0800292
Garfield, Tan171e6f52016-07-29 14:44:58 -0700293 // Set summary header's visibility. Only recents and downloads root may have summary in
294 // their docs.
295 mState.sortModel.setDimensionVisibility(
296 SortModel.SORT_DIMENSION_ID_SUMMARY,
297 root.isRecents() || root.isDownloads() ? View.VISIBLE : View.INVISIBLE);
298
Steve McKayb68dd222015-04-20 17:18:15 -0700299 // Clear entire backstack and start in new root
Daichi Hirono2806beb2016-01-07 15:29:12 +0900300 mState.onRootChanged(root);
Steve McKay0269fb62015-04-22 15:55:34 -0700301
302 // Recents is always in memory, so we just load it directly.
303 // Otherwise we delegate loading data from disk to a task
304 // to ensure a responsive ui.
305 if (mRoots.isRecentsRoot(root)) {
Steve McKayfb4fd2f2016-03-11 10:49:32 -0800306 refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
Steve McKay0269fb62015-04-22 15:55:34 -0700307 } else {
Steve McKayc7dc0cf2016-02-04 12:15:22 -0800308 new PickRootTask(this, root).executeOnExecutor(getExecutorForCurrentDirectory());
Daichi Hirono3067d0d2015-12-25 11:08:42 +0900309 }
310 }
311
Steve McKayb68dd222015-04-20 17:18:15 -0700312 @Override
313 public boolean onOptionsItemSelected(MenuItem item) {
Steve McKayb68dd222015-04-20 17:18:15 -0700314
Steve McKay6035b3c2015-12-04 11:19:09 -0800315 switch (item.getItemId()) {
316 case android.R.id.home:
317 onBackPressed();
318 return true;
319
320 case R.id.menu_create_dir:
321 showCreateDirectoryDialog();
322 return true;
323
324 case R.id.menu_search:
Steve McKayf2eb8d92016-03-08 14:01:47 -0800325 // SearchViewManager listens for this directly.
Steve McKay6035b3c2015-12-04 11:19:09 -0800326 return false;
327
Steve McKay6035b3c2015-12-04 11:19:09 -0800328 case R.id.menu_grid:
Steve McKay7776aa52016-01-25 19:00:22 -0800329 setViewMode(State.MODE_GRID);
Steve McKay6035b3c2015-12-04 11:19:09 -0800330 return true;
331
332 case R.id.menu_list:
Steve McKay7776aa52016-01-25 19:00:22 -0800333 setViewMode(State.MODE_LIST);
Steve McKay6035b3c2015-12-04 11:19:09 -0800334 return true;
335
Aga Wronskaceb990e2016-03-30 18:07:59 -0700336 case R.id.menu_advanced:
337 setDisplayAdvancedDevices(!mState.showAdvanced);
338 return true;
339
Steve McKay6035b3c2015-12-04 11:19:09 -0800340 default:
341 return super.onOptionsItemSelected(item);
342 }
Steve McKayb68dd222015-04-20 17:18:15 -0700343 }
344
Steve McKay16e0c1f2016-09-15 12:41:13 -0700345 protected final @Nullable DirectoryFragment getDirectoryFragment() {
Steve McKayd4800812016-02-02 11:41:03 -0800346 return DirectoryFragment.get(getFragmentManager());
347 }
348
Steve McKay16e0c1f2016-09-15 12:41:13 -0700349 protected void showCreateDirectoryDialog() {
Aga Wronska94e53e42016-04-07 13:09:58 -0700350 Metrics.logUserAction(this, Metrics.USER_ACTION_CREATE_DIR);
351
Steve McKayceeb3f72015-05-19 16:10:25 -0700352 CreateDirectoryFragment.show(getFragmentManager());
353 }
354
355 /**
356 * Returns true if a directory can be created in the current location.
357 * @return
358 */
Steve McKay16e0c1f2016-09-15 12:41:13 -0700359 protected boolean canCreateDirectory() {
Steve McKayceeb3f72015-05-19 16:10:25 -0700360 final RootInfo root = getCurrentRoot();
361 final DocumentInfo cwd = getCurrentDirectory();
362 return cwd != null
363 && cwd.isCreateSupported()
364 && !mSearchManager.isSearching()
Steve McKaye9809272015-10-01 11:39:24 -0700365 && !root.isRecents()
Steve McKayceeb3f72015-05-19 16:10:25 -0700366 && !root.isDownloads();
367 }
368
Steve McKay16e0c1f2016-09-15 12:41:13 -0700369 protected void openContainerDocument(DocumentInfo doc) {
Steve McKay0af8afd2016-02-25 13:34:03 -0800370 assert(doc.isContainer());
371
Tomasz Mikolajewskie29e3412016-02-24 12:53:44 +0900372 notifyDirectoryNavigated(doc.derivedUri);
373
Daichi Hirono2806beb2016-01-07 15:29:12 +0900374 mState.pushDocument(doc);
Tomasz Mikolajewski2b5170b2016-02-03 15:49:58 +0900375 // Show an opening animation only if pressing "back" would get us back to the
376 // previous directory. Especially after opening a root document, pressing
377 // back, wouldn't go to the previous root, but close the activity.
378 final int anim = (mState.hasLocationChanged() && mState.stack.size() > 1)
Steve McKayfb4fd2f2016-03-11 10:49:32 -0800379 ? AnimationView.ANIM_ENTER : AnimationView.ANIM_NONE;
Tomasz Mikolajewski2b5170b2016-02-03 15:49:58 +0900380 refreshCurrentRootAndDirectory(anim);
Steve McKay351a7492015-08-04 10:11:01 -0700381 }
382
Steve McKayb68dd222015-04-20 17:18:15 -0700383 /**
Aga Wronska8788dad2016-01-15 17:30:15 -0800384 * Refreshes the content of the director and the menu/action bar.
385 * The current directory name and selection will get updated.
Steve McKayb68dd222015-04-20 17:18:15 -0700386 * @param anim
387 */
Steve McKay18d01e82016-02-03 11:15:57 -0800388 @Override
389 public final void refreshCurrentRootAndDirectory(int anim) {
Aga Wronska8788dad2016-01-15 17:30:15 -0800390 mSearchManager.cancelSearch();
391
Aga Wronska8788dad2016-01-15 17:30:15 -0800392 refreshDirectory(anim);
Steve McKayb68dd222015-04-20 17:18:15 -0700393
394 final RootsFragment roots = RootsFragment.get(getFragmentManager());
395 if (roots != null) {
396 roots.onCurrentRootChanged();
397 }
398
Steve McKay18d01e82016-02-03 11:15:57 -0800399 mNavigator.update();
Aga Wronska8788dad2016-01-15 17:30:15 -0800400 invalidateOptionsMenu();
401 }
402
Steve McKay16e0c1f2016-09-15 12:41:13 -0700403 protected final void loadRoot(final Uri uri) {
Steve McKayd9caa6a2016-09-15 16:36:45 -0700404 new LoadRootTask(this, mRoots, mState, uri).executeOnExecutor(
Steve McKayfd8425a2016-02-23 14:34:50 -0800405 ProviderExecutor.forAuthority(uri.getAuthority()));
406 }
407
Aga Wronska8788dad2016-01-15 17:30:15 -0800408 /**
Garfield, Tan804133e2016-04-20 15:13:56 -0700409 * This is called when user hovers over a doc for enough time during a drag n' drop, to open a
410 * folder that accepts drop. We should only open a container that's not an archive.
411 */
412 public void springOpenDirectory(DocumentInfo doc) {
413 }
414
415 /**
Aga Wronska8788dad2016-01-15 17:30:15 -0800416 * Called when search results changed.
417 * Refreshes the content of the directory. It doesn't refresh elements on the action bar.
418 * e.g. The current directory name displayed on the action bar won't get updated.
419 */
420 @Override
Aga Wronskaaf5ace52016-02-17 13:50:42 -0800421 public void onSearchChanged(@Nullable String query) {
422 // We should not get here if root is not searchable
Steve McKay0af8afd2016-02-25 13:34:03 -0800423 assert(canSearchRoot());
Aga Wronskaaf5ace52016-02-17 13:50:42 -0800424 reloadSearch(query);
Aga Wronska8e21daa2016-03-24 18:22:09 -0700425 }
426
427 @Override
428 public void onSearchFinished() {
429 // Restores menu icons state
Steve McKayf2eb8d92016-03-08 14:01:47 -0800430 invalidateOptionsMenu();
Aga Wronska8788dad2016-01-15 17:30:15 -0800431 }
432
Aga Wronskaaf5ace52016-02-17 13:50:42 -0800433 private void reloadSearch(String query) {
434 FragmentManager fm = getFragmentManager();
435 RootInfo root = getCurrentRoot();
436 DocumentInfo cwd = getCurrentDirectory();
437
438 DirectoryFragment.reloadSearch(fm, root, cwd, query);
Steve McKayb68dd222015-04-20 17:18:15 -0700439 }
440
Steve McKay16e0c1f2016-09-15 12:41:13 -0700441 private final List<String> getExcludedAuthorities() {
Ben Kwa0bcdec32015-05-29 15:40:31 -0700442 List<String> authorities = new ArrayList<>();
443 if (getIntent().getBooleanExtra(DocumentsContract.EXTRA_EXCLUDE_SELF, false)) {
444 // Exclude roots provided by the calling package.
445 String packageName = getCallingPackageMaybeExtra();
446 try {
447 PackageInfo pkgInfo = getPackageManager().getPackageInfo(packageName,
448 PackageManager.GET_PROVIDERS);
449 for (ProviderInfo provider: pkgInfo.providers) {
450 authorities.add(provider.authority);
451 }
452 } catch (PackageManager.NameNotFoundException e) {
453 Log.e(mTag, "Calling package name does not resolve: " + packageName);
454 }
455 }
456 return authorities;
457 }
458
Aga Wronska654e25c2016-01-29 11:41:41 -0800459 boolean canSearchRoot() {
460 final RootInfo root = getCurrentRoot();
461 return (root.flags & Root.FLAG_SUPPORTS_SEARCH) != 0;
462 }
463
Steve McKay16e0c1f2016-09-15 12:41:13 -0700464 public final String getCallingPackageMaybeExtra() {
Ben Kwa0bcdec32015-05-29 15:40:31 -0700465 String callingPackage = getCallingPackage();
466 // System apps can set the calling package name using an extra.
467 try {
468 ApplicationInfo info = getPackageManager().getApplicationInfo(callingPackage, 0);
469 if (info.isSystemApp() || info.isUpdatedSystemApp()) {
470 final String extra = getIntent().getStringExtra(DocumentsContract.EXTRA_PACKAGE_NAME);
471 if (extra != null) {
472 callingPackage = extra;
473 }
474 }
475 } finally {
476 return callingPackage;
477 }
Steve McKayb68dd222015-04-20 17:18:15 -0700478 }
Steve McKayd0a2a2c2015-03-25 14:35:33 -0700479
480 public static BaseActivity get(Fragment fragment) {
481 return (BaseActivity) fragment.getActivity();
482 }
483
Ben Kwa0574b182015-09-08 07:31:19 -0700484 public State getDisplayState() {
485 return mState;
486 }
487
Aga Wronska64ae1f42016-03-22 14:18:43 -0700488 /*
489 * Get the default directory to be presented after starting the activity.
490 * Method can be overridden if the change of the behavior of the the child activity is needed.
491 */
492 public Uri getDefaultRoot() {
Steve McKay17f7e582016-04-04 15:26:48 -0700493 return Shared.shouldShowDocumentsRoot(this, getIntent())
494 ? DocumentsContract.buildHomeUri()
495 : DocumentsContract.buildRootUri(
496 "com.android.providers.downloads.documents", "downloads");
497 }
Aga Wronskaceb990e2016-03-30 18:07:59 -0700498
Aga Wronska94e53e42016-04-07 13:09:58 -0700499 /**
500 * Set internal storage visible based on explicit user action.
501 */
Aga Wronskaceb990e2016-03-30 18:07:59 -0700502 void setDisplayAdvancedDevices(boolean display) {
Aga Wronska94e53e42016-04-07 13:09:58 -0700503 Metrics.logUserAction(this,
504 display ? Metrics.USER_ACTION_SHOW_ADVANCED : Metrics.USER_ACTION_HIDE_ADVANCED);
505
Steve McKay91226ee2016-06-20 11:46:56 -0700506 LocalPreferences.setShowDeviceRoot(this, mState.action, display);
Aga Wronskaceb990e2016-03-30 18:07:59 -0700507 mState.showAdvanced = display;
508 RootsFragment.get(getFragmentManager()).onDisplayStateChanged();
509 invalidateOptionsMenu();
Aga Wronska64ae1f42016-03-22 14:18:43 -0700510 }
511
Aga Wronska94e53e42016-04-07 13:09:58 -0700512 /**
Steve McKay7776aa52016-01-25 19:00:22 -0800513 * Set mode based on explicit user action.
Steve McKayb68dd222015-04-20 17:18:15 -0700514 */
Steve McKay7776aa52016-01-25 19:00:22 -0800515 void setViewMode(@ViewMode int mode) {
Aga Wronska94e53e42016-04-07 13:09:58 -0700516 if (mode == State.MODE_GRID) {
517 Metrics.logUserAction(this, Metrics.USER_ACTION_GRID);
518 } else if (mode == State.MODE_LIST) {
519 Metrics.logUserAction(this, Metrics.USER_ACTION_LIST);
520 }
521
Steve McKay30aabb82016-02-17 11:00:55 -0800522 LocalPreferences.setViewMode(this, getCurrentRoot(), mode);
Steve McKay7776aa52016-01-25 19:00:22 -0800523 mState.derivedMode = mode;
524
525 // view icon needs to be updated, but we *could* do it
526 // in onOptionsItemSelected, and not do the full invalidation
527 // But! That's a larger refactoring we'll save for another day.
528 invalidateOptionsMenu();
Steve McKayd4800812016-02-02 11:41:03 -0800529 DirectoryFragment dir = getDirectoryFragment();
530 if (dir != null) {
531 dir.onViewModeChanged();
Ben Kwa65f393a2016-02-17 10:48:57 -0800532 }
Garfield, Tan171e6f52016-07-29 14:44:58 -0700533
534 mSortController.onViewModeChanged(mode);
Steve McKayb68dd222015-04-20 17:18:15 -0700535 }
536
Aga Wronska3c237182016-01-20 16:32:33 -0800537 public void setPending(boolean pending) {
Steve McKay16e0c1f2016-09-15 12:41:13 -0700538 // TODO: Isolate this behavior to PickActivity.
Steve McKayb68dd222015-04-20 17:18:15 -0700539 }
540
541 @Override
542 protected void onSaveInstanceState(Bundle state) {
543 super.onSaveInstanceState(state);
Aga Wronskaaf5ace52016-02-17 13:50:42 -0800544 state.putParcelable(Shared.EXTRA_STATE, mState);
545 mSearchManager.onSaveInstanceState(state);
Steve McKayb68dd222015-04-20 17:18:15 -0700546 }
547
548 @Override
549 protected void onRestoreInstanceState(Bundle state) {
550 super.onRestoreInstanceState(state);
551 }
552
Steve McKay2bab2f82016-06-03 09:23:39 -0700553 /**
554 * Delegate ths call to the current fragment so it can save selection.
555 * Feel free to expand on this with other useful state.
556 */
557 @Override
558 public RetainedState onRetainNonConfigurationInstance() {
559 RetainedState retained = new RetainedState();
560 DirectoryFragment fragment = DirectoryFragment.get(getFragmentManager());
561 if (fragment != null) {
562 fragment.retainState(retained);
563 }
564 return retained;
565 }
566
567 public @Nullable RetainedState getRetainedState() {
568 return mRetainedState;
569 }
570
Steve McKay18d01e82016-02-03 11:15:57 -0800571 @Override
572 public boolean isSearchExpanded() {
573 return mSearchManager.isExpanded();
574 }
575
576 @Override
Steve McKayf68210e2015-11-03 15:23:16 -0800577 public RootInfo getCurrentRoot() {
Steve McKay4d0255f2015-09-25 16:02:56 -0700578 if (mState.stack.root != null) {
579 return mState.stack.root;
Steve McKayb68dd222015-04-20 17:18:15 -0700580 } else {
581 return mRoots.getRecentsRoot();
582 }
583 }
584
585 public DocumentInfo getCurrentDirectory() {
Steve McKay4d0255f2015-09-25 16:02:56 -0700586 return mState.stack.peek();
Steve McKayb68dd222015-04-20 17:18:15 -0700587 }
588
Steve McKay459bc2b2015-09-16 15:07:31 -0700589 public Executor getExecutorForCurrentDirectory() {
Steve McKayb68dd222015-04-20 17:18:15 -0700590 final DocumentInfo cwd = getCurrentDirectory();
591 if (cwd != null && cwd.authority != null) {
592 return ProviderExecutor.forAuthority(cwd.authority);
593 } else {
594 return AsyncTask.THREAD_POOL_EXECUTOR;
595 }
596 }
597
Steve McKay0fbfc652015-08-20 16:48:49 -0700598 @Override
599 public void onBackPressed() {
600 // While action bar is expanded, the state stack UI is hidden.
601 if (mSearchManager.cancelSearch()) {
602 return;
603 }
604
Steve McKayd4800812016-02-02 11:41:03 -0800605 DirectoryFragment dir = getDirectoryFragment();
606 if (dir != null && dir.onBackPressed()) {
Steve McKay86c05762016-01-28 15:30:10 -0800607 return;
608 }
609
Daichi Hirono2806beb2016-01-07 15:29:12 +0900610 if (!mState.hasLocationChanged()) {
Steve McKay0fbfc652015-08-20 16:48:49 -0700611 super.onBackPressed();
612 return;
613 }
614
Steve McKayfd8425a2016-02-23 14:34:50 -0800615 if (onBeforePopDir() || popDir()) {
Steve McKay95c79f52016-02-04 19:40:45 -0800616 return;
Steve McKay0fbfc652015-08-20 16:48:49 -0700617 }
Steve McKay95c79f52016-02-04 19:40:45 -0800618
619 super.onBackPressed();
Steve McKay0fbfc652015-08-20 16:48:49 -0700620 }
621
Steve McKay16e0c1f2016-09-15 12:41:13 -0700622 protected boolean onBeforePopDir() {
Steve McKayfd8425a2016-02-23 14:34:50 -0800623 // Files app overrides this with some fancy logic.
624 return false;
625 }
626
Steve McKayb68dd222015-04-20 17:18:15 -0700627 public void onStackPicked(DocumentStack stack) {
628 try {
629 // Update the restored stack to ensure we have freshest data
630 stack.updateDocuments(getContentResolver());
Daichi Hirono2806beb2016-01-07 15:29:12 +0900631 mState.setStack(stack);
Steve McKayfb4fd2f2016-03-11 10:49:32 -0800632 refreshCurrentRootAndDirectory(AnimationView.ANIM_SIDE);
Steve McKayb68dd222015-04-20 17:18:15 -0700633
634 } catch (FileNotFoundException e) {
635 Log.w(mTag, "Failed to restore stack: " + e);
636 }
637 }
638
Ben Kwa74e5d412016-02-10 07:46:35 -0800639 /**
640 * Declare a global key handler to route key events when there isn't a specific focus view. This
641 * covers the scenario where a user opens DocumentsUI and just starts typing.
642 *
643 * @param keyCode
644 * @param event
645 * @return
646 */
647 @CallSuper
648 @Override
649 public boolean onKeyDown(int keyCode, KeyEvent event) {
650 if (Events.isNavigationKeyCode(keyCode)) {
651 // Forward all unclaimed navigation keystrokes to the DirectoryFragment. This causes any
652 // stray navigation keystrokes focus the content pane, which is probably what the user
653 // is trying to do.
654 DirectoryFragment df = DirectoryFragment.get(getFragmentManager());
655 if (df != null) {
656 df.requestFocus();
657 return true;
658 }
659 } else if (keyCode == KeyEvent.KEYCODE_TAB) {
Ben Kwa65f393a2016-02-17 10:48:57 -0800660 // Tab toggles focus on the navigation drawer.
Ben Kwa74e5d412016-02-10 07:46:35 -0800661 toggleNavDrawerFocus();
662 return true;
Ben Kwa65f393a2016-02-17 10:48:57 -0800663 } else if (keyCode == KeyEvent.KEYCODE_DEL) {
664 popDir();
665 return true;
Ben Kwa74e5d412016-02-10 07:46:35 -0800666 }
667 return super.onKeyDown(keyCode, event);
668 }
669
Tomasz Mikolajewskie29e3412016-02-24 12:53:44 +0900670 public void addEventListener(EventListener listener) {
671 mEventListeners.add(listener);
672 }
673
Tomasz Mikolajewskie29e3412016-02-24 12:53:44 +0900674 public void removeEventListener(EventListener listener) {
675 mEventListeners.remove(listener);
676 }
677
678 public void notifyDirectoryLoaded(Uri uri) {
679 for (EventListener listener : mEventListeners) {
680 listener.onDirectoryLoaded(uri);
681 }
682 }
683
684 void notifyDirectoryNavigated(Uri uri) {
685 for (EventListener listener : mEventListeners) {
686 listener.onDirectoryNavigated(uri);
687 }
688 }
689
Ben Kwa74e5d412016-02-10 07:46:35 -0800690 /**
691 * Toggles focus between the navigation drawer and the directory listing. If the drawer isn't
692 * locked, open/close it as appropriate.
693 */
694 void toggleNavDrawerFocus() {
695 if (mNavDrawerHasFocus) {
696 mDrawer.setOpen(false);
697 DirectoryFragment df = DirectoryFragment.get(getFragmentManager());
698 if (df != null) {
699 df.requestFocus();
700 }
701 } else {
702 mDrawer.setOpen(true);
703 RootsFragment rf = RootsFragment.get(getFragmentManager());
704 if (rf != null) {
705 rf.requestFocus();
706 }
707 }
708 mNavDrawerHasFocus = !mNavDrawerHasFocus;
709 }
710
Ben Kwa65f393a2016-02-17 10:48:57 -0800711 /**
712 * Pops the top entry off the directory stack, and returns the user to the previous directory.
713 * If the directory stack only contains one item, this method does nothing.
714 *
715 * @return Whether the stack was popped.
716 */
717 private boolean popDir() {
718 if (mState.stack.size() > 1) {
719 mState.stack.pop();
Steve McKayfb4fd2f2016-03-11 10:49:32 -0800720 refreshCurrentRootAndDirectory(AnimationView.ANIM_LEAVE);
Ben Kwa65f393a2016-02-17 10:48:57 -0800721 return true;
722 }
723 return false;
724 }
725
Daichi Hirono320a08f2016-03-25 19:04:39 +0900726 /**
727 * Closes the activity when it's idle.
728 */
729 private void addListenerForLaunchCompletion() {
730 addEventListener(new EventListener() {
731 @Override
732 public void onDirectoryNavigated(Uri uri) {
733 }
734
735 @Override
736 public void onDirectoryLoaded(Uri uri) {
737 removeEventListener(this);
738 getMainLooper().getQueue().addIdleHandler(new IdleHandler() {
739 @Override
740 public boolean queueIdle() {
741 // If startup benchmark is requested by a whitelisted testing package, then
742 // close the activity once idle, and notify the testing activity.
743 if (getIntent().getBooleanExtra(EXTRA_BENCHMARK, false) &&
744 BENCHMARK_TESTING_PACKAGE.equals(getCallingPackage())) {
745 setResult(RESULT_OK);
746 finish();
747 }
748
749 Metrics.logStartupMs(
750 BaseActivity.this, (int) (new Date().getTime() - mStartTime));
751
752 // Remove the idle handler.
753 return false;
754 }
755 });
756 new Handler().post(new Runnable() {
757 @Override public void run() {
758 }
759 });
760 }
761 });
762 }
763
Steve McKayc7dc0cf2016-02-04 12:15:22 -0800764 private static final class PickRootTask extends PairedTask<BaseActivity, Void, DocumentInfo> {
Steve McKayb68dd222015-04-20 17:18:15 -0700765 private RootInfo mRoot;
766
Steve McKayc7dc0cf2016-02-04 12:15:22 -0800767 public PickRootTask(BaseActivity activity, RootInfo root) {
768 super(activity);
Steve McKayb68dd222015-04-20 17:18:15 -0700769 mRoot = root;
770 }
771
772 @Override
Steve McKayc7dc0cf2016-02-04 12:15:22 -0800773 protected DocumentInfo run(Void... params) {
Garfield, Tana5588b62016-07-13 09:23:04 -0700774 return mRoot.getRootDocumentBlocking(mOwner);
Steve McKayb68dd222015-04-20 17:18:15 -0700775 }
776
777 @Override
Steve McKayc7dc0cf2016-02-04 12:15:22 -0800778 protected void finish(DocumentInfo result) {
779 if (result != null) {
780 mOwner.openContainerDocument(result);
Steve McKayb68dd222015-04-20 17:18:15 -0700781 }
782 }
783 }
784
Steve McKayc7dc0cf2016-02-04 12:15:22 -0800785 private static final class HandleRootsChangedTask
786 extends PairedTask<BaseActivity, RootInfo, RootInfo> {
Daichi Hirono6ff85c32016-04-19 17:56:13 +0900787 RootInfo mCurrentRoot;
Tomasz Mikolajewskic6a7faf2016-04-15 11:27:53 +0900788 DocumentInfo mDefaultRootDocument;
Daichi Hirono2806beb2016-01-07 15:29:12 +0900789
Steve McKayc7dc0cf2016-02-04 12:15:22 -0800790 public HandleRootsChangedTask(BaseActivity activity) {
791 super(activity);
792 }
793
Daichi Hirono3067d0d2015-12-25 11:08:42 +0900794 @Override
Steve McKayc7dc0cf2016-02-04 12:15:22 -0800795 protected RootInfo run(RootInfo... roots) {
Steve McKay0af8afd2016-02-25 13:34:03 -0800796 assert(roots.length == 1);
Daichi Hirono6ff85c32016-04-19 17:56:13 +0900797 mCurrentRoot = roots[0];
Steve McKayc7dc0cf2016-02-04 12:15:22 -0800798 final Collection<RootInfo> cachedRoots = mOwner.mRoots.getRootsBlocking();
Daichi Hirono3067d0d2015-12-25 11:08:42 +0900799 for (final RootInfo root : cachedRoots) {
Daichi Hirono6ff85c32016-04-19 17:56:13 +0900800 if (root.getUri().equals(mCurrentRoot.getUri())) {
Daichi Hirono3067d0d2015-12-25 11:08:42 +0900801 // We don't need to change the current root as the current root was not removed.
802 return null;
803 }
804 }
Tomasz Mikolajewskic6a7faf2016-04-15 11:27:53 +0900805
806 // Choose the default root.
807 final RootInfo defaultRoot = mOwner.mRoots.getDefaultRootBlocking(mOwner.mState);
808 assert(defaultRoot != null);
809 if (!defaultRoot.isRecents()) {
Garfield, Tana5588b62016-07-13 09:23:04 -0700810 mDefaultRootDocument = defaultRoot.getRootDocumentBlocking(mOwner);
Tomasz Mikolajewskic6a7faf2016-04-15 11:27:53 +0900811 }
812 return defaultRoot;
Daichi Hirono3067d0d2015-12-25 11:08:42 +0900813 }
814
815 @Override
Tomasz Mikolajewskic6a7faf2016-04-15 11:27:53 +0900816 protected void finish(RootInfo defaultRoot) {
817 if (defaultRoot == null) {
818 return;
819 }
820
Daichi Hirono6ff85c32016-04-19 17:56:13 +0900821 // If the activity has been launched for the specific root and it is removed, finish the
822 // activity.
823 final Uri uri = mOwner.getIntent().getData();
824 if (uri != null && uri.equals(mCurrentRoot.getUri())) {
825 mOwner.finish();
826 return;
827 }
828
Tomasz Mikolajewskic6a7faf2016-04-15 11:27:53 +0900829 // Clear entire backstack and start in new root.
830 mOwner.mState.onRootChanged(defaultRoot);
831 mOwner.mSearchManager.update(defaultRoot);
832
833 if (defaultRoot.isRecents()) {
834 mOwner.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
835 } else {
836 mOwner.openContainerDocument(mDefaultRootDocument);
Daichi Hirono3067d0d2015-12-25 11:08:42 +0900837 }
838 }
839 }
Steve McKayd9caa6a2016-09-15 16:36:45 -0700840
841 public final class RetainedState {
842 public @Nullable Selection selection;
843
844 public boolean hasSelection() {
845 return selection != null;
846 }
847 }
848
849 @VisibleForTesting
850 protected interface EventListener {
851 /**
852 * @param uri Uri navigated to. If recents, then null.
853 */
854 void onDirectoryNavigated(@Nullable Uri uri);
855
856 /**
857 * @param uri Uri of the loaded directory. If recents, then null.
858 */
859 void onDirectoryLoaded(@Nullable Uri uri);
860 }
Steve McKayd0a2a2c2015-03-25 14:35:33 -0700861}