1 /*
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 
17 package com.android.documentsui;
18 
19 import static com.android.documentsui.Shared.DEBUG;
20 
21 import android.app.Activity;
22 import android.app.Fragment;
23 import android.app.FragmentManager;
24 import android.app.FragmentTransaction;
25 import android.app.LoaderManager.LoaderCallbacks;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.Loader;
29 import android.content.pm.PackageManager;
30 import android.content.pm.ResolveInfo;
31 import android.net.Uri;
32 import android.os.Bundle;
33 import android.provider.Settings;
34 import android.support.annotation.Nullable;
35 import android.text.TextUtils;
36 import android.text.format.Formatter;
37 import android.util.Log;
38 import android.view.LayoutInflater;
39 import android.view.View;
40 import android.view.ViewGroup;
41 import android.widget.AdapterView;
42 import android.widget.AdapterView.OnItemClickListener;
43 import android.widget.AdapterView.OnItemLongClickListener;
44 import android.widget.ArrayAdapter;
45 import android.widget.ImageView;
46 import android.widget.ListView;
47 import android.widget.TextView;
48 
49 import com.android.documentsui.model.RootInfo;
50 
51 import java.util.ArrayList;
52 import java.util.Collection;
53 import java.util.Collections;
54 import java.util.Comparator;
55 import java.util.List;
56 import java.util.Objects;
57 
58 /**
59  * Display list of known storage backend roots.
60  */
61 public class RootsFragment extends Fragment {
62 
63     private static final String TAG = "RootsFragment";
64     private static final String EXTRA_INCLUDE_APPS = "includeApps";
65 
66     private ListView mList;
67     private RootsAdapter mAdapter;
68     private LoaderCallbacks<Collection<RootInfo>> mCallbacks;
69 
70 
show(FragmentManager fm, Intent includeApps)71     public static void show(FragmentManager fm, Intent includeApps) {
72         final Bundle args = new Bundle();
73         args.putParcelable(EXTRA_INCLUDE_APPS, includeApps);
74 
75         final RootsFragment fragment = new RootsFragment();
76         fragment.setArguments(args);
77 
78         final FragmentTransaction ft = fm.beginTransaction();
79         ft.replace(R.id.container_roots, fragment);
80         ft.commitAllowingStateLoss();
81     }
82 
get(FragmentManager fm)83     public static RootsFragment get(FragmentManager fm) {
84         return (RootsFragment) fm.findFragmentById(R.id.container_roots);
85     }
86 
87     @Override
onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)88     public View onCreateView(
89             LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
90 
91         final View view = inflater.inflate(R.layout.fragment_roots, container, false);
92         mList = (ListView) view.findViewById(R.id.roots_list);
93         mList.setOnItemClickListener(mItemListener);
94         mList.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
95         return view;
96     }
97 
98     @Override
onActivityCreated(Bundle savedInstanceState)99     public void onActivityCreated(Bundle savedInstanceState) {
100         super.onActivityCreated(savedInstanceState);
101 
102         final Context context = getActivity();
103         final RootsCache roots = DocumentsApplication.getRootsCache(context);
104         final State state = ((BaseActivity) context).getDisplayState();
105 
106         mCallbacks = new LoaderCallbacks<Collection<RootInfo>>() {
107             @Override
108             public Loader<Collection<RootInfo>> onCreateLoader(int id, Bundle args) {
109                 return new RootsLoader(context, roots, state);
110             }
111 
112             @Override
113             public void onLoadFinished(
114                     Loader<Collection<RootInfo>> loader, Collection<RootInfo> result) {
115                 if (!isAdded()) {
116                     return;
117                 }
118 
119                 Intent handlerAppIntent = getArguments().getParcelable(EXTRA_INCLUDE_APPS);
120 
121                 mAdapter = new RootsAdapter(context, result, handlerAppIntent, state);
122                 mList.setAdapter(mAdapter);
123 
124                 onCurrentRootChanged();
125             }
126 
127             @Override
128             public void onLoaderReset(Loader<Collection<RootInfo>> loader) {
129                 mAdapter = null;
130                 mList.setAdapter(null);
131             }
132         };
133     }
134 
135     @Override
onResume()136     public void onResume() {
137         super.onResume();
138         onDisplayStateChanged();
139     }
140 
onDisplayStateChanged()141     public void onDisplayStateChanged() {
142         final Context context = getActivity();
143         final State state = ((BaseActivity) context).getDisplayState();
144 
145         if (state.action == State.ACTION_GET_CONTENT) {
146             mList.setOnItemLongClickListener(mItemLongClickListener);
147         } else {
148             mList.setOnItemLongClickListener(null);
149             mList.setLongClickable(false);
150         }
151 
152         getLoaderManager().restartLoader(2, null, mCallbacks);
153     }
154 
onCurrentRootChanged()155     public void onCurrentRootChanged() {
156         if (mAdapter == null) {
157             return;
158         }
159 
160         final RootInfo root = ((BaseActivity) getActivity()).getCurrentRoot();
161         for (int i = 0; i < mAdapter.getCount(); i++) {
162             final Object item = mAdapter.getItem(i);
163             if (item instanceof RootItem) {
164                 final RootInfo testRoot = ((RootItem) item).root;
165                 if (Objects.equals(testRoot, root)) {
166                     mList.setItemChecked(i, true);
167                     mList.setSelection(i);
168                     return;
169                 }
170             }
171         }
172     }
173 
174     /**
175      * Attempts to shift focus back to the navigation drawer.
176      */
requestFocus()177     public void requestFocus() {
178         mList.requestFocus();
179     }
180 
showAppDetails(ResolveInfo ri)181     private void showAppDetails(ResolveInfo ri) {
182         final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
183         intent.setData(Uri.fromParts("package", ri.activityInfo.packageName, null));
184         intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
185         startActivity(intent);
186     }
187 
188     private OnItemClickListener mItemListener = new OnItemClickListener() {
189         @Override
190         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
191             Item item = mAdapter.getItem(position);
192             if (item instanceof RootItem) {
193                 BaseActivity activity = BaseActivity.get(RootsFragment.this);
194                 RootInfo newRoot = ((RootItem) item).root;
195                 Metrics.logRootVisited(getActivity(), newRoot);
196                 activity.onRootPicked(newRoot);
197             } else if (item instanceof AppItem) {
198                 DocumentsActivity activity = DocumentsActivity.get(RootsFragment.this);
199                 ResolveInfo info = ((AppItem) item).info;
200                 Metrics.logAppVisited(getActivity(), info);
201                 activity.onAppPicked(info);
202             } else if (item instanceof SpacerItem) {
203                 if (DEBUG) Log.d(TAG, "Ignoring click on spacer item.");
204             } else {
205                 throw new IllegalStateException("Unknown root: " + item);
206             }
207         }
208     };
209 
210     private OnItemLongClickListener mItemLongClickListener = new OnItemLongClickListener() {
211         @Override
212         public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
213             final Item item = mAdapter.getItem(position);
214             if (item instanceof AppItem) {
215                 showAppDetails(((AppItem) item).info);
216                 return true;
217             } else {
218                 return false;
219             }
220         }
221     };
222 
223     private static abstract class Item {
224         private final int mLayoutId;
225 
Item(int layoutId)226         public Item(int layoutId) {
227             mLayoutId = layoutId;
228         }
229 
getView(View convertView, ViewGroup parent)230         public View getView(View convertView, ViewGroup parent) {
231             // Disable recycling views because 1) it's very unlikely a view can be recycled here;
232             // 2) there is no easy way for us to know with which layout id the convertView was
233             // inflated; and 3) simplicity is much appreciated at this time.
234             convertView = LayoutInflater.from(parent.getContext())
235                         .inflate(mLayoutId, parent, false);
236             bindView(convertView);
237             return convertView;
238         }
239 
bindView(View convertView)240         public abstract void bindView(View convertView);
241     }
242 
243     private static class RootItem extends Item {
244         public final RootInfo root;
245 
RootItem(RootInfo root)246         public RootItem(RootInfo root) {
247             super(R.layout.item_root);
248             this.root = root;
249         }
250 
251         @Override
bindView(View convertView)252         public void bindView(View convertView) {
253             final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon);
254             final TextView title = (TextView) convertView.findViewById(android.R.id.title);
255             final TextView summary = (TextView) convertView.findViewById(android.R.id.summary);
256 
257             final Context context = convertView.getContext();
258             icon.setImageDrawable(root.loadDrawerIcon(context));
259             title.setText(root.title);
260 
261             // Show available space if no summary
262             String summaryText = root.summary;
263             if (TextUtils.isEmpty(summaryText) && root.availableBytes >= 0) {
264                 summaryText = context.getString(R.string.root_available_bytes,
265                         Formatter.formatFileSize(context, root.availableBytes));
266             }
267 
268             summary.setText(summaryText);
269             summary.setVisibility(TextUtils.isEmpty(summaryText) ? View.GONE : View.VISIBLE);
270         }
271     }
272 
273     private static class SpacerItem extends Item {
SpacerItem()274         public SpacerItem() {
275             super(R.layout.item_root_spacer);
276         }
277 
278         @Override
bindView(View convertView)279         public void bindView(View convertView) {
280             // Nothing to bind
281         }
282     }
283 
284     private static class AppItem extends Item {
285         public final ResolveInfo info;
286 
AppItem(ResolveInfo info)287         public AppItem(ResolveInfo info) {
288             super(R.layout.item_root);
289             this.info = info;
290         }
291 
292         @Override
bindView(View convertView)293         public void bindView(View convertView) {
294             final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon);
295             final TextView title = (TextView) convertView.findViewById(android.R.id.title);
296             final TextView summary = (TextView) convertView.findViewById(android.R.id.summary);
297 
298             final PackageManager pm = convertView.getContext().getPackageManager();
299             icon.setImageDrawable(info.loadIcon(pm));
300             title.setText(info.loadLabel(pm));
301 
302             // TODO: match existing summary behavior from disambig dialog
303             summary.setVisibility(View.GONE);
304         }
305     }
306 
307     private static class RootsAdapter extends ArrayAdapter<Item> {
308 
309         /**
310          * @param handlerAppIntent When not null, apps capable of handling the original
311          *     intent will be included in list of roots (in special section at bottom).
312          */
RootsAdapter(Context context, Collection<RootInfo> roots, @Nullable Intent handlerAppIntent, State state)313         public RootsAdapter(Context context, Collection<RootInfo> roots,
314                 @Nullable Intent handlerAppIntent, State state) {
315             super(context, 0);
316 
317             final List<RootItem> libraries = new ArrayList<>();
318             final List<RootItem> others = new ArrayList<>();
319 
320             for (final RootInfo root : roots) {
321                 final RootItem item = new RootItem(root);
322 
323                 if (root.isHome() &&
324                         !Shared.shouldShowDocumentsRoot(context, ((Activity) context).getIntent())) {
325                     continue;
326                 } else if (root.isLibrary()) {
327                     if (DEBUG) Log.d(TAG, "Adding " + root + " as library.");
328                     libraries.add(item);
329                 } else {
330                     if (DEBUG) Log.d(TAG, "Adding " + root + " as non-library.");
331                     others.add(item);
332                 }
333             }
334 
335             final RootComparator comp = new RootComparator();
336             Collections.sort(libraries, comp);
337             Collections.sort(others, comp);
338 
339             addAll(libraries);
340             // Only add the spacer if it is actually separating something.
341             if (!libraries.isEmpty() && !others.isEmpty()) {
342                 add(new SpacerItem());
343             }
344             addAll(others);
345 
346             // Include apps that can handle this intent too.
347             if (handlerAppIntent != null) {
348                 includeHandlerApps(context, handlerAppIntent);
349             }
350         }
351 
352         /**
353          * Adds apps capable of handling the original intent will be included
354          * in list of roots (in special section at bottom).
355          */
includeHandlerApps(Context context, Intent handlerAppIntent)356         private void includeHandlerApps(Context context, Intent handlerAppIntent) {
357             final PackageManager pm = context.getPackageManager();
358             final List<ResolveInfo> infos = pm.queryIntentActivities(
359                     handlerAppIntent, PackageManager.MATCH_DEFAULT_ONLY);
360 
361             final List<AppItem> apps = new ArrayList<>();
362 
363             // Omit ourselves from the list
364             for (ResolveInfo info : infos) {
365                 if (!context.getPackageName().equals(info.activityInfo.packageName)) {
366                     apps.add(new AppItem(info));
367                 }
368             }
369 
370             if (apps.size() > 0) {
371                 add(new SpacerItem());
372                 addAll(apps);
373             }
374         }
375 
376         @Override
getView(int position, View convertView, ViewGroup parent)377         public View getView(int position, View convertView, ViewGroup parent) {
378             final Item item = getItem(position);
379             return item.getView(convertView, parent);
380         }
381 
382         @Override
areAllItemsEnabled()383         public boolean areAllItemsEnabled() {
384             return false;
385         }
386 
387         @Override
isEnabled(int position)388         public boolean isEnabled(int position) {
389             return getItemViewType(position) != 1;
390         }
391 
392         @Override
getItemViewType(int position)393         public int getItemViewType(int position) {
394             final Item item = getItem(position);
395             if (item instanceof RootItem || item instanceof AppItem) {
396                 return 0;
397             } else {
398                 return 1;
399             }
400         }
401 
402         @Override
getViewTypeCount()403         public int getViewTypeCount() {
404             return 2;
405         }
406     }
407 
408     public static class RootComparator implements Comparator<RootItem> {
409         @Override
compare(RootItem lhs, RootItem rhs)410         public int compare(RootItem lhs, RootItem rhs) {
411             return lhs.root.compareTo(rhs.root);
412         }
413     }
414 }
415