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.DocumentsActivity.State.ACTION_GET_CONTENT;
20 
21 import android.app.Fragment;
22 import android.app.FragmentManager;
23 import android.app.FragmentTransaction;
24 import android.app.LoaderManager.LoaderCallbacks;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.Loader;
28 import android.content.pm.PackageManager;
29 import android.content.pm.ResolveInfo;
30 import android.net.Uri;
31 import android.os.Bundle;
32 import android.provider.Settings;
33 import android.text.TextUtils;
34 import android.text.format.Formatter;
35 import android.view.LayoutInflater;
36 import android.view.View;
37 import android.view.ViewGroup;
38 import android.widget.AdapterView;
39 import android.widget.AdapterView.OnItemClickListener;
40 import android.widget.AdapterView.OnItemLongClickListener;
41 import android.widget.ArrayAdapter;
42 import android.widget.ImageView;
43 import android.widget.ListView;
44 import android.widget.TextView;
45 
46 import com.android.documentsui.DocumentsActivity.State;
47 import com.android.documentsui.model.DocumentInfo;
48 import com.android.documentsui.model.RootInfo;
49 import com.google.common.collect.Lists;
50 
51 import java.util.Collection;
52 import java.util.Collections;
53 import java.util.Comparator;
54 import java.util.List;
55 import java.util.Objects;
56 
57 /**
58  * Display list of known storage backend roots.
59  */
60 public class RootsFragment extends Fragment {
61 
62     private ListView mList;
63     private RootsAdapter mAdapter;
64 
65     private LoaderCallbacks<Collection<RootInfo>> mCallbacks;
66 
67     private static final String EXTRA_INCLUDE_APPS = "includeApps";
68 
show(FragmentManager fm, Intent includeApps)69     public static void show(FragmentManager fm, Intent includeApps) {
70         final Bundle args = new Bundle();
71         args.putParcelable(EXTRA_INCLUDE_APPS, includeApps);
72 
73         final RootsFragment fragment = new RootsFragment();
74         fragment.setArguments(args);
75 
76         final FragmentTransaction ft = fm.beginTransaction();
77         ft.replace(R.id.container_roots, fragment);
78         ft.commitAllowingStateLoss();
79     }
80 
get(FragmentManager fm)81     public static RootsFragment get(FragmentManager fm) {
82         return (RootsFragment) fm.findFragmentById(R.id.container_roots);
83     }
84 
85     @Override
onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)86     public View onCreateView(
87             LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
88         final Context context = inflater.getContext();
89 
90         final View view = inflater.inflate(R.layout.fragment_roots, container, false);
91         mList = (ListView) view.findViewById(android.R.id.list);
92         mList.setOnItemClickListener(mItemListener);
93         mList.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
94 
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 = ((DocumentsActivity) 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()) return;
116 
117                 final Intent includeApps = getArguments().getParcelable(EXTRA_INCLUDE_APPS);
118 
119                 mAdapter = new RootsAdapter(context, result, includeApps);
120                 mList.setAdapter(mAdapter);
121 
122                 onCurrentRootChanged();
123             }
124 
125             @Override
126             public void onLoaderReset(Loader<Collection<RootInfo>> loader) {
127                 mAdapter = null;
128                 mList.setAdapter(null);
129             }
130         };
131     }
132 
133     @Override
onResume()134     public void onResume() {
135         super.onResume();
136         onDisplayStateChanged();
137     }
138 
onDisplayStateChanged()139     public void onDisplayStateChanged() {
140         final Context context = getActivity();
141         final State state = ((DocumentsActivity) context).getDisplayState();
142 
143         if (state.action == ACTION_GET_CONTENT) {
144             mList.setOnItemLongClickListener(mItemLongClickListener);
145         } else {
146             mList.setOnItemLongClickListener(null);
147             mList.setLongClickable(false);
148         }
149 
150         getLoaderManager().restartLoader(2, null, mCallbacks);
151     }
152 
onCurrentRootChanged()153     public void onCurrentRootChanged() {
154         if (mAdapter == null) return;
155 
156         final RootInfo root = ((DocumentsActivity) getActivity()).getCurrentRoot();
157         for (int i = 0; i < mAdapter.getCount(); i++) {
158             final Object item = mAdapter.getItem(i);
159             if (item instanceof RootItem) {
160                 final RootInfo testRoot = ((RootItem) item).root;
161                 if (Objects.equals(testRoot, root)) {
162                     mList.setItemChecked(i, true);
163                     return;
164                 }
165             }
166         }
167     }
168 
showAppDetails(ResolveInfo ri)169     private void showAppDetails(ResolveInfo ri) {
170         final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
171         intent.setData(Uri.fromParts("package", ri.activityInfo.packageName, null));
172         intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
173         startActivity(intent);
174     }
175 
176     private OnItemClickListener mItemListener = new OnItemClickListener() {
177         @Override
178         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
179             final DocumentsActivity activity = DocumentsActivity.get(RootsFragment.this);
180             final Item item = mAdapter.getItem(position);
181             if (item instanceof RootItem) {
182                 activity.onRootPicked(((RootItem) item).root, true);
183             } else if (item instanceof AppItem) {
184                 activity.onAppPicked(((AppItem) item).info);
185             } else {
186                 throw new IllegalStateException("Unknown root: " + item);
187             }
188         }
189     };
190 
191     private OnItemLongClickListener mItemLongClickListener = new OnItemLongClickListener() {
192         @Override
193         public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
194             final Item item = mAdapter.getItem(position);
195             if (item instanceof AppItem) {
196                 showAppDetails(((AppItem) item).info);
197                 return true;
198             } else {
199                 return false;
200             }
201         }
202     };
203 
204     private static abstract class Item {
205         private final int mLayoutId;
206 
Item(int layoutId)207         public Item(int layoutId) {
208             mLayoutId = layoutId;
209         }
210 
getView(View convertView, ViewGroup parent)211         public View getView(View convertView, ViewGroup parent) {
212             if (convertView == null) {
213                 convertView = LayoutInflater.from(parent.getContext())
214                         .inflate(mLayoutId, parent, false);
215             }
216             bindView(convertView);
217             return convertView;
218         }
219 
bindView(View convertView)220         public abstract void bindView(View convertView);
221     }
222 
223     private static class RootItem extends Item {
224         public final RootInfo root;
225 
RootItem(RootInfo root)226         public RootItem(RootInfo root) {
227             super(R.layout.item_root);
228             this.root = root;
229         }
230 
231         @Override
bindView(View convertView)232         public void bindView(View convertView) {
233             final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon);
234             final TextView title = (TextView) convertView.findViewById(android.R.id.title);
235             final TextView summary = (TextView) convertView.findViewById(android.R.id.summary);
236 
237             final Context context = convertView.getContext();
238             icon.setImageDrawable(root.loadDrawerIcon(context));
239             title.setText(root.title);
240 
241             // Show available space if no summary
242             String summaryText = root.summary;
243             if (TextUtils.isEmpty(summaryText) && root.availableBytes >= 0) {
244                 summaryText = context.getString(R.string.root_available_bytes,
245                         Formatter.formatFileSize(context, root.availableBytes));
246             }
247 
248             summary.setText(summaryText);
249             summary.setVisibility(TextUtils.isEmpty(summaryText) ? View.GONE : View.VISIBLE);
250         }
251     }
252 
253     private static class SpacerItem extends Item {
SpacerItem()254         public SpacerItem() {
255             super(R.layout.item_root_spacer);
256         }
257 
258         @Override
bindView(View convertView)259         public void bindView(View convertView) {
260             // Nothing to bind
261         }
262     }
263 
264     private static class AppItem extends Item {
265         public final ResolveInfo info;
266 
AppItem(ResolveInfo info)267         public AppItem(ResolveInfo info) {
268             super(R.layout.item_root);
269             this.info = info;
270         }
271 
272         @Override
bindView(View convertView)273         public void bindView(View convertView) {
274             final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon);
275             final TextView title = (TextView) convertView.findViewById(android.R.id.title);
276             final TextView summary = (TextView) convertView.findViewById(android.R.id.summary);
277 
278             final PackageManager pm = convertView.getContext().getPackageManager();
279             icon.setImageDrawable(info.loadIcon(pm));
280             title.setText(info.loadLabel(pm));
281 
282             // TODO: match existing summary behavior from disambig dialog
283             summary.setVisibility(View.GONE);
284         }
285     }
286 
287     private static class RootsAdapter extends ArrayAdapter<Item> {
RootsAdapter(Context context, Collection<RootInfo> roots, Intent includeApps)288         public RootsAdapter(Context context, Collection<RootInfo> roots, Intent includeApps) {
289             super(context, 0);
290 
291             RootItem recents = null;
292             RootItem images = null;
293             RootItem videos = null;
294             RootItem audio = null;
295             RootItem downloads = null;
296 
297             final List<RootInfo> clouds = Lists.newArrayList();
298             final List<RootInfo> locals = Lists.newArrayList();
299 
300             for (RootInfo root : roots) {
301                 if (root.isRecents()) {
302                     recents = new RootItem(root);
303                 } else if (root.isExternalStorage()) {
304                     locals.add(root);
305                 } else if (root.isDownloads()) {
306                     downloads = new RootItem(root);
307                 } else if (root.isImages()) {
308                     images = new RootItem(root);
309                 } else if (root.isVideos()) {
310                     videos = new RootItem(root);
311                 } else if (root.isAudio()) {
312                     audio = new RootItem(root);
313                 } else {
314                     clouds.add(root);
315                 }
316             }
317 
318             final RootComparator comp = new RootComparator();
319             Collections.sort(clouds, comp);
320             Collections.sort(locals, comp);
321 
322             if (recents != null) add(recents);
323 
324             for (RootInfo cloud : clouds) {
325                 add(new RootItem(cloud));
326             }
327 
328             if (images != null) add(images);
329             if (videos != null) add(videos);
330             if (audio != null) add(audio);
331             if (downloads != null) add(downloads);
332 
333             for (RootInfo local : locals) {
334                 add(new RootItem(local));
335             }
336 
337             if (includeApps != null) {
338                 final PackageManager pm = context.getPackageManager();
339                 final List<ResolveInfo> infos = pm.queryIntentActivities(
340                         includeApps, PackageManager.MATCH_DEFAULT_ONLY);
341 
342                 final List<AppItem> apps = Lists.newArrayList();
343 
344                 // Omit ourselves from the list
345                 for (ResolveInfo info : infos) {
346                     if (!context.getPackageName().equals(info.activityInfo.packageName)) {
347                         apps.add(new AppItem(info));
348                     }
349                 }
350 
351                 if (apps.size() > 0) {
352                     add(new SpacerItem());
353                     for (Item item : apps) {
354                         add(item);
355                     }
356                 }
357             }
358         }
359 
360         @Override
getView(int position, View convertView, ViewGroup parent)361         public View getView(int position, View convertView, ViewGroup parent) {
362             final Item item = getItem(position);
363             return item.getView(convertView, parent);
364         }
365 
366         @Override
areAllItemsEnabled()367         public boolean areAllItemsEnabled() {
368             return false;
369         }
370 
371         @Override
isEnabled(int position)372         public boolean isEnabled(int position) {
373             return getItemViewType(position) != 1;
374         }
375 
376         @Override
getItemViewType(int position)377         public int getItemViewType(int position) {
378             final Item item = getItem(position);
379             if (item instanceof RootItem || item instanceof AppItem) {
380                 return 0;
381             } else {
382                 return 1;
383             }
384         }
385 
386         @Override
getViewTypeCount()387         public int getViewTypeCount() {
388             return 2;
389         }
390     }
391 
392     public static class RootComparator implements Comparator<RootInfo> {
393         @Override
compare(RootInfo lhs, RootInfo rhs)394         public int compare(RootInfo lhs, RootInfo rhs) {
395             final int score = DocumentInfo.compareToIgnoreCaseNullable(lhs.title, rhs.title);
396             if (score != 0) {
397                 return score;
398             } else {
399                 return DocumentInfo.compareToIgnoreCaseNullable(lhs.summary, rhs.summary);
400             }
401         }
402     }
403 }
404