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.android.internal.util.Objects; 50 import com.google.common.collect.Lists; 51 52 import java.util.Collection; 53 import java.util.Collections; 54 import java.util.Comparator; 55 import java.util.List; 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 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 81 public static RootsFragment get(FragmentManager fm) { 82 return (RootsFragment) fm.findFragmentById(R.id.container_roots); 83 } 84 85 @Override 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 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 134 public void onResume() { 135 super.onResume(); 136 137 final Context context = getActivity(); 138 final State state = ((DocumentsActivity) context).getDisplayState(); 139 state.showAdvanced = state.forceAdvanced 140 | SettingsActivity.getDisplayAdvancedDevices(context); 141 142 if (state.action == ACTION_GET_CONTENT) { 143 mList.setOnItemLongClickListener(mItemLongClickListener); 144 } else { 145 mList.setOnItemLongClickListener(null); 146 mList.setLongClickable(false); 147 } 148 149 getLoaderManager().restartLoader(2, null, mCallbacks); 150 } 151 152 public void onCurrentRootChanged() { 153 if (mAdapter == null) return; 154 155 final RootInfo root = ((DocumentsActivity) getActivity()).getCurrentRoot(); 156 for (int i = 0; i < mAdapter.getCount(); i++) { 157 final Object item = mAdapter.getItem(i); 158 if (item instanceof RootItem) { 159 final RootInfo testRoot = ((RootItem) item).root; 160 if (Objects.equal(testRoot, root)) { 161 mList.setItemChecked(i, true); 162 return; 163 } 164 } 165 } 166 } 167 168 private void showAppDetails(ResolveInfo ri) { 169 final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); 170 intent.setData(Uri.fromParts("package", ri.activityInfo.packageName, null)); 171 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 172 startActivity(intent); 173 } 174 175 private OnItemClickListener mItemListener = new OnItemClickListener() { 176 @Override 177 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 178 final DocumentsActivity activity = DocumentsActivity.get(RootsFragment.this); 179 final Item item = mAdapter.getItem(position); 180 if (item instanceof RootItem) { 181 activity.onRootPicked(((RootItem) item).root, true); 182 } else if (item instanceof AppItem) { 183 activity.onAppPicked(((AppItem) item).info); 184 } else { 185 throw new IllegalStateException("Unknown root: " + item); 186 } 187 } 188 }; 189 190 private OnItemLongClickListener mItemLongClickListener = new OnItemLongClickListener() { 191 @Override 192 public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { 193 final Item item = mAdapter.getItem(position); 194 if (item instanceof AppItem) { 195 showAppDetails(((AppItem) item).info); 196 return true; 197 } else { 198 return false; 199 } 200 } 201 }; 202 203 private static abstract class Item { 204 private final int mLayoutId; 205 206 public Item(int layoutId) { 207 mLayoutId = layoutId; 208 } 209 210 public View getView(View convertView, ViewGroup parent) { 211 if (convertView == null) { 212 convertView = LayoutInflater.from(parent.getContext()) 213 .inflate(mLayoutId, parent, false); 214 } 215 bindView(convertView); 216 return convertView; 217 } 218 219 public abstract void bindView(View convertView); 220 } 221 222 private static class RootItem extends Item { 223 public final RootInfo root; 224 225 public RootItem(RootInfo root) { 226 super(R.layout.item_root); 227 this.root = root; 228 } 229 230 @Override 231 public void bindView(View convertView) { 232 final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon); 233 final TextView title = (TextView) convertView.findViewById(android.R.id.title); 234 final TextView summary = (TextView) convertView.findViewById(android.R.id.summary); 235 236 final Context context = convertView.getContext(); 237 icon.setImageDrawable(root.loadIcon(context)); 238 title.setText(root.title); 239 240 // Show available space if no summary 241 String summaryText = root.summary; 242 if (TextUtils.isEmpty(summaryText) && root.availableBytes >= 0) { 243 summaryText = context.getString(R.string.root_available_bytes, 244 Formatter.formatFileSize(context, root.availableBytes)); 245 } 246 247 summary.setText(summaryText); 248 summary.setVisibility(TextUtils.isEmpty(summaryText) ? View.GONE : View.VISIBLE); 249 } 250 } 251 252 private static class SpacerItem extends Item { 253 public SpacerItem() { 254 super(R.layout.item_root_spacer); 255 } 256 257 @Override 258 public void bindView(View convertView) { 259 // Nothing to bind 260 } 261 } 262 263 private static class AppItem extends Item { 264 public final ResolveInfo info; 265 266 public AppItem(ResolveInfo info) { 267 super(R.layout.item_root); 268 this.info = info; 269 } 270 271 @Override 272 public void bindView(View convertView) { 273 final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon); 274 final TextView title = (TextView) convertView.findViewById(android.R.id.title); 275 final TextView summary = (TextView) convertView.findViewById(android.R.id.summary); 276 277 final PackageManager pm = convertView.getContext().getPackageManager(); 278 icon.setImageDrawable(info.loadIcon(pm)); 279 title.setText(info.loadLabel(pm)); 280 281 // TODO: match existing summary behavior from disambig dialog 282 summary.setVisibility(View.GONE); 283 } 284 } 285 286 private static class RootsAdapter extends ArrayAdapter<Item> { 287 public RootsAdapter(Context context, Collection<RootInfo> roots, Intent includeApps) { 288 super(context, 0); 289 290 RootItem recents = null; 291 RootItem images = null; 292 RootItem videos = null; 293 RootItem audio = null; 294 RootItem downloads = null; 295 296 final List<RootInfo> clouds = Lists.newArrayList(); 297 final List<RootInfo> locals = Lists.newArrayList(); 298 299 for (RootInfo root : roots) { 300 if (root.isRecents()) { 301 recents = new RootItem(root); 302 } else if (root.isExternalStorage()) { 303 locals.add(root); 304 } else if (root.isDownloads()) { 305 downloads = new RootItem(root); 306 } else if (root.isImages()) { 307 images = new RootItem(root); 308 } else if (root.isVideos()) { 309 videos = new RootItem(root); 310 } else if (root.isAudio()) { 311 audio = new RootItem(root); 312 } else { 313 clouds.add(root); 314 } 315 } 316 317 final RootComparator comp = new RootComparator(); 318 Collections.sort(clouds, comp); 319 Collections.sort(locals, comp); 320 321 if (recents != null) add(recents); 322 323 for (RootInfo cloud : clouds) { 324 add(new RootItem(cloud)); 325 } 326 327 if (images != null) add(images); 328 if (videos != null) add(videos); 329 if (audio != null) add(audio); 330 if (downloads != null) add(downloads); 331 332 for (RootInfo local : locals) { 333 add(new RootItem(local)); 334 } 335 336 if (includeApps != null) { 337 final PackageManager pm = context.getPackageManager(); 338 final List<ResolveInfo> infos = pm.queryIntentActivities( 339 includeApps, PackageManager.MATCH_DEFAULT_ONLY); 340 341 final List<AppItem> apps = Lists.newArrayList(); 342 343 // Omit ourselves from the list 344 for (ResolveInfo info : infos) { 345 if (!context.getPackageName().equals(info.activityInfo.packageName)) { 346 apps.add(new AppItem(info)); 347 } 348 } 349 350 if (apps.size() > 0) { 351 add(new SpacerItem()); 352 for (Item item : apps) { 353 add(item); 354 } 355 } 356 } 357 } 358 359 @Override 360 public View getView(int position, View convertView, ViewGroup parent) { 361 final Item item = getItem(position); 362 return item.getView(convertView, parent); 363 } 364 365 @Override 366 public boolean areAllItemsEnabled() { 367 return false; 368 } 369 370 @Override 371 public boolean isEnabled(int position) { 372 return getItemViewType(position) != 1; 373 } 374 375 @Override 376 public int getItemViewType(int position) { 377 final Item item = getItem(position); 378 if (item instanceof RootItem || item instanceof AppItem) { 379 return 0; 380 } else { 381 return 1; 382 } 383 } 384 385 @Override 386 public int getViewTypeCount() { 387 return 2; 388 } 389 } 390 391 public static class RootComparator implements Comparator<RootInfo> { 392 @Override 393 public int compare(RootInfo lhs, RootInfo rhs) { 394 final int score = DocumentInfo.compareToIgnoreCaseNullable(lhs.title, rhs.title); 395 if (score != 0) { 396 return score; 397 } else { 398 return DocumentInfo.compareToIgnoreCaseNullable(lhs.summary, rhs.summary); 399 } 400 } 401 } 402 } 403