Merge "Iterate on storage UI."

This commit is contained in:
Jeff Sharkey
2013-07-01 22:34:10 +00:00
committed by Android (Google) Code Review
22 changed files with 1138 additions and 87 deletions

View File

@@ -7,13 +7,15 @@
<activity
android:name=".DocumentsActivity"
android:finishOnCloseSystemDialogs="true"
android:excludeFromRecents="true">
android:excludeFromRecents="true"
android:theme="@android:style/Theme.Holo.Light">
<intent-filter android:priority="100">
<action android:name="android.intent.action.OPEN_DOCUMENT" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter android:priority="100">
<action android:name="android.intent.action.CREATE_DOCUMENT" />
<data android:mimeType="*/*"/>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 B

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2013 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:id="@+id/directory"
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="1" />
<FrameLayout
android:id="@+id/save"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2013 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<GridView
android:id="@+id/grid"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:listSelector="@android:color/transparent"
android:paddingTop="?android:attr/listPreferredItemPaddingStart"
android:paddingStart="?android:attr/listPreferredItemPaddingStart" />
</FrameLayout>

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2013 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<GridView
android:id="@+id/grid"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:listSelector="@android:color/transparent"
android:paddingTop="?android:attr/listPreferredItemPaddingStart"
android:paddingStart="?android:attr/listPreferredItemPaddingStart" />
</FrameLayout>

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2013 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:background="@color/chip"
android:minHeight="?android:attr/listPreferredItemHeightSmall">
<ImageView
android:id="@android:id/icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:scaleType="centerInside"
android:contentDescription="@null" />
<EditText
android:id="@android:id/title"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:singleLine="true" />
<Button
android:id="@android:id/button1"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?android:attr/selectableItemBackground"
android:text="@string/btn_save"
android:textAllCaps="true"
android:textAppearance="?android:attr/textAppearanceSmall"
android:padding="8dp" />
</LinearLayout>

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2013 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="?android:attr/listPreferredItemPaddingEnd"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/chip"
android:foreground="?android:attr/selectableItemBackground"
android:duplicateParentState="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:orientation="horizontal">
<ImageView
android:id="@android:id/icon"
android:layout_width="24dip"
android:layout_height="24dip"
android:layout_marginEnd="8dp"
android:scaleType="centerInside"
android:contentDescription="@null" />
<TextView
android:id="@android:id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:singleLine="true"
android:ellipsize="marquee"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textAlignment="viewStart" />
</LinearLayout>
</FrameLayout>
</FrameLayout>

View File

@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2013 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="140dip"
android:paddingBottom="?android:attr/listPreferredItemPaddingEnd"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/chip"
android:foreground="?android:attr/selectableItemBackground"
android:duplicateParentState="true">
<GridLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:columnCount="1">
<ImageView
android:id="@android:id/icon"
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_gravity="fill_vertical"
android:background="#bbb"
android:scaleType="centerInside"
android:contentDescription="@null" />
<TextView
android:id="@android:id/title"
android:layout_width="match_parent"
android:layout_marginStart="8dip"
android:layout_marginEnd="8dip"
android:layout_marginTop="8dip"
android:layout_marginBottom="8dip"
android:singleLine="true"
android:ellipsize="marquee"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textAlignment="viewStart" />
</GridLayout>
</FrameLayout>
</FrameLayout>

View File

@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2013 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?android:attr/listPreferredItemHeight"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:paddingTop="8dip"
android:paddingBottom="8dip"
android:columnCount="3">
<ImageView
android:id="@android:id/icon"
android:layout_width="@android:dimen/app_icon_size"
android:layout_height="@android:dimen/app_icon_size"
android:layout_rowSpan="2"
android:layout_marginEnd="8dip"
android:scaleType="centerInside"
android:contentDescription="@null" />
<TextView
android:id="@android:id/title"
android:layout_width="0dip"
android:layout_gravity="fill_horizontal"
android:layout_marginTop="2dip"
android:layout_columnSpan="2"
android:singleLine="true"
android:ellipsize="marquee"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textAlignment="viewStart" />
<ImageView
android:id="@android:id/icon1"
android:layout_width="24dip"
android:layout_height="24dip"
android:layout_marginEnd="8dip"
android:visibility="gone"
android:scaleType="centerInside"
android:contentDescription="@null" />
<TextView
android:id="@android:id/summary"
android:layout_marginTop="2dip"
android:textAppearance="?android:attr/textAppearanceSmall" />
</GridLayout>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2013 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_create_dir"
android:title="@string/menu_create_dir"
android:icon="@drawable/ic_menu_create_dir" />
</menu>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2013 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_grid"
android:title="@string/menu_grid"
android:icon="@drawable/ic_menu_grid" />
<item
android:id="@+id/menu_list"
android:title="@string/menu_list"
android:icon="@drawable/ic_menu_list" />
<item
android:id="@+id/menu_sort"
android:title="@string/menu_sort"
android:icon="@drawable/ic_menu_sort" />
</menu>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2013 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<color name="chip">#ddd</color>
</resources>

View File

@@ -16,4 +16,15 @@
<resources>
<string name="app_label">Documents</string>
<string name="title_open">Open</string>
<string name="title_save">Save</string>
<string name="menu_create_dir">Create folder</string>
<string name="menu_grid">Grid view</string>
<string name="menu_list">List view</string>
<string name="menu_sort">Sort by</string>
<string name="btn_save">Save</string>
</resources>

View File

@@ -0,0 +1,119 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.documentsui;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.os.Bundle;
import android.provider.DocumentsContract;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.TextView;
import com.google.android.collect.Lists;
import java.util.List;
/**
* Display all known storage backends.
*/
public class BackendFragment extends Fragment {
// TODO: handle multiple accounts from single backend
private GridView mGridView;
private BackendAdapter mAdapter;
public static void show(FragmentManager fm) {
final BackendFragment fragment = new BackendFragment();
final FragmentTransaction ft = fm.beginTransaction();
ft.replace(R.id.directory, fragment);
ft.setBreadCrumbTitle("TOP");
ft.commitAllowingStateLoss();
}
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final Context context = inflater.getContext();
// Gather known storage providers
final List<ProviderInfo> providers = context.getPackageManager()
.queryContentProviders(null, -1, PackageManager.GET_META_DATA);
final List<ProviderInfo> backends = Lists.newArrayList();
for (ProviderInfo info : providers) {
if (info.metaData != null
&& info.metaData.containsKey(DocumentsContract.META_DATA_DOCUMENT_PROVIDER)) {
backends.add(info);
}
}
final View view = inflater.inflate(R.layout.fragment_backend, container, false);
mGridView = (GridView) view.findViewById(R.id.grid);
mGridView.setOnItemClickListener(mItemListener);
mAdapter = new BackendAdapter(context, backends);
mGridView.setAdapter(mAdapter);
mGridView.setNumColumns(GridView.AUTO_FIT);
return view;
}
private OnItemClickListener mItemListener = new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
final ProviderInfo info = mAdapter.getItem(position);
((DocumentsActivity) getActivity()).onBackendPicked(info);
}
};
public static class BackendAdapter extends ArrayAdapter<ProviderInfo> {
public BackendAdapter(Context context, List<ProviderInfo> list) {
super(context, android.R.layout.simple_list_item_1, list);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_backend, parent, false);
}
final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon);
final TextView text1 = (TextView) convertView.findViewById(android.R.id.text1);
final PackageManager pm = parent.getContext().getPackageManager();
final ProviderInfo info = getItem(position);
icon.setImageDrawable(info.loadIcon(pm));
text1.setText(info.loadLabel(pm));
return convertView;
}
}
}

View File

@@ -0,0 +1,139 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.documentsui;
import android.database.DataSetObserver;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.LinearLayout;
import android.widget.ListAdapter;
import android.widget.ListView;
import com.android.internal.util.Preconditions;
/**
* Adapter that wraps an existing adapter, presenting its contents in multiple
* equally-sized horizontal columns.
*/
public class ColumnAdapter extends BaseAdapter {
private final ListAdapter mWrapped;
private final OnItemClickListener mListener;
private int mColumns = 1;
public interface OnItemClickListener {
public void onItemClick(ListAdapter adapter, int position);
}
public ColumnAdapter(ListAdapter wrapped, OnItemClickListener listener) {
mWrapped = Preconditions.checkNotNull(wrapped);
mListener = Preconditions.checkNotNull(listener);
if (!wrapped.areAllItemsEnabled()) {
throw new IllegalStateException("All items must be enabled");
}
if (wrapped.getViewTypeCount() > 1) {
throw new IllegalStateException("All items must be identical");
}
}
public static void prepare(ListView list) {
list.setItemsCanFocus(true);
}
public void setColumns(int columns) {
mColumns = columns;
notifyDataSetChanged();
}
private View.OnClickListener mItemListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
final int position = (Integer) v.getTag();
mListener.onItemClick(mWrapped, position);
}
};
@Override
public int getCount() {
return (mWrapped.getCount() + mColumns - 1) / mColumns;
}
@Override
public Object getItem(int position) {
return position;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = new LinearLayout(parent.getContext());
}
final LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
0, LinearLayout.LayoutParams.WRAP_CONTENT);
params.weight = 1f / mColumns;
final LinearLayout row = (LinearLayout) convertView;
final int first = position * mColumns;
final int last = mWrapped.getCount() - 1;
for (int i = 0; i < mColumns; i++) {
View convertItem = null;
if (i < row.getChildCount()) {
convertItem = row.getChildAt(i);
}
final int pos = first + i;
final int validPos = Math.min(pos, last);
final View item = mWrapped.getView(validPos, convertItem, row);
item.setTag(validPos);
item.setOnClickListener(mItemListener);
item.setFocusable(true);
if (pos == validPos) {
item.setVisibility(View.VISIBLE);
} else {
item.setVisibility(View.INVISIBLE);
}
if (convertItem == null) {
row.addView(item, params);
}
}
return convertView;
}
@Override
public void registerDataSetObserver(DataSetObserver observer) {
super.registerDataSetObserver(observer);
mWrapped.registerDataSetObserver(observer);
}
@Override
public void unregisterDataSetObserver(DataSetObserver observer) {
super.unregisterDataSetObserver(observer);
mWrapped.unregisterDataSetObserver(observer);
}
}

View File

@@ -16,9 +16,9 @@
package com.android.documentsui;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.app.ListFragment;
import android.app.LoaderManager.LoaderCallbacks;
import android.content.Context;
import android.content.CursorLoader;
@@ -28,49 +28,94 @@ import android.net.Uri;
import android.os.Bundle;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.DocumentColumns;
import android.text.format.DateUtils;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.CursorAdapter;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
public class DirectoryFragment extends ListFragment {
import com.android.documentsui.DocumentsActivity.Document;
/**
* Display the documents inside a single directory.
*/
public class DirectoryFragment extends Fragment {
// TODO: show storage backend in item views when requested
// TODO: implement sorting dialog
// TODO: support multiple selection with actionmode
private ListView mListView;
private GridView mGridView;
private DocumentsAdapter mAdapter;
private LoaderCallbacks<Cursor> mCallbacks;
private int mFlags;
private static final String EXTRA_URI = "uri";
private static final String EXTRA_MODE = "display_mode";
private static final int MODE_LIST = 1;
private static final int MODE_GRID = 2;
private static final int LOADER_DOCUMENTS = 2;
public static void show(FragmentManager fm, Uri uri, CharSequence title) {
public static void show(FragmentManager fm, Uri uri, String displayName) {
final Bundle args = new Bundle();
args.putParcelable(EXTRA_URI, uri);
args.putInt(EXTRA_MODE, MODE_LIST);
final DirectoryFragment fragment = new DirectoryFragment();
fragment.setArguments(args);
final FragmentTransaction ft = fm.beginTransaction();
ft.replace(android.R.id.content, fragment);
ft.addToBackStack(title.toString());
ft.setBreadCrumbTitle(title);
ft.replace(R.id.directory, fragment);
ft.addToBackStack(displayName);
ft.setBreadCrumbTitle(displayName);
ft.commitAllowingStateLoss();
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final Context context = inflater.getContext();
final View view = inflater.inflate(R.layout.fragment_directory, container, false);
mListView = (ListView) view.findViewById(R.id.list);
mListView.setOnItemClickListener(mItemListener);
mGridView = (GridView) view.findViewById(R.id.grid);
mGridView.setOnItemClickListener(mItemListener);
mAdapter = new DocumentsAdapter(context);
setListAdapter(mAdapter);
updateMode();
// TODO: migrate flags query to loader
final Uri uri = getArguments().getParcelable(EXTRA_URI);
mFlags = getDocumentFlags(context, uri);
mCallbacks = new LoaderCallbacks<Cursor>() {
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
final Uri uri = args.getParcelable(EXTRA_URI);
return new CursorLoader(context, uri, null, null, null, null);
final Uri contentsUri = DocumentsContract.buildContentsUri(uri);
return new CursorLoader(context, contentsUri, null, null, null, null);
}
@Override
@@ -84,13 +129,17 @@ public class DirectoryFragment extends ListFragment {
}
};
return super.onCreateView(inflater, container, savedInstanceState);
return view;
}
@Override
public void onStart() {
super.onStart();
getLoaderManager().restartLoader(LOADER_DOCUMENTS, getArguments(), mCallbacks);
// TODO: clean up tracking of current directory
final Uri uri = getArguments().getParcelable(EXTRA_URI);
((DocumentsActivity) getActivity()).onDirectoryChanged(uri, mFlags);
}
@Override
@@ -100,26 +149,70 @@ public class DirectoryFragment extends ListFragment {
}
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
final Cursor cursor = (Cursor) mAdapter.getItem(position);
final String guid = getCursorString(cursor, DocumentColumns.GUID);
final String mimeType = getCursorString(cursor, DocumentColumns.MIME_TYPE);
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.directory, menu);
}
final Uri uri = getArguments().getParcelable(EXTRA_URI);
final Uri childUri = DocumentsContract.buildDocumentUri(uri.getAuthority(), guid);
@Override
public void onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
final int mode = getMode();
menu.findItem(R.id.menu_grid).setVisible(mode != MODE_GRID);
menu.findItem(R.id.menu_list).setVisible(mode != MODE_LIST);
}
if (DocumentsContract.MIME_TYPE_DIRECTORY.equals(mimeType)) {
// Nested directory picked, recurse using new fragment
final Uri childContentsUri = DocumentsContract.buildContentsUri(childUri);
final String displayName = cursor.getString(
cursor.getColumnIndex(DocumentColumns.DISPLAY_NAME));
DirectoryFragment.show(getFragmentManager(), childContentsUri, displayName);
@Override
public boolean onOptionsItemSelected(MenuItem item) {
final int id = item.getItemId();
if (id == R.id.menu_grid) {
getArguments().putInt(EXTRA_MODE, MODE_GRID);
updateMode();
return true;
} else if (id == R.id.menu_list) {
getArguments().putInt(EXTRA_MODE, MODE_LIST);
updateMode();
return true;
} else {
// Explicit file picked, return
((DocumentsActivity) getActivity()).onDocumentPicked(childUri);
return super.onOptionsItemSelected(item);
}
}
private void updateMode() {
final int mode = getMode();
mListView.setVisibility(mode == MODE_LIST ? View.VISIBLE : View.GONE);
mGridView.setVisibility(mode == MODE_GRID ? View.VISIBLE : View.GONE);
if (mode == MODE_GRID) {
mListView.setAdapter(null);
mGridView.setAdapter(mAdapter);
mGridView.setNumColumns(GridView.AUTO_FIT);
} else {
mGridView.setAdapter(null);
mListView.setAdapter(mAdapter);
}
}
private OnItemClickListener mItemListener = new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
final Cursor cursor = (Cursor) mAdapter.getItem(position);
final Uri uri = getArguments().getParcelable(EXTRA_URI);
final Document doc = Document.fromCursor(uri.getAuthority(), cursor);
((DocumentsActivity) getActivity()).onDocumentPicked(doc);
}
};
private boolean getSupportsCreate() {
return (mFlags & DocumentsContract.FLAG_SUPPORTS_CREATE) != 0;
}
private int getMode() {
return getArguments().getInt(EXTRA_MODE, MODE_LIST);
}
private class DocumentsAdapter extends CursorAdapter {
public DocumentsAdapter(Context context) {
super(context, null, false);
@@ -127,8 +220,15 @@ public class DirectoryFragment extends ListFragment {
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return LayoutInflater.from(context)
.inflate(com.android.internal.R.layout.preference, parent, false);
final LayoutInflater inflater = LayoutInflater.from(context);
final int mode = getMode();
if (mode == MODE_LIST) {
return inflater.inflate(R.layout.item_doc_list, parent, false);
} else if (mode == MODE_GRID) {
return inflater.inflate(R.layout.item_doc_grid, parent, false);
} else {
throw new IllegalStateException();
}
}
@Override
@@ -137,12 +237,10 @@ public class DirectoryFragment extends ListFragment {
final TextView summary = (TextView) view.findViewById(android.R.id.summary);
final ImageView icon = (ImageView) view.findViewById(android.R.id.icon);
icon.setMaxWidth(128);
icon.setMaxHeight(128);
final String guid = getCursorString(cursor, DocumentColumns.GUID);
final String displayName = getCursorString(cursor, DocumentColumns.DISPLAY_NAME);
final String mimeType = getCursorString(cursor, DocumentColumns.MIME_TYPE);
final long lastModified = getCursorLong(cursor, DocumentColumns.LAST_MODIFIED);
final int flags = getCursorInt(cursor, DocumentColumns.FLAGS);
if ((flags & DocumentsContract.FLAG_SUPPORTS_THUMBNAIL) != 0) {
@@ -150,19 +248,39 @@ public class DirectoryFragment extends ListFragment {
final Uri childUri = DocumentsContract.buildDocumentUri(uri.getAuthority(), guid);
icon.setImageURI(childUri);
} else {
icon.setImageURI(null);
icon.setImageDrawable(DocumentsActivity.resolveDocumentIcon(context, mimeType));
}
title.setText(displayName);
summary.setText(mimeType);
if (summary != null) {
summary.setText(DateUtils.getRelativeTimeSpanString(lastModified));
}
}
}
private static String getCursorString(Cursor cursor, String columnName) {
private static int getDocumentFlags(Context context, Uri uri) {
final Cursor cursor = context.getContentResolver().query(uri, new String[] {
DocumentColumns.FLAGS }, null, null, null);
try {
if (cursor.moveToFirst()) {
return getCursorInt(cursor, DocumentColumns.FLAGS);
} else {
return 0;
}
} finally {
cursor.close();
}
}
public static String getCursorString(Cursor cursor, String columnName) {
return cursor.getString(cursor.getColumnIndex(columnName));
}
private static int getCursorInt(Cursor cursor, String columnName) {
public static long getCursorLong(Cursor cursor, String columnName) {
return cursor.getLong(cursor.getColumnIndex(columnName));
}
public static int getCursorInt(Cursor cursor, String columnName) {
return cursor.getInt(cursor.getColumnIndex(columnName));
}
}

View File

@@ -16,49 +16,229 @@
package com.android.documentsui;
import static com.android.documentsui.DirectoryFragment.getCursorString;
import android.app.ActionBar;
import android.app.ActionBar.OnNavigationListener;
import android.app.Activity;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.app.ListFragment;
import android.app.FragmentManager.BackStackEntry;
import android.app.FragmentManager.OnBackStackChangedListener;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.content.pm.ResolveInfo;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.DocumentColumns;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import com.google.android.collect.Lists;
import java.util.ArrayList;
import java.util.List;
import android.widget.BaseAdapter;
import android.widget.TextView;
public class DocumentsActivity extends Activity {
private static final String TAG = "Documents";
// TODO: fragment to show recently opened documents
// TODO: pull actionbar icon from current backend
private static final int MODE_OPEN = 1;
private static final int MODE_CREATE = 2;
private int mMode;
private boolean mIgnoreNextNavigation;
private Uri mCurrentDir;
private boolean mCurrentSupportsCreate;
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
SourceFragment.show(getFragmentManager());
final String action = getIntent().getAction();
if (Intent.ACTION_OPEN_DOCUMENT.equals(action)) {
mMode = MODE_OPEN;
} else if (Intent.ACTION_CREATE_DOCUMENT.equals(action)) {
mMode = MODE_CREATE;
}
setResult(Activity.RESULT_CANCELED);
setContentView(R.layout.activity);
getFragmentManager().addOnBackStackChangedListener(mStackListener);
BackendFragment.show(getFragmentManager());
updateActionBar();
if (mMode == MODE_CREATE) {
final String mimeType = getIntent().getType();
final String title = getIntent().getStringExtra(Intent.EXTRA_TITLE);
SaveFragment.show(getFragmentManager(), mimeType, title);
}
}
public void onDocumentPicked(Uri uri) {
Log.d(TAG, "onDocumentPicked() " + uri);
public void updateActionBar() {
final FragmentManager fm = getFragmentManager();
final ActionBar actionBar = getActionBar();
if (fm.getBackStackEntryCount() > 0) {
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
actionBar.setDisplayShowHomeEnabled(true);
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setTitle(null);
actionBar.setListNavigationCallbacks(mStackAdapter, mNavigationListener);
actionBar.setSelectedNavigationItem(mStackAdapter.getCount() - 1);
mIgnoreNextNavigation = true;
} else {
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
actionBar.setDisplayShowHomeEnabled(false);
actionBar.setDisplayHomeAsUpEnabled(false);
if (mMode == MODE_OPEN) {
actionBar.setTitle(R.string.title_open);
} else if (mMode == MODE_CREATE) {
actionBar.setTitle(R.string.title_save);
}
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
getMenuInflater().inflate(R.menu.activity, menu);
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
final MenuItem createDir = menu.findItem(R.id.menu_create_dir);
createDir.setVisible(mMode == MODE_CREATE);
createDir.setEnabled(mCurrentSupportsCreate);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
final int id = item.getItemId();
if (id == android.R.id.home) {
getFragmentManager().popBackStack();
updateActionBar();
} else if (id == R.id.menu_create_dir) {
// TODO: show dialog to create directory
}
return super.onOptionsItemSelected(item);
}
private OnBackStackChangedListener mStackListener = new OnBackStackChangedListener() {
@Override
public void onBackStackChanged() {
updateActionBar();
}
};
private BaseAdapter mStackAdapter = new BaseAdapter() {
@Override
public int getCount() {
return getFragmentManager().getBackStackEntryCount();
}
@Override
public Object getItem(int position) {
return getFragmentManager().getBackStackEntryAt(position);
}
@Override
public long getItemId(int position) {
return getFragmentManager().getBackStackEntryAt(position).getId();
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(parent.getContext())
.inflate(android.R.layout.simple_dropdown_item_1line, parent, false);
}
final BackStackEntry entry = getFragmentManager().getBackStackEntryAt(position);
final TextView text1 = (TextView) convertView.findViewById(android.R.id.text1);
text1.setText(entry.getBreadCrumbTitle());
return convertView;
}
};
private OnNavigationListener mNavigationListener = new OnNavigationListener() {
@Override
public boolean onNavigationItemSelected(int itemPosition, long itemId) {
if (mIgnoreNextNavigation) {
mIgnoreNextNavigation = false;
return false;
}
getFragmentManager().popBackStack((int) itemId, 0);
return true;
}
};
public void onDirectoryChanged(Uri uri, int flags) {
mCurrentDir = uri;
mCurrentSupportsCreate = (flags & DocumentsContract.FLAG_SUPPORTS_CREATE) != 0;
if (mMode == MODE_CREATE) {
final FragmentManager fm = getFragmentManager();
SaveFragment.get(fm).setSaveEnabled(mCurrentSupportsCreate);
}
invalidateOptionsMenu();
}
public void onBackendPicked(ProviderInfo info) {
final Uri uri = DocumentsContract.buildDocumentUri(
info.authority, DocumentsContract.ROOT_GUID);
final CharSequence displayName = info.loadLabel(getPackageManager());
DirectoryFragment.show(getFragmentManager(), uri, displayName.toString());
}
public void onDocumentPicked(Document doc) {
final FragmentManager fm = getFragmentManager();
if (DocumentsContract.MIME_TYPE_DIRECTORY.equals(doc.mimeType)) {
// Nested directory picked, recurse using new fragment
DirectoryFragment.show(fm, doc.uri, doc.displayName);
} else if (mMode == MODE_OPEN) {
// Explicit file picked, return
onFinished(doc.uri);
} else if (mMode == MODE_CREATE) {
// Overwrite current filename
SaveFragment.get(fm).setDisplayName(doc.displayName);
}
}
public void onSaveRequested(String mimeType, String displayName) {
// TODO: create file, confirming before overwriting
onFinished(null);
}
private void onFinished(Uri uri) {
Log.d(TAG, "onFinished() " + uri);
final Intent intent = new Intent();
intent.setData(uri);
intent.addFlags(
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_PERSIST_GRANT_URI_PERMISSION);
if (Intent.ACTION_CREATE_DOCUMENT.equals(getIntent().getAction())) {
if (mMode == MODE_CREATE) {
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
@@ -66,50 +246,37 @@ public class DocumentsActivity extends Activity {
finish();
}
public static class SourceFragment extends ListFragment {
private ArrayList<ProviderInfo> mProviders = Lists.newArrayList();
private ArrayAdapter<ProviderInfo> mAdapter;
public static class Document {
public Uri uri;
public String mimeType;
public String displayName;
public static void show(FragmentManager fm) {
final SourceFragment fragment = new SourceFragment();
final FragmentTransaction ft = fm.beginTransaction();
ft.replace(android.R.id.content, fragment);
ft.setBreadCrumbTitle("TOP");
ft.commitAllowingStateLoss();
public static Document fromCursor(String authority, Cursor cursor) {
final Document doc = new Document();
final String guid = getCursorString(cursor, DocumentColumns.GUID);
doc.uri = DocumentsContract.buildDocumentUri(authority, guid);
doc.mimeType = getCursorString(cursor, DocumentColumns.MIME_TYPE);
doc.displayName = getCursorString(cursor, DocumentColumns.DISPLAY_NAME);
return doc;
}
}
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final Context context = inflater.getContext();
public static Drawable resolveDocumentIcon(Context context, String mimeType) {
// TODO: allow backends to provide custom MIME icons
if (DocumentsContract.MIME_TYPE_DIRECTORY.equals(mimeType)) {
return context.getResources().getDrawable(R.drawable.ic_dir);
} else {
final PackageManager pm = context.getPackageManager();
final Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setType(mimeType);
// Gather known storage providers
mProviders.clear();
final List<ProviderInfo> providers = context.getPackageManager()
.queryContentProviders(null, -1, PackageManager.GET_META_DATA);
for (ProviderInfo info : providers) {
if (info.metaData != null
&& info.metaData.containsKey(
DocumentsContract.META_DATA_DOCUMENT_PROVIDER)) {
mProviders.add(info);
}
final ResolveInfo info = pm.resolveActivity(
intent, PackageManager.MATCH_DEFAULT_ONLY);
if (info != null) {
return info.loadIcon(pm);
} else {
return null;
}
mAdapter = new ArrayAdapter<ProviderInfo>(
context, android.R.layout.simple_list_item_1, mProviders);
setListAdapter(mAdapter);
return super.onCreateView(inflater, container, savedInstanceState);
}
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
final ProviderInfo info = mAdapter.getItem(position);
final Uri uri = DocumentsContract.buildContentsUri(DocumentsContract.buildDocumentUri(
info.authority, DocumentsContract.ROOT_GUID));
final String displayName = info.name;
DirectoryFragment.show(getFragmentManager(), uri, displayName);
}
}
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.documentsui;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
/**
* Display document title editor and save button.
*/
public class SaveFragment extends Fragment {
public static final String TAG = "SaveFragment";
private EditText mDisplayName;
private Button mSave;
private static final String EXTRA_MIME_TYPE = "mime_type";
private static final String EXTRA_DISPLAY_NAME = "display_name";
public static void show(FragmentManager fm, String mimeType, String displayName) {
final Bundle args = new Bundle();
args.putString(EXTRA_MIME_TYPE, mimeType);
args.putString(EXTRA_DISPLAY_NAME, displayName);
final SaveFragment fragment = new SaveFragment();
fragment.setArguments(args);
final FragmentTransaction ft = fm.beginTransaction();
ft.replace(R.id.save, fragment, TAG);
ft.commitAllowingStateLoss();
}
public static SaveFragment get(FragmentManager fm) {
return (SaveFragment) fm.findFragmentByTag(TAG);
}
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final Context context = inflater.getContext();
final View view = inflater.inflate(R.layout.fragment_save, container, false);
final ImageView icon = (ImageView) view.findViewById(android.R.id.icon);
icon.setImageDrawable(DocumentsActivity.resolveDocumentIcon(
context, getArguments().getString(EXTRA_MIME_TYPE)));
mDisplayName = (EditText) view.findViewById(android.R.id.title);
mDisplayName.setText(getArguments().getString(EXTRA_DISPLAY_NAME));
mSave = (Button) view.findViewById(android.R.id.button1);
mSave.setOnClickListener(mSaveListener);
mSave.setEnabled(false);
return view;
}
private View.OnClickListener mSaveListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
final String mimeType = getArguments().getString(EXTRA_MIME_TYPE);
final String displayName = getArguments().getString(EXTRA_DISPLAY_NAME);
((DocumentsActivity) getActivity()).onSaveRequested(mimeType, displayName);
}
};
public void setDisplayName(String displayName) {
getArguments().putString(EXTRA_DISPLAY_NAME, displayName);
mDisplayName.setText(displayName);
}
public void setSaveEnabled(boolean enabled) {
mSave.setEnabled(enabled);
}
}