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
17package com.android.documentsui;
18
19import static com.android.documentsui.Shared.DEBUG;
20
21import android.app.Activity;
22import android.app.Fragment;
23import android.app.FragmentManager;
24import android.app.FragmentTransaction;
25import android.app.LoaderManager.LoaderCallbacks;
26import android.content.Context;
27import android.content.Intent;
28import android.content.Loader;
29import android.content.pm.PackageManager;
30import android.content.pm.ResolveInfo;
31import android.net.Uri;
32import android.os.Bundle;
33import android.provider.Settings;
34import android.support.annotation.Nullable;
35import android.text.TextUtils;
36import android.text.format.Formatter;
37import android.util.Log;
38import android.view.LayoutInflater;
39import android.view.View;
40import android.view.ViewGroup;
41import android.widget.AdapterView;
42import android.widget.AdapterView.OnItemClickListener;
43import android.widget.AdapterView.OnItemLongClickListener;
44import android.widget.ArrayAdapter;
45import android.widget.ImageView;
46import android.widget.ListView;
47import android.widget.TextView;
48
49import com.android.documentsui.model.RootInfo;
50
51import java.util.ArrayList;
52import java.util.Collection;
53import java.util.Collections;
54import java.util.Comparator;
55import java.util.List;
56import java.util.Objects;
57
58/**
59 * Display list of known storage backend roots.
60 */
61public 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
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
83    public static RootsFragment get(FragmentManager fm) {
84        return (RootsFragment) fm.findFragmentById(R.id.container_roots);
85    }
86
87    @Override
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
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
136    public void onResume() {
137        super.onResume();
138        onDisplayStateChanged();
139    }
140
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
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     */
177    public void requestFocus() {
178        mList.requestFocus();
179    }
180
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
226        public Item(int layoutId) {
227            mLayoutId = layoutId;
228        }
229
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
240        public abstract void bindView(View convertView);
241    }
242
243    private static class RootItem extends Item {
244        public final RootInfo root;
245
246        public RootItem(RootInfo root) {
247            super(R.layout.item_root);
248            this.root = root;
249        }
250
251        @Override
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 {
274        public SpacerItem() {
275            super(R.layout.item_root_spacer);
276        }
277
278        @Override
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
287        public AppItem(ResolveInfo info) {
288            super(R.layout.item_root);
289            this.info = info;
290        }
291
292        @Override
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         */
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         */
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
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
383        public boolean areAllItemsEnabled() {
384            return false;
385        }
386
387        @Override
388        public boolean isEnabled(int position) {
389            return getItemViewType(position) != 1;
390        }
391
392        @Override
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
403        public int getViewTypeCount() {
404            return 2;
405        }
406    }
407
408    public static class RootComparator implements Comparator<RootItem> {
409        @Override
410        public int compare(RootItem lhs, RootItem rhs) {
411            return lhs.root.compareTo(rhs.root);
412        }
413    }
414}
415