From 6efba22ce510352bb84910d6efc42fecafd31ed7 Mon Sep 17 00:00:00 2001 From: Jeff Sharkey Date: Fri, 27 Sep 2013 16:44:11 -0700 Subject: [PATCH] New roots UX, async, performance, docs. Yet another iteration from UX on how roots should be ordered. Since we no longer categorize by type, remove from public API. Updated asset drop with new dividers. Update public API docs to be explicit about required columns. Hide flags and columns that aren't required for third-party apps. Move remainder of potentially blocking work to AsyncTasks, including creating directories, picked root resolution, and creation of new documents once picked. Improve performance of layouts by removing baseline alignment and reduce hierarchy depth. Set alpha on ImageViews directly to avoid offscreen rendering hit. Limit returned recents to 45 days. Show load in recents when still waiting for backends. Show empty message when no recents stacks to create from. Use unique key when saving recent stacks. Bug: 10941423, 10819454, 10964412, 10960718 Change-Id: I08cf589dcda7e203acf67928f4d30322ae36ee94 --- api/current.txt | 11 +- .../android/provider/DocumentsContract.java | 131 ++++------ .../res/drawable-hdpi/ic_dir_shadow.9.png | Bin 244 -> 184 bytes .../ic_drawer_hairline_divider.9.png | Bin 0 -> 119 bytes .../ic_drawer_tall_divider.9.png | Bin 0 -> 150 bytes .../res/drawable-mdpi/ic_dir_shadow.9.png | Bin 244 -> 163 bytes .../ic_drawer_hairline_divider.9.png | Bin 0 -> 119 bytes .../ic_drawer_tall_divider.9.png | Bin 0 -> 132 bytes .../res/drawable-xhdpi/ic_dir_shadow.9.png | Bin 244 -> 193 bytes .../ic_drawer_hairline_divider.9.png | Bin 0 -> 119 bytes .../ic_drawer_tall_divider.9.png | Bin 0 -> 154 bytes .../res/drawable-xxhdpi/ic_dir_shadow.9.png | Bin 244 -> 233 bytes .../ic_drawer_hairline_divider.9.png | Bin 0 -> 119 bytes .../ic_drawer_tall_divider.9.png | Bin 0 -> 156 bytes .../res/layout-sw720dp-land/item_doc_list.xml | 15 +- .../res/layout-sw720dp/activity.xml | 5 +- packages/DocumentsUI/res/layout/activity.xml | 2 +- .../DocumentsUI/res/layout/fragment_roots.xml | 2 +- .../DocumentsUI/res/layout/fragment_save.xml | 3 +- .../DocumentsUI/res/layout/item_doc_grid.xml | 61 ++--- .../DocumentsUI/res/layout/item_doc_list.xml | 38 +-- .../res/layout/item_loading_grid.xml | 4 +- .../res/layout/item_loading_list.xml | 5 +- .../res/layout/item_message_list.xml | 9 +- packages/DocumentsUI/res/layout/item_root.xml | 3 +- .../res/layout/item_root_spacer.xml | 20 ++ .../DocumentsUI/res/layout/item_title.xml | 3 +- .../documentsui/CreateDirectoryFragment.java | 52 +++- .../documentsui/DirectoryFragment.java | 14 +- .../android/documentsui/DirectoryLoader.java | 6 +- .../documentsui/DocumentsActivity.java | 112 ++++++-- .../documentsui/FilteringCursorWrapper.java | 12 +- .../com/android/documentsui/RecentLoader.java | 43 ++- .../documentsui/RecentsCreateFragment.java | 10 + .../android/documentsui/RecentsProvider.java | 9 +- .../com/android/documentsui/RootsCache.java | 3 +- .../android/documentsui/RootsFragment.java | 246 ++++++++++-------- .../com/android/documentsui/TestActivity.java | 22 +- .../documentsui/model/DocumentStack.java | 19 ++ .../android/documentsui/model/RootInfo.java | 63 +++-- .../ExternalStorageProvider.java | 8 +- .../TestDocumentsProvider.java | 3 +- 42 files changed, 560 insertions(+), 374 deletions(-) create mode 100644 packages/DocumentsUI/res/drawable-hdpi/ic_drawer_hairline_divider.9.png create mode 100644 packages/DocumentsUI/res/drawable-hdpi/ic_drawer_tall_divider.9.png create mode 100644 packages/DocumentsUI/res/drawable-mdpi/ic_drawer_hairline_divider.9.png create mode 100644 packages/DocumentsUI/res/drawable-mdpi/ic_drawer_tall_divider.9.png create mode 100644 packages/DocumentsUI/res/drawable-xhdpi/ic_drawer_hairline_divider.9.png create mode 100644 packages/DocumentsUI/res/drawable-xhdpi/ic_drawer_tall_divider.9.png create mode 100644 packages/DocumentsUI/res/drawable-xxhdpi/ic_drawer_hairline_divider.9.png create mode 100644 packages/DocumentsUI/res/drawable-xxhdpi/ic_drawer_tall_divider.9.png create mode 100644 packages/DocumentsUI/res/layout/item_root_spacer.xml diff --git a/api/current.txt b/api/current.txt index fd9c6c21f89c1..1ae9ab76f61d4 100644 --- a/api/current.txt +++ b/api/current.txt @@ -20985,7 +20985,6 @@ package android.provider { field public static final java.lang.String COLUMN_MIME_TYPE = "mime_type"; field public static final java.lang.String COLUMN_SIZE = "_size"; field public static final java.lang.String COLUMN_SUMMARY = "summary"; - field public static final int FLAG_DIR_HIDE_GRID_TITLES = 64; // 0x40 field public static final int FLAG_DIR_PREFERS_GRID = 16; // 0x10 field public static final int FLAG_DIR_PREFERS_LAST_MODIFIED = 32; // 0x20 field public static final int FLAG_DIR_SUPPORTS_CREATE = 8; // 0x8 @@ -21002,18 +21001,12 @@ package android.provider { field public static final java.lang.String COLUMN_ICON = "icon"; field public static final java.lang.String COLUMN_MIME_TYPES = "mime_types"; field public static final java.lang.String COLUMN_ROOT_ID = "root_id"; - field public static final java.lang.String COLUMN_ROOT_TYPE = "root_type"; field public static final java.lang.String COLUMN_SUMMARY = "summary"; field public static final java.lang.String COLUMN_TITLE = "title"; - field public static final int FLAG_ADVANCED = 4; // 0x4 - field public static final int FLAG_EMPTY = 32; // 0x20 field public static final int FLAG_LOCAL_ONLY = 2; // 0x2 field public static final int FLAG_SUPPORTS_CREATE = 1; // 0x1 - field public static final int FLAG_SUPPORTS_RECENTS = 8; // 0x8 - field public static final int FLAG_SUPPORTS_SEARCH = 16; // 0x10 - field public static final int ROOT_TYPE_DEVICE = 3; // 0x3 - field public static final int ROOT_TYPE_SERVICE = 1; // 0x1 - field public static final int ROOT_TYPE_SHORTCUT = 2; // 0x2 + field public static final int FLAG_SUPPORTS_RECENTS = 4; // 0x4 + field public static final int FLAG_SUPPORTS_SEARCH = 8; // 0x8 } public abstract class DocumentsProvider extends android.content.ContentProvider { diff --git a/core/java/android/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java index 4c9af19ab1839..85ec80368c2a6 100644 --- a/core/java/android/provider/DocumentsContract.java +++ b/core/java/android/provider/DocumentsContract.java @@ -99,7 +99,7 @@ public final class DocumentsContract { /** * Unique ID of a document. This ID is both provided by and interpreted * by a {@link DocumentsProvider}, and should be treated as an opaque - * value by client applications. + * value by client applications. This column is required. *

* Each document must have a unique ID within a provider, but that * single document may be included as a child of multiple directories. @@ -117,7 +117,7 @@ public final class DocumentsContract { * Concrete MIME type of a document. For example, "image/png" or * "application/pdf" for openable files. A document can also be a * directory containing additional documents, which is represented with - * the {@link #MIME_TYPE_DIR} MIME type. + * the {@link #MIME_TYPE_DIR} MIME type. This column is required. *

* Type: STRING * @@ -127,15 +127,15 @@ public final class DocumentsContract { /** * Display name of a document, used as the primary title displayed to a - * user. + * user. This column is required. *

* Type: STRING */ public static final String COLUMN_DISPLAY_NAME = OpenableColumns.DISPLAY_NAME; /** - * Summary of a document, which may be shown to a user. The summary may - * be {@code null}. + * Summary of a document, which may be shown to a user. This column is + * optional, and may be {@code null}. *

* Type: STRING */ @@ -143,9 +143,9 @@ public final class DocumentsContract { /** * Timestamp when a document was last modified, in milliseconds since - * January 1, 1970 00:00:00.0 UTC, or {@code null} if unknown. A - * {@link DocumentsProvider} can update this field using events from - * {@link OnCloseListener} or other reliable + * January 1, 1970 00:00:00.0 UTC. This column is required, and may be + * {@code null} if unknown. A {@link DocumentsProvider} can update this + * field using events from {@link OnCloseListener} or other reliable * {@link ParcelFileDescriptor} transports. *

* Type: INTEGER (long) @@ -155,15 +155,16 @@ public final class DocumentsContract { public static final String COLUMN_LAST_MODIFIED = "last_modified"; /** - * Specific icon resource ID for a document, or {@code null} to use - * platform default icon based on {@link #COLUMN_MIME_TYPE}. + * Specific icon resource ID for a document. This column is optional, + * and may be {@code null} to use a platform-provided default icon based + * on {@link #COLUMN_MIME_TYPE}. *

* Type: INTEGER (int) */ public static final String COLUMN_ICON = "icon"; /** - * Flags that apply to a document. + * Flags that apply to a document. This column is required. *

* Type: INTEGER (int) * @@ -171,12 +172,13 @@ public final class DocumentsContract { * @see #FLAG_SUPPORTS_DELETE * @see #FLAG_SUPPORTS_THUMBNAIL * @see #FLAG_DIR_PREFERS_GRID - * @see #FLAG_DIR_SUPPORTS_CREATE + * @see #FLAG_DIR_PREFERS_LAST_MODIFIED */ public static final String COLUMN_FLAGS = "flags"; /** - * Size of a document, in bytes, or {@code null} if unknown. + * Size of a document, in bytes, or {@code null} if unknown. This column + * is required. *

* Type: INTEGER (long) */ @@ -211,7 +213,7 @@ public final class DocumentsContract { * writability of a document may change over time, for example due to * remote access changes. This flag indicates that a document client can * expect {@link ContentResolver#openOutputStream(Uri)} to succeed. - * + * * @see #COLUMN_FLAGS */ public static final int FLAG_SUPPORTS_WRITE = 1 << 1; @@ -265,8 +267,9 @@ public final class DocumentsContract { * * @see #COLUMN_FLAGS * @see #FLAG_DIR_PREFERS_GRID + * @hide */ - public static final int FLAG_DIR_HIDE_GRID_TITLES = 1 << 6; + public static final int FLAG_DIR_HIDE_GRID_TITLES = 1 << 16; } /** @@ -282,31 +285,17 @@ public final class DocumentsContract { /** * Unique ID of a root. This ID is both provided by and interpreted by a * {@link DocumentsProvider}, and should be treated as an opaque value - * by client applications. + * by client applications. This column is required. *

* Type: STRING */ public static final String COLUMN_ROOT_ID = "root_id"; /** - * Type of a root, used for clustering when presenting multiple roots to - * a user. + * Flags that apply to a root. This column is required. *

* Type: INTEGER (int) * - * @see #ROOT_TYPE_SERVICE - * @see #ROOT_TYPE_SHORTCUT - * @see #ROOT_TYPE_DEVICE - */ - public static final String COLUMN_ROOT_TYPE = "root_type"; - - /** - * Flags that apply to a root. - *

- * Type: INTEGER (int) - * - * @see #FLAG_ADVANCED - * @see #FLAG_EMPTY * @see #FLAG_LOCAL_ONLY * @see #FLAG_SUPPORTS_CREATE * @see #FLAG_SUPPORTS_RECENTS @@ -315,22 +304,23 @@ public final class DocumentsContract { public static final String COLUMN_FLAGS = "flags"; /** - * Icon resource ID for a root. + * Icon resource ID for a root. This column is required. *

* Type: INTEGER (int) */ public static final String COLUMN_ICON = "icon"; /** - * Title for a root, which will be shown to a user. + * Title for a root, which will be shown to a user. This column is + * required. *

* Type: STRING */ public static final String COLUMN_TITLE = "title"; /** - * Summary for this root, which may be shown to a user. The summary may - * be {@code null}. + * Summary for this root, which may be shown to a user. This column is + * optional, and may be {@code null}. *

* Type: STRING */ @@ -338,7 +328,7 @@ public final class DocumentsContract { /** * Document which is a directory that represents the top directory of - * this root. + * this root. This column is required. *

* Type: STRING * @@ -347,49 +337,26 @@ public final class DocumentsContract { public static final String COLUMN_DOCUMENT_ID = "document_id"; /** - * Number of bytes available in this root, or {@code null} if unknown or - * unbounded. + * Number of bytes available in this root. This column is optional, and + * may be {@code null} if unknown or unbounded. *

* Type: INTEGER (long) */ public static final String COLUMN_AVAILABLE_BYTES = "available_bytes"; /** - * MIME types supported by this root, or {@code null} if the root - * supports all MIME types. Multiple MIME types can be separated by a - * newline. For example, a root supporting audio might use - * "audio/*\napplication/x-flac". + * MIME types supported by this root. This column is optional, and if + * {@code null} the root is assumed to support all MIME types. Multiple + * MIME types can be separated by a newline. For example, a root + * supporting audio might return "audio/*\napplication/x-flac". *

- * Type: String + * Type: STRING */ public static final String COLUMN_MIME_TYPES = "mime_types"; /** {@hide} */ public static final String MIME_TYPE_ITEM = "vnd.android.document/root"; - /** - * Type of root that represents a storage service, such as a cloud-based - * service. - * - * @see #COLUMN_ROOT_TYPE - */ - public static final int ROOT_TYPE_SERVICE = 1; - - /** - * Type of root that represents a shortcut to content that may be - * available elsewhere through another storage root. - * - * @see #COLUMN_ROOT_TYPE - */ - public static final int ROOT_TYPE_SHORTCUT = 2; - - /** - * Type of root that represents a physical storage device. - * - * @see #COLUMN_ROOT_TYPE - */ - public static final int ROOT_TYPE_DEVICE = 3; - /** * Flag indicating that at least one directory under this root supports * creating content. Roots with this flag will be shown when an @@ -408,14 +375,6 @@ public final class DocumentsContract { */ public static final int FLAG_LOCAL_ONLY = 1 << 1; - /** - * Flag indicating that this root should only be visible to advanced - * users. - * - * @see #COLUMN_FLAGS - */ - public static final int FLAG_ADVANCED = 1 << 2; - /** * Flag indicating that this root can report recently modified * documents. @@ -423,7 +382,7 @@ public final class DocumentsContract { * @see #COLUMN_FLAGS * @see DocumentsContract#buildRecentDocumentsUri(String, String) */ - public static final int FLAG_SUPPORTS_RECENTS = 1 << 3; + public static final int FLAG_SUPPORTS_RECENTS = 1 << 2; /** * Flag indicating that this root supports search. @@ -432,19 +391,31 @@ public final class DocumentsContract { * @see DocumentsProvider#querySearchDocuments(String, String, * String[]) */ - public static final int FLAG_SUPPORTS_SEARCH = 1 << 4; + public static final int FLAG_SUPPORTS_SEARCH = 1 << 3; /** * Flag indicating that this root is currently empty. This may be used * to hide the root when opening documents, but the root will still be * shown when creating documents and {@link #FLAG_SUPPORTS_CREATE} is - * also set. + * also set. If the value of this flag changes, such as when a root + * becomes non-empty, you must send a content changed notification for + * {@link DocumentsContract#buildRootsUri(String)}. * * @see #COLUMN_FLAGS - * @see DocumentsProvider#querySearchDocuments(String, String, - * String[]) + * @see ContentResolver#notifyChange(Uri, + * android.database.ContentObserver, boolean) + * @hide */ - public static final int FLAG_EMPTY = 1 << 5; + public static final int FLAG_EMPTY = 1 << 16; + + /** + * Flag indicating that this root should only be visible to advanced + * users. + * + * @see #COLUMN_FLAGS + * @hide + */ + public static final int FLAG_ADVANCED = 1 << 17; } /** diff --git a/packages/DocumentsUI/res/drawable-hdpi/ic_dir_shadow.9.png b/packages/DocumentsUI/res/drawable-hdpi/ic_dir_shadow.9.png index 02408741c9cfb950a3790469dab97d4b1ef8a98c..904d5253672caaa4ebf688b7227f1ffc4886c4c9 100644 GIT binary patch delta 169 zcmeyuxP#HKGr-TCmrII^fq{Y7)59eQNDBb55eFNP44obw1Eg4rojgN0fS`sWrDLLl zM1GW~i(`mK=i4*hTnz?1Ef4LNavweZSO2DQ&+Gs%^F@#Hm=qOL_C2UvsULL3#^s*Y z1}5nOhV*l`)tPHcHI+izKCIS?Kg7l(VUTd3fsq-=_#xF;F`$ywY}@AJ1>9DVu; PXf1=MtDnm{r-UW|D=9a> literal 244 zcmeAS@N?(olHy`uVBq!ia0vp^B0%iK!3HG5| zgW!U_%O?XxI14-?iy0WWJ3*My{N(AiKtc8rPhVH|yPQJelH3jF=LQ0WN<3X0Ln02p zyXah{P01K7Pez54w(#huK*9W`Dp TE7Cs&w4cG#)z4*}Q$iB}j4nJ zaCd?*qxs3xYk`82o-U3d95a&>8XErd7`Qn*Hyd^_FarTYW-052b1%!60~IlNy85}S Ib4q9e06}~kLI3~& literal 0 HcmV?d00001 diff --git a/packages/DocumentsUI/res/drawable-hdpi/ic_drawer_tall_divider.9.png b/packages/DocumentsUI/res/drawable-hdpi/ic_drawer_tall_divider.9.png new file mode 100644 index 0000000000000000000000000000000000000000..403eddb4f90a6e0ab8a4603cc60c0a2926077eb5 GIT binary patch literal 150 zcmeAS@N?(olHy`uVBq!ia0vp^EI`c3!3HEn^LrP71d5$JLpXq-h9ji|Bw64QSPBvEalM!wAL2cyrJQ45_k};| zgW!U_%O?XxI14-?iy0WWJ3*My{N(AiKtc8rPhVH|yPQJelH3jF=LQ0WN<3X0Ln02p zyXah{P01K7Pez54w(#huK*9W`Dp TE7Cs&w4cG#)z4*}Q$iB}j4nJ zaCd?*qxs3xYk`82o-U3d95a&>8XErd7`Qn*Hyd^_FarTYW-052b1%!60~IlNy85}S Ib4q9e06}~kLI3~& literal 0 HcmV?d00001 diff --git a/packages/DocumentsUI/res/drawable-mdpi/ic_drawer_tall_divider.9.png b/packages/DocumentsUI/res/drawable-mdpi/ic_drawer_tall_divider.9.png new file mode 100644 index 0000000000000000000000000000000000000000..9a9cf5ede773a598acc8f90defc68d114a6bc2c3 GIT binary patch literal 132 zcmeAS@N?(olHy`uVBq!ia0vp^%s|Y>!3H9qs_+0QmSQK*5Dp-y;YjHK@;M7UB8wRq zxI00Z(fs7;wLn2NPZ!4!j+x0n{{OdUW@bKI&!+OAQAgqQfde1rbDsLiE$kY|FV4Vl YWdmpI1@^BOfI1jFUHx3vIVCg!01uWTzW@LL literal 0 HcmV?d00001 diff --git a/packages/DocumentsUI/res/drawable-xhdpi/ic_dir_shadow.9.png b/packages/DocumentsUI/res/drawable-xhdpi/ic_dir_shadow.9.png index 02408741c9cfb950a3790469dab97d4b1ef8a98c..e38a8685dcea8ab8d3100c44e3b805bc2b570443 100644 GIT binary patch delta 144 zcmeyuc#v^|3NK5slV=DA5Y%v_bWC)Rs7Us7aSVxQeS5}}tHFVX^GZx^prw85kJYlDyqr82*Fcg1yTpPc)RMXD{*eb!ETHDI_k* z-Ee+xAW&6_r;B4q#NoF$47nN%cpNT$?4{I;{TfXA!Dk@0k2O z*WCKL9Q*b)+0kAv(>E(FeG*z4boJvL!($iE)SSC{J<8g6cZOoiT0uc|`)eo8GcsWS jS-UrbP0l+XkKICo2I diff --git a/packages/DocumentsUI/res/drawable-xhdpi/ic_drawer_hairline_divider.9.png b/packages/DocumentsUI/res/drawable-xhdpi/ic_drawer_hairline_divider.9.png new file mode 100644 index 0000000000000000000000000000000000000000..0d75172a1eb11ac974d2273484caf60c66874be9 GIT binary patch literal 119 zcmeAS@N?(olHy`uVBq!ia0vp^%plCc1|-8Yw(bW~EX7WqAsj$Z!;#Vf4nJ zaCd?*qxs3xYk`82o-U3d95a&>8XErd7`Qn*Hyd^_FarTYW-052b1%!60~IlNy85}S Ib4q9e06}~kLI3~& literal 0 HcmV?d00001 diff --git a/packages/DocumentsUI/res/drawable-xhdpi/ic_drawer_tall_divider.9.png b/packages/DocumentsUI/res/drawable-xhdpi/ic_drawer_tall_divider.9.png new file mode 100644 index 0000000000000000000000000000000000000000..205c34bcbe6084128ef0f3e776a2b99b1b4d55b3 GIT binary patch literal 154 zcmeAS@N?(olHy`uVBq!ia0vp^EI`c7!3HEFmg}qr2^2edhHwBu4M$1`kk47*5m^jW z(+R?i<|j|D1q#}Dx;Tb#%$$2}BX5I&fb+$2jpj{e$p(#@I|5unHm2PCRg~zdkh^%* ugD+Z(Pv6aK%4WU)XhR0`nc)2f9~i!e^XNr%JNE$1VeoYIb6Mw<&;$Teb1zT; literal 0 HcmV?d00001 diff --git a/packages/DocumentsUI/res/drawable-xxhdpi/ic_dir_shadow.9.png b/packages/DocumentsUI/res/drawable-xxhdpi/ic_dir_shadow.9.png index 02408741c9cfb950a3790469dab97d4b1ef8a98c..0b332e410a77a020cd9cc239f86b0a2c468a908d 100644 GIT binary patch literal 233 zcmeAS@N?(olHy`uVBq!ia0vp^vOt`~!3HGPJij3gq$EpRBT9nv(@M${i&7aJQ}UBi z6+Ckj(^G>|6H_V+Po~-c6?J>MIEGZ*dUMN{%Slm$Ex>g3-}AQ@ac2ma>$})Ivea0l z_Pm*&wZ4yg!_=7_72mo$_b$6w9F!i%CzDqE)}s2>mpw1a{O7$)?zubNM=PrD>G8~` z{Ebcv12jaqTJci%z2a+B_Sshb{qiX4b?Vj<@dSns-{i_aJoDQRbQXiBtDnm{r-UW| D+=y2= literal 244 zcmeAS@N?(olHy`uVBq!ia0vp^B0%iK!3HG5| zgW!U_%O?XxI14-?iy0WWJ3*My{N(AiKtc8rPhVH|yPQJelH3jF=LQ0WN<3X0Ln02p zyXah{P01K7Pez54w(#huK*9W`Dp TE7Cs&w4cG#)z4*}Q$iB}jrHsZZ_;<=wRS5V8|?G-7rb3MHi@u N!PC{xWt~$(69ALhAHe_s literal 0 HcmV?d00001 diff --git a/packages/DocumentsUI/res/drawable-xxhdpi/ic_drawer_tall_divider.9.png b/packages/DocumentsUI/res/drawable-xxhdpi/ic_drawer_tall_divider.9.png new file mode 100644 index 0000000000000000000000000000000000000000..f47d50a834b8f3c4823b75382342bfb76c6cc74e GIT binary patch literal 156 zcmeAS@N?(olHy`uVBq!ia0vp^tU%1q!3HFCzOXg}DajJoh?3y^w370~qErUQl>DSr z1<%~X^wgl##FWaylc_d9MRuMpjv*Ddl79UEZ_h07$NdnG4v)Zu0}TxSXG&~fm~ra+ z4VL#pA)QyxG>WFM{1oMoc*xY#!((u+KBbj`k&WTF1;4qD=lTsmlNdZ*{an^LB{Ts5 DZOSo& literal 0 HcmV?d00001 diff --git a/packages/DocumentsUI/res/layout-sw720dp-land/item_doc_list.xml b/packages/DocumentsUI/res/layout-sw720dp-land/item_doc_list.xml index 3bea1663c1f4c..851061fe9b51f 100644 --- a/packages/DocumentsUI/res/layout-sw720dp-land/item_doc_list.xml +++ b/packages/DocumentsUI/res/layout-sw720dp-land/item_doc_list.xml @@ -21,17 +21,18 @@ android:minHeight="?android:attr/listPreferredItemHeight" android:paddingStart="?android:attr/listPreferredItemPaddingStart" android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" - android:paddingTop="8dip" - android:paddingBottom="8dip" - android:orientation="horizontal"> + android:paddingTop="8dp" + android:paddingBottom="8dp" + android:gravity="center_vertical" + android:orientation="horizontal" + android:baselineAligned="false"> + android:layout_marginEnd="20dp"> + + android:orientation="horizontal" + android:baselineAligned="false"> + android:divider="@drawable/ic_drawer_hairline_divider" /> diff --git a/packages/DocumentsUI/res/layout/fragment_save.xml b/packages/DocumentsUI/res/layout/fragment_save.xml index 570b517f30fd8..891f0a07129fd 100644 --- a/packages/DocumentsUI/res/layout/fragment_save.xml +++ b/packages/DocumentsUI/res/layout/fragment_save.xml @@ -29,6 +29,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" + android:baselineAligned="false" android:gravity="center_vertical" android:background="#ddd" android:minHeight="?android:attr/listPreferredItemHeightSmall"> @@ -44,7 +45,7 @@ - - - - - - - - + android:background="#fff" + android:foreground="@drawable/ic_grid_gradient_bg" + android:foregroundGravity="fill"> + + @@ -67,7 +55,9 @@ android:id="@+id/line1" android:layout_width="match_parent" android:layout_height="wrap_content" + android:gravity="center_vertical" android:orientation="horizontal" + android:baselineAligned="false" android:paddingStart="?android:attr/listPreferredItemPaddingStart" android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> @@ -85,7 +75,7 @@ android:id="@android:id/icon1" android:layout_width="@dimen/root_icon_size" android:layout_height="@dimen/root_icon_size" - android:layout_marginStart="8dip" + android:layout_marginStart="8dp" android:scaleType="centerInside" android:contentDescription="@null" /> @@ -95,16 +85,17 @@ android:id="@+id/line2" android:layout_width="match_parent" android:layout_height="wrap_content" + android:gravity="center_vertical" android:orientation="horizontal" + android:baselineAligned="false" android:paddingStart="?android:attr/listPreferredItemPaddingStart" android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> - - diff --git a/packages/DocumentsUI/res/layout/item_doc_list.xml b/packages/DocumentsUI/res/layout/item_doc_list.xml index 84fda9d36ad32..4c5b2e304f6ee 100644 --- a/packages/DocumentsUI/res/layout/item_doc_list.xml +++ b/packages/DocumentsUI/res/layout/item_doc_list.xml @@ -21,17 +21,18 @@ android:minHeight="?android:attr/listPreferredItemHeight" android:paddingStart="?android:attr/listPreferredItemPaddingStart" android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" - android:paddingTop="8dip" - android:paddingBottom="8dip" - android:orientation="horizontal"> + android:paddingTop="8dp" + android:paddingBottom="8dp" + android:gravity="center_vertical" + android:orientation="horizontal" + android:baselineAligned="false"> + android:layout_marginEnd="20dp"> + android:orientation="horizontal" + android:baselineAligned="false"> @@ -85,13 +86,15 @@ android:id="@+id/line2" android:layout_width="match_parent" android:layout_height="wrap_content" - android:orientation="horizontal"> + android:gravity="center_vertical" + android:orientation="horizontal" + android:baselineAligned="false"> + android:paddingTop="8dp" + android:paddingBottom="8dp"> + android:paddingTop="8dp" + android:paddingBottom="8dp" + android:orientation="horizontal" + android:baselineAligned="false"> diff --git a/packages/DocumentsUI/res/layout/item_root.xml b/packages/DocumentsUI/res/layout/item_root.xml index 98d78dac545f1..9b52d85ca45e9 100644 --- a/packages/DocumentsUI/res/layout/item_root.xml +++ b/packages/DocumentsUI/res/layout/item_root.xml @@ -22,13 +22,14 @@ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" android:gravity="center_vertical" android:orientation="horizontal" + android:baselineAligned="false" android:background="@drawable/item_root"> diff --git a/packages/DocumentsUI/res/layout/item_root_spacer.xml b/packages/DocumentsUI/res/layout/item_root_spacer.xml new file mode 100644 index 0000000000000..7d96ac8a5c946 --- /dev/null +++ b/packages/DocumentsUI/res/layout/item_root_spacer.xml @@ -0,0 +1,20 @@ + + + + diff --git a/packages/DocumentsUI/res/layout/item_title.xml b/packages/DocumentsUI/res/layout/item_title.xml index 7eb100a245b0c..58016f1fe962f 100644 --- a/packages/DocumentsUI/res/layout/item_title.xml +++ b/packages/DocumentsUI/res/layout/item_title.xml @@ -21,7 +21,8 @@ android:paddingStart="?android:attr/listPreferredItemPaddingStart" android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" android:gravity="center_vertical" - android:orientation="horizontal"> + android:orientation="horizontal" + android:baselineAligned="false"> { + private final String mDisplayName; + + public CreateDirectoryTask(String displayName) { + mDisplayName = displayName; + } + + @Override + protected DocumentInfo doInBackground(Void... params) { + final DocumentsActivity activity = (DocumentsActivity) getActivity(); + final ContentResolver resolver = activity.getContentResolver(); + + final DocumentInfo cwd = activity.getCurrentDirectory(); + final Uri childUri = DocumentsContract.createDocument( + resolver, cwd.derivedUri, Document.MIME_TYPE_DIR, mDisplayName); + try { + return DocumentInfo.fromUri(resolver, childUri); + } catch (FileNotFoundException e) { + return null; + } + } + + @Override + protected void onPostExecute(DocumentInfo result) { + final DocumentsActivity activity = (DocumentsActivity) getActivity(); + if (result != null) { + // Navigate into newly created child + activity.onDocumentPicked(result); + } else { + Toast.makeText(activity, R.string.create_error, Toast.LENGTH_SHORT).show(); + } + } + } } diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java index c46dfb2c009b0..1f11aed91358d 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java +++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java @@ -742,7 +742,6 @@ public class DirectoryFragment extends Fragment { final View line1 = convertView.findViewById(R.id.line1); final View line2 = convertView.findViewById(R.id.line2); - final View icon = convertView.findViewById(android.R.id.icon); final ImageView iconMime = (ImageView) convertView.findViewById(R.id.icon_mime); final ImageView iconThumb = (ImageView) convertView.findViewById(R.id.icon_thumb); final TextView title = (TextView) convertView.findViewById(android.R.id.title); @@ -786,10 +785,12 @@ public class DirectoryFragment extends Fragment { // loaded in background. if (cacheHit) { iconMime.setAlpha(0f); + iconMime.setImageDrawable(null); iconThumb.setAlpha(1f); } else { iconMime.setAlpha(1f); iconThumb.setAlpha(0f); + iconThumb.setImageDrawable(null); if (docIcon != 0) { iconMime.setImageDrawable( IconUtils.loadPackageIcon(context, docAuthority, docIcon)); @@ -895,12 +896,14 @@ public class DirectoryFragment extends Fragment { final boolean enabled = isDocumentEnabled(docMimeType, docFlags); if (enabled) { setEnabledRecursive(convertView, true); - icon.setAlpha(1f); + iconMime.setAlpha(1f); + iconThumb.setAlpha(1f); if (icon1 != null) icon1.setAlpha(1f); if (icon2 != null) icon2.setAlpha(1f); } else { setEnabledRecursive(convertView, false); - icon.setAlpha(0.5f); + iconMime.setAlpha(0.5f); + iconThumb.setAlpha(0.5f); if (icon1 != null) icon1.setAlpha(0.5f); if (icon2 != null) icon2.setAlpha(0.5f); } @@ -991,10 +994,11 @@ public class DirectoryFragment extends Fragment { mIconThumb.setTag(null); mIconThumb.setImageBitmap(result); - mIconMime.setAlpha(1f); + final float targetAlpha = mIconMime.isEnabled() ? 1f : 0.5f; + mIconMime.setAlpha(targetAlpha); mIconMime.animate().alpha(0f).start(); mIconThumb.setAlpha(0f); - mIconThumb.animate().alpha(1f).start(); + mIconThumb.animate().alpha(targetAlpha).start(); } } } diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java index 8627ecfaa2f7e..0b3ecf84900e9 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java +++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java @@ -63,6 +63,9 @@ class DirectoryResult implements AutoCloseable { } public class DirectoryLoader extends AsyncTaskLoader { + + private static final String[] SEARCH_REJECT_MIMES = new String[] { Document.MIME_TYPE_DIR }; + private final ForceLoadContentObserver mObserver = new ForceLoadContentObserver(); private final int mType; @@ -164,8 +167,7 @@ public class DirectoryLoader extends AsyncTaskLoader { if (mType == DirectoryFragment.TYPE_SEARCH) { // Filter directories out of search results, for now - cursor = new FilteringCursorWrapper(cursor, null, new String[] { - Document.MIME_TYPE_DIR }); + cursor = new FilteringCursorWrapper(cursor, null, SEARCH_REJECT_MIMES); } else { // Normal directories should have sorting applied cursor = new SortingCursorWrapper(cursor, result.sortOrder); diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java index 72fdc57f9caba..4caec8fb0ad23 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java +++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java @@ -854,14 +854,7 @@ public class DocumentsActivity extends Activity { mState.stackTouched = true; if (!mRoots.isRecentsRoot(root)) { - try { - final Uri uri = DocumentsContract.buildDocumentUri(root.authority, root.documentId); - final DocumentInfo doc = DocumentInfo.fromUri(getContentResolver(), uri); - mState.stack.push(doc); - mState.stackTouched = true; - onCurrentDirectoryChanged(ANIM_SIDE); - } catch (FileNotFoundException e) { - } + new PickRootTask(root).execute(); } else { onCurrentDirectoryChanged(ANIM_SIDE); } @@ -871,6 +864,34 @@ public class DocumentsActivity extends Activity { } } + private class PickRootTask extends AsyncTask { + private RootInfo mRoot; + + public PickRootTask(RootInfo root) { + mRoot = root; + } + + @Override + protected DocumentInfo doInBackground(Void... params) { + try { + final Uri uri = DocumentsContract.buildDocumentUri( + mRoot.authority, mRoot.documentId); + return DocumentInfo.fromUri(getContentResolver(), uri); + } catch (FileNotFoundException e) { + return null; + } + } + + @Override + protected void onPostExecute(DocumentInfo result) { + if (result != null) { + mState.stack.push(result); + mState.stackTouched = true; + onCurrentDirectoryChanged(ANIM_SIDE); + } + } + } + public void onAppPicked(ResolveInfo info) { final Intent intent = new Intent(getIntent()); intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_FORWARD_RESULT); @@ -909,7 +930,7 @@ public class DocumentsActivity extends Activity { onCurrentDirectoryChanged(ANIM_DOWN); } else if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) { // Explicit file picked, return - onFinished(doc.derivedUri); + new ExistingFinishTask(doc.derivedUri).execute(); } else if (mState.action == ACTION_CREATE) { // Replace selected file SaveFragment.get(fm).setReplaceTarget(doc); @@ -943,29 +964,19 @@ public class DocumentsActivity extends Activity { for (int i = 0; i < size; i++) { uris[i] = docs.get(i).derivedUri; } - onFinished(uris); + new ExistingFinishTask(uris).execute(); } } public void onSaveRequested(DocumentInfo replaceTarget) { - onFinished(replaceTarget.derivedUri); + new ExistingFinishTask(replaceTarget.derivedUri).execute(); } public void onSaveRequested(String mimeType, String displayName) { - final DocumentInfo cwd = getCurrentDirectory(); - - final Uri childUri = DocumentsContract.createDocument( - getContentResolver(), cwd.derivedUri, mimeType, displayName); - if (childUri != null) { - onFinished(childUri); - } else { - Toast.makeText(this, R.string.save_error, Toast.LENGTH_SHORT).show(); - } + new CreateFinishTask(mimeType, displayName).execute(); } - private void onFinished(Uri... uris) { - Log.d(TAG, "onFinished() " + Arrays.toString(uris)); - + private void saveStackBlocking() { final ContentResolver resolver = getContentResolver(); final ContentValues values = new ContentValues(); @@ -973,6 +984,7 @@ public class DocumentsActivity extends Activity { if (mState.action == ACTION_CREATE) { // Remember stack for last create values.clear(); + values.put(RecentColumns.KEY, mState.stack.buildKey()); values.put(RecentColumns.STACK, rawStack); resolver.insert(RecentsProvider.buildRecent(), values); } @@ -983,6 +995,10 @@ public class DocumentsActivity extends Activity { values.put(ResumeColumns.STACK, rawStack); values.put(ResumeColumns.EXTERNAL, 0); resolver.insert(RecentsProvider.buildResume(packageName), values); + } + + private void onFinished(Uri... uris) { + Log.d(TAG, "onFinished() " + Arrays.toString(uris)); final Intent intent = new Intent(); if (uris.length == 1) { @@ -1008,6 +1024,56 @@ public class DocumentsActivity extends Activity { finish(); } + private class CreateFinishTask extends AsyncTask { + private final String mMimeType; + private final String mDisplayName; + + public CreateFinishTask(String mimeType, String displayName) { + mMimeType = mimeType; + mDisplayName = displayName; + } + + @Override + protected Uri doInBackground(Void... params) { + final DocumentInfo cwd = getCurrentDirectory(); + final Uri childUri = DocumentsContract.createDocument( + getContentResolver(), cwd.derivedUri, mMimeType, mDisplayName); + if (childUri != null) { + saveStackBlocking(); + } + return childUri; + } + + @Override + protected void onPostExecute(Uri result) { + if (result != null) { + onFinished(result); + } else { + Toast.makeText(DocumentsActivity.this, R.string.save_error, Toast.LENGTH_SHORT) + .show(); + } + } + } + + private class ExistingFinishTask extends AsyncTask { + private final Uri[] mUris; + + public ExistingFinishTask(Uri... uris) { + mUris = uris; + } + + @Override + protected Void doInBackground(Void... params) { + saveStackBlocking(); + return null; + } + + @Override + protected void onPostExecute(Void result) { + onFinished(mUris); + } + } + public static class State implements android.os.Parcelable { public int action; public String[] acceptMimes; diff --git a/packages/DocumentsUI/src/com/android/documentsui/FilteringCursorWrapper.java b/packages/DocumentsUI/src/com/android/documentsui/FilteringCursorWrapper.java index 5f56963ae463b..52d816f1f968e 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/FilteringCursorWrapper.java +++ b/packages/DocumentsUI/src/com/android/documentsui/FilteringCursorWrapper.java @@ -34,10 +34,15 @@ public class FilteringCursorWrapper extends AbstractCursor { private int mCount; public FilteringCursorWrapper(Cursor cursor, String[] acceptMimes) { - this(cursor, acceptMimes, null); + this(cursor, acceptMimes, null, Long.MIN_VALUE); } public FilteringCursorWrapper(Cursor cursor, String[] acceptMimes, String[] rejectMimes) { + this(cursor, acceptMimes, rejectMimes, Long.MIN_VALUE); + } + + public FilteringCursorWrapper( + Cursor cursor, String[] acceptMimes, String[] rejectMimes, long rejectBefore) { mCursor = cursor; final int count = cursor.getCount(); @@ -47,9 +52,14 @@ public class FilteringCursorWrapper extends AbstractCursor { while (cursor.moveToNext()) { final String mimeType = cursor.getString( cursor.getColumnIndex(Document.COLUMN_MIME_TYPE)); + final long lastModified = cursor.getLong( + cursor.getColumnIndex(Document.COLUMN_LAST_MODIFIED)); if (rejectMimes != null && MimePredicate.mimeMatches(rejectMimes, mimeType)) { continue; } + if (lastModified < rejectBefore) { + continue; + } if (MimePredicate.mimeMatches(acceptMimes, mimeType)) { mPosition[mCount++] = cursor.getPosition(); } diff --git a/packages/DocumentsUI/src/com/android/documentsui/RecentLoader.java b/packages/DocumentsUI/src/com/android/documentsui/RecentLoader.java index e390456773225..9a4fb7da725fd 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/RecentLoader.java +++ b/packages/DocumentsUI/src/com/android/documentsui/RecentLoader.java @@ -26,9 +26,11 @@ import android.content.Context; import android.database.Cursor; import android.database.MergeCursor; import android.net.Uri; +import android.os.Bundle; import android.provider.DocumentsContract; import android.provider.DocumentsContract.Document; import android.provider.DocumentsContract.Root; +import android.text.format.DateUtils; import android.util.Log; import com.android.documentsui.DocumentsActivity.State; @@ -54,17 +56,23 @@ import java.util.concurrent.TimeUnit; public class RecentLoader extends AsyncTaskLoader { private static final boolean LOGD = true; - public static final int MAX_OUTSTANDING_RECENTS = 2; + // TODO: adjust for svelte devices + // TODO: add support for oneway queries to avoid wedging loader + private static final int MAX_OUTSTANDING_RECENTS = 2; /** * Time to wait for first pass to complete before returning partial results. */ - public static final int MAX_FIRST_PASS_WAIT_MILLIS = 500; + private static final int MAX_FIRST_PASS_WAIT_MILLIS = 500; - /** - * Maximum documents from a single root. - */ - public static final int MAX_DOCS_FROM_ROOT = 64; + /** Maximum documents from a single root. */ + private static final int MAX_DOCS_FROM_ROOT = 64; + + /** Ignore documents older than this age. */ + private static final long REJECT_OLDER_THAN = 45 * DateUtils.DAY_IN_MILLIS; + + /** MIME types that should always be excluded from recents. */ + private static final String[] RECENT_REJECT_MIMES = new String[] { Document.MIME_TYPE_DIR }; private static final ExecutorService sExecutor = buildExecutor(); @@ -173,6 +181,8 @@ public class RecentLoader extends AsyncTaskLoader { } } + final long rejectBefore = System.currentTimeMillis() - REJECT_OLDER_THAN; + // Collect all finished tasks List cursors = Lists.newArrayList(); for (RecentTask task : mTasks.values()) { @@ -180,7 +190,7 @@ public class RecentLoader extends AsyncTaskLoader { try { final Cursor cursor = task.get(); final FilteringCursorWrapper filtered = new FilteringCursorWrapper( - cursor, mState.acceptMimes, new String[] { Document.MIME_TYPE_DIR }) { + cursor, mState.acceptMimes, RECENT_REJECT_MIMES, rejectBefore) { @Override public void close() { // Ignored, since we manage cursor lifecycle internally @@ -203,11 +213,22 @@ public class RecentLoader extends AsyncTaskLoader { final DirectoryResult result = new DirectoryResult(); result.sortOrder = SORT_ORDER_LAST_MODIFIED; - if (cursors.size() > 0) { - final MergeCursor merged = new MergeCursor(cursors.toArray(new Cursor[cursors.size()])); - final SortingCursorWrapper sorted = new SortingCursorWrapper(merged, result.sortOrder); - result.cursor = sorted; + // Hint to UI if we're still loading + final Bundle extras = new Bundle(); + if (cursors.size() != mTasks.size()) { + extras.putBoolean(DocumentsContract.EXTRA_LOADING, true); } + + final MergeCursor merged = new MergeCursor(cursors.toArray(new Cursor[cursors.size()])); + final SortingCursorWrapper sorted = new SortingCursorWrapper(merged, result.sortOrder) { + @Override + public Bundle getExtras() { + return extras; + } + }; + + result.cursor = sorted; + return result; } diff --git a/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java b/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java index c975382b8bae3..39541731eda50 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java +++ b/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java @@ -66,6 +66,7 @@ import java.util.List; */ public class RecentsCreateFragment extends Fragment { + private View mEmptyView; private ListView mListView; private DocumentStackAdapter mAdapter; @@ -87,6 +88,8 @@ public class RecentsCreateFragment extends Fragment { final View view = inflater.inflate(R.layout.fragment_directory, container, false); + mEmptyView = view.findViewById(android.R.id.empty); + mListView = (ListView) view.findViewById(R.id.list); mListView.setOnItemClickListener(mItemListener); @@ -189,6 +192,13 @@ public class RecentsCreateFragment extends Fragment { public void swapStacks(List stacks) { mStacks = stacks; + + if (isEmpty()) { + mEmptyView.setVisibility(View.VISIBLE); + } else { + mEmptyView.setVisibility(View.GONE); + } + notifyDataSetChanged(); } diff --git a/packages/DocumentsUI/src/com/android/documentsui/RecentsProvider.java b/packages/DocumentsUI/src/com/android/documentsui/RecentsProvider.java index 7386cae9b84e7..4313fa71c4904 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/RecentsProvider.java +++ b/packages/DocumentsUI/src/com/android/documentsui/RecentsProvider.java @@ -33,7 +33,7 @@ import android.util.Log; public class RecentsProvider extends ContentProvider { private static final String TAG = "RecentsProvider"; - public static final long MAX_HISTORY_IN_MILLIS = DateUtils.DAY_IN_MILLIS * 45; + public static final long MAX_HISTORY_IN_MILLIS = 45 * DateUtils.DAY_IN_MILLIS; private static final String AUTHORITY = "com.android.documentsui.recents"; @@ -56,6 +56,7 @@ public class RecentsProvider extends ContentProvider { public static final String TABLE_RESUME = "resume"; public static class RecentColumns { + public static final String KEY = "key"; public static final String STACK = "stack"; public static final String TIMESTAMP = "timestamp"; } @@ -99,16 +100,18 @@ public class RecentsProvider extends ContentProvider { private static final int VERSION_INIT = 1; private static final int VERSION_AS_BLOB = 3; private static final int VERSION_ADD_EXTERNAL = 4; + private static final int VERSION_ADD_RECENT_KEY = 5; public DatabaseHelper(Context context) { - super(context, DB_NAME, null, VERSION_ADD_EXTERNAL); + super(context, DB_NAME, null, VERSION_ADD_RECENT_KEY); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL("CREATE TABLE " + TABLE_RECENT + " (" + - RecentColumns.STACK + " BLOB PRIMARY KEY ON CONFLICT REPLACE," + + RecentColumns.KEY + " TEXT PRIMARY KEY ON CONFLICT REPLACE," + + RecentColumns.STACK + " BLOB DEFAULT NULL," + RecentColumns.TIMESTAMP + " INTEGER" + ")"); diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java b/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java index 15af8aab45a7c..e3908e9ad5150 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java +++ b/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java @@ -99,7 +99,8 @@ public class RootsCache { */ public void updateAsync() { // Special root for recents - mRecentsRoot.rootType = Root.ROOT_TYPE_SHORTCUT; + mRecentsRoot.authority = null; + mRecentsRoot.rootId = null; mRecentsRoot.icon = R.drawable.ic_root_recent; mRecentsRoot.flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_CREATE; mRecentsRoot.title = mContext.getString(R.string.root_recent); diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java b/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java index d6026226e499a..2fb12bb29233c 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java +++ b/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java @@ -26,7 +26,6 @@ import android.content.Loader; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.Bundle; -import android.provider.DocumentsContract.Root; import android.text.TextUtils; import android.text.format.Formatter; import android.view.LayoutInflater; @@ -37,16 +36,16 @@ import android.widget.AdapterView.OnItemClickListener; import android.widget.ArrayAdapter; import android.widget.ImageView; import android.widget.ListView; -import android.widget.Space; import android.widget.TextView; import com.android.documentsui.DocumentsActivity.State; -import com.android.documentsui.SectionedListAdapter.SectionAdapter; import com.android.documentsui.model.DocumentInfo; import com.android.documentsui.model.RootInfo; import com.android.internal.util.Objects; +import com.google.common.collect.Lists; import java.util.Collection; +import java.util.Collections; import java.util.Comparator; import java.util.List; @@ -56,7 +55,7 @@ import java.util.List; public class RootsFragment extends Fragment { private ListView mList; - private SectionedRootsAdapter mAdapter; + private RootsAdapter mAdapter; private LoaderCallbacks> mCallbacks; @@ -112,7 +111,7 @@ public class RootsFragment extends Fragment { final Intent includeApps = getArguments().getParcelable(EXTRA_INCLUDE_APPS); - mAdapter = new SectionedRootsAdapter(context, result, includeApps); + mAdapter = new RootsAdapter(context, result, includeApps); mList.setAdapter(mAdapter); onCurrentRootChanged(); @@ -154,136 +153,148 @@ public class RootsFragment extends Fragment { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { final DocumentsActivity activity = DocumentsActivity.get(RootsFragment.this); - final Object item = mAdapter.getItem(position); - if (item instanceof RootInfo) { - activity.onRootPicked((RootInfo) item, true); - } else if (item instanceof ResolveInfo) { - activity.onAppPicked((ResolveInfo) item); + final Item item = mAdapter.getItem(position); + if (item instanceof RootItem) { + activity.onRootPicked(((RootItem) item).root, true); + } else if (item instanceof AppItem) { + activity.onAppPicked(((AppItem) item).info); } else { throw new IllegalStateException("Unknown root: " + item); } } }; - private static class RootsAdapter extends ArrayAdapter implements SectionAdapter { - public RootsAdapter(Context context) { - super(context, 0); + private static abstract class Item { + private final int mLayoutId; + + public Item(int layoutId) { + mLayoutId = layoutId; + } + + public View getView(View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = LayoutInflater.from(parent.getContext()) + .inflate(mLayoutId, parent, false); + } + bindView(convertView); + return convertView; + } + + public abstract void bindView(View convertView); + } + + private static class RootItem extends Item { + public final RootInfo root; + + public RootItem(RootInfo root) { + super(R.layout.item_root); + this.root = root; } @Override - public View getView(int position, View convertView, ViewGroup parent) { - final Context context = parent.getContext(); - if (convertView == null) { - convertView = LayoutInflater.from(context) - .inflate(R.layout.item_root, parent, false); - } - + public void bindView(View convertView) { final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon); final TextView title = (TextView) convertView.findViewById(android.R.id.title); final TextView summary = (TextView) convertView.findViewById(android.R.id.summary); - final RootInfo root = getItem(position); + final Context context = convertView.getContext(); icon.setImageDrawable(root.loadIcon(context)); title.setText(root.title); - // Device summary is always available space - final String summaryText; - if (root.rootType == Root.ROOT_TYPE_DEVICE && root.availableBytes >= 0) { + // Show available space if no summary + String summaryText = root.summary; + if (TextUtils.isEmpty(summaryText) && root.availableBytes >= 0) { summaryText = context.getString(R.string.root_available_bytes, Formatter.formatFileSize(context, root.availableBytes)); - } else { - summaryText = root.summary; } summary.setText(summaryText); summary.setVisibility(TextUtils.isEmpty(summaryText) ? View.GONE : View.VISIBLE); - - return convertView; - } - - @Override - public View getHeaderView(View convertView, ViewGroup parent) { - if (convertView == null) { - convertView = new Space(parent.getContext()); - } - return convertView; } } - private static class AppsAdapter extends ArrayAdapter implements SectionAdapter { - public AppsAdapter(Context context) { - super(context, 0); + private static class SpacerItem extends Item { + public SpacerItem() { + super(R.layout.item_root_spacer); } @Override - public View getView(int position, View convertView, ViewGroup parent) { - final Context context = parent.getContext(); - final PackageManager pm = context.getPackageManager(); - if (convertView == null) { - convertView = LayoutInflater.from(context) - .inflate(R.layout.item_root, parent, false); - } + public void bindView(View convertView) { + // Nothing to bind + } + } + private static class AppItem extends Item { + public final ResolveInfo info; + + public AppItem(ResolveInfo info) { + super(R.layout.item_root); + this.info = info; + } + + @Override + public void bindView(View convertView) { final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon); final TextView title = (TextView) convertView.findViewById(android.R.id.title); final TextView summary = (TextView) convertView.findViewById(android.R.id.summary); - final ResolveInfo info = getItem(position); + final PackageManager pm = convertView.getContext().getPackageManager(); icon.setImageDrawable(info.loadIcon(pm)); title.setText(info.loadLabel(pm)); // TODO: match existing summary behavior from disambig dialog summary.setVisibility(View.GONE); - - return convertView; - } - - @Override - public View getHeaderView(View convertView, ViewGroup parent) { - if (convertView == null) { - convertView = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_root_header, parent, false); - } - - final TextView title = (TextView) convertView.findViewById(android.R.id.title); - title.setText(R.string.root_type_apps); - - return convertView; } } - private static class SectionedRootsAdapter extends SectionedListAdapter { - private final RootsAdapter mRecent; - private final RootsAdapter mServices; - private final RootsAdapter mShortcuts; - private final RootsAdapter mDevices; - private final AppsAdapter mApps; + private static class RootsAdapter extends ArrayAdapter { + public RootsAdapter(Context context, Collection roots, Intent includeApps) { + super(context, 0); - public SectionedRootsAdapter( - Context context, Collection roots, Intent includeApps) { - mRecent = new RootsAdapter(context); - mServices = new RootsAdapter(context); - mShortcuts = new RootsAdapter(context); - mDevices = new RootsAdapter(context); - mApps = new AppsAdapter(context); + RootItem recents = null; + RootItem images = null; + RootItem videos = null; + RootItem audio = null; + RootItem downloads = null; + + final List clouds = Lists.newArrayList(); + final List locals = Lists.newArrayList(); for (RootInfo root : roots) { - if (root.authority == null) { - mRecent.add(root); - continue; + if (root.isRecents()) { + recents = new RootItem(root); + } else if (root.isExternalStorage()) { + locals.add(root); + } else if (root.isDownloads()) { + downloads = new RootItem(root); + } else if (root.isImages()) { + images = new RootItem(root); + } else if (root.isVideos()) { + videos = new RootItem(root); + } else if (root.isAudio()) { + audio = new RootItem(root); + } else { + clouds.add(root); } + } - switch (root.rootType) { - case Root.ROOT_TYPE_SERVICE: - mServices.add(root); - break; - case Root.ROOT_TYPE_SHORTCUT: - mShortcuts.add(root); - break; - case Root.ROOT_TYPE_DEVICE: - mDevices.add(root); - break; - } + final RootComparator comp = new RootComparator(); + Collections.sort(clouds, comp); + Collections.sort(locals, comp); + + if (recents != null) add(recents); + + for (RootInfo cloud : clouds) { + add(new RootItem(cloud)); + } + + if (images != null) add(images); + if (videos != null) add(videos); + if (audio != null) add(audio); + if (downloads != null) add(downloads); + + for (RootInfo local : locals) { + add(new RootItem(local)); } if (includeApps != null) { @@ -291,35 +302,54 @@ public class RootsFragment extends Fragment { final List infos = pm.queryIntentActivities( includeApps, PackageManager.MATCH_DEFAULT_ONLY); + final List apps = Lists.newArrayList(); + // Omit ourselves from the list for (ResolveInfo info : infos) { if (!context.getPackageName().equals(info.activityInfo.packageName)) { - mApps.add(info); + apps.add(new AppItem(info)); + } + } + + if (apps.size() > 0) { + add(new SpacerItem()); + for (Item item : apps) { + add(item); } } } + } - final RootComparator comp = new RootComparator(); - mServices.sort(comp); - mShortcuts.sort(comp); - mDevices.sort(comp); + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final Item item = getItem(position); + return item.getView(convertView, parent); + } - if (mRecent.getCount() > 0) { - addSection(mRecent); - } - if (mServices.getCount() > 0) { - addSection(mServices); - } - if (mShortcuts.getCount() > 0) { - addSection(mShortcuts); - } - if (mDevices.getCount() > 0) { - addSection(mDevices); - } - if (mApps.getCount() > 0) { - addSection(mApps); + @Override + public boolean areAllItemsEnabled() { + return false; + } + + @Override + public boolean isEnabled(int position) { + return getItemViewType(position) != 1; + } + + @Override + public int getItemViewType(int position) { + final Item item = getItem(position); + if (item instanceof RootItem || item instanceof AppItem) { + return 0; + } else { + return 1; } } + + @Override + public int getViewTypeCount() { + return 2; + } } public static class RootComparator implements Comparator { diff --git a/packages/DocumentsUI/src/com/android/documentsui/TestActivity.java b/packages/DocumentsUI/src/com/android/documentsui/TestActivity.java index 57fc7e4eb9e59..1a473080fbb3c 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/TestActivity.java +++ b/packages/DocumentsUI/src/com/android/documentsui/TestActivity.java @@ -213,8 +213,13 @@ public class TestActivity extends Activity { if (DocumentsContract.isDocumentUri(this, uri)) { result += "; DOC_ID"; } - getContentResolver() - .takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); + try { + getContentResolver().takePersistableUriPermission( + uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); + } catch (SecurityException e) { + result += "; FAILED TO TAKE"; + Log.e(TAG, "Failed to take", e); + } InputStream is = null; try { is = getContentResolver().openInputStream(uri); @@ -222,7 +227,7 @@ public class TestActivity extends Activity { result += "; read length=" + length; } catch (Exception e) { result += "; ERROR"; - Log.w(TAG, "Failed to read " + uri, e); + Log.e(TAG, "Failed to read " + uri, e); } finally { IoUtils.closeQuietly(is); } @@ -235,15 +240,20 @@ public class TestActivity extends Activity { if (DocumentsContract.isDocumentUri(this, uri)) { result += "; DOC_ID"; } - getContentResolver() - .takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + try { + getContentResolver().takePersistableUriPermission( + uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + } catch (SecurityException e) { + result += "; FAILED TO TAKE"; + Log.e(TAG, "Failed to take", e); + } OutputStream os = null; try { os = getContentResolver().openOutputStream(uri); os.write("THE COMPLETE WORKS OF SHAKESPEARE".getBytes()); } catch (Exception e) { result += "; ERROR"; - Log.w(TAG, "Failed to write " + uri, e); + Log.e(TAG, "Failed to write " + uri, e); } finally { IoUtils.closeQuietly(os); } diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/DocumentStack.java b/packages/DocumentsUI/src/com/android/documentsui/model/DocumentStack.java index 0a378c07532a7..28bab6cf4966e 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/model/DocumentStack.java +++ b/packages/DocumentsUI/src/com/android/documentsui/model/DocumentStack.java @@ -71,6 +71,25 @@ public class DocumentStack extends LinkedList implements Durable { } } + /** + * Build key that uniquely identifies this stack. It omits most of the raw + * details included in {@link #write(DataOutputStream)}, since they change + * too regularly to be used as a key. + */ + public String buildKey() { + final StringBuilder builder = new StringBuilder(); + if (root != null) { + builder.append(root.authority).append('#'); + builder.append(root.rootId).append('#'); + } else { + builder.append("[null]").append('#'); + } + for (DocumentInfo doc : this) { + builder.append(doc.documentId).append('#'); + } + return builder.toString(); + } + @Override public void reset() { clear(); diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/RootInfo.java b/packages/DocumentsUI/src/com/android/documentsui/model/RootInfo.java index 014901a2937f0..e220c9e382fb1 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/model/RootInfo.java +++ b/packages/DocumentsUI/src/com/android/documentsui/model/RootInfo.java @@ -42,10 +42,10 @@ import java.util.Objects; */ public class RootInfo implements Durable, Parcelable { private static final int VERSION_INIT = 1; + private static final int VERSION_DROP_TYPE = 2; public String authority; public String rootId; - public int rootType; public int flags; public int icon; public String title; @@ -67,7 +67,6 @@ public class RootInfo implements Durable, Parcelable { public void reset() { authority = null; rootId = null; - rootType = 0; flags = 0; icon = 0; title = null; @@ -85,10 +84,9 @@ public class RootInfo implements Durable, Parcelable { public void read(DataInputStream in) throws IOException { final int version = in.readInt(); switch (version) { - case VERSION_INIT: + case VERSION_DROP_TYPE: authority = DurableUtils.readNullableString(in); rootId = DurableUtils.readNullableString(in); - rootType = in.readInt(); flags = in.readInt(); icon = in.readInt(); title = DurableUtils.readNullableString(in); @@ -105,10 +103,9 @@ public class RootInfo implements Durable, Parcelable { @Override public void write(DataOutputStream out) throws IOException { - out.writeInt(VERSION_INIT); + out.writeInt(VERSION_DROP_TYPE); DurableUtils.writeNullableString(out, authority); DurableUtils.writeNullableString(out, rootId); - out.writeInt(rootType); out.writeInt(flags); out.writeInt(icon); DurableUtils.writeNullableString(out, title); @@ -146,7 +143,6 @@ public class RootInfo implements Durable, Parcelable { final RootInfo root = new RootInfo(); root.authority = authority; root.rootId = getCursorString(cursor, Root.COLUMN_ROOT_ID); - root.rootType = getCursorInt(cursor, Root.COLUMN_ROOT_TYPE); root.flags = getCursorInt(cursor, Root.COLUMN_FLAGS); root.icon = getCursorInt(cursor, Root.COLUMN_ICON); root.title = getCursorString(cursor, Root.COLUMN_TITLE); @@ -162,25 +158,44 @@ public class RootInfo implements Durable, Parcelable { derivedMimeTypes = (mimeTypes != null) ? mimeTypes.split("\n") : null; // TODO: remove these special case icons - if ("com.android.externalstorage.documents".equals(authority)) { - if ("documents".equals(rootId)) { - derivedIcon = R.drawable.ic_doc_text; - } else { - derivedIcon = R.drawable.ic_root_sdcard; - } - } - if ("com.android.providers.downloads.documents".equals(authority)) { + if (isExternalStorage()) { + derivedIcon = R.drawable.ic_root_sdcard; + } else if (isDownloads()) { derivedIcon = R.drawable.ic_root_download; + } else if (isImages()) { + derivedIcon = R.drawable.ic_doc_image; + } else if (isVideos()) { + derivedIcon = R.drawable.ic_doc_video; + } else if (isAudio()) { + derivedIcon = R.drawable.ic_doc_audio; } - if ("com.android.providers.media.documents".equals(authority)) { - if ("images_root".equals(rootId)) { - derivedIcon = R.drawable.ic_doc_image; - } else if ("videos_root".equals(rootId)) { - derivedIcon = R.drawable.ic_doc_video; - } else if ("audio_root".equals(rootId)) { - derivedIcon = R.drawable.ic_doc_audio; - } - } + } + + public boolean isRecents() { + return authority == null && rootId == null; + } + + public boolean isExternalStorage() { + return "com.android.externalstorage.documents".equals(authority); + } + + public boolean isDownloads() { + return "com.android.providers.downloads.documents".equals(authority); + } + + public boolean isImages() { + return "com.android.providers.media.documents".equals(authority) + && "images_root".equals(rootId); + } + + public boolean isVideos() { + return "com.android.providers.media.documents".equals(authority) + && "videos_root".equals(rootId); + } + + public boolean isAudio() { + return "com.android.providers.media.documents".equals(authority) + && "audio_root".equals(rootId); } @Override diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java index ed28da5225e9e..9328b33a73aea 100644 --- a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java +++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java @@ -47,9 +47,8 @@ public class ExternalStorageProvider extends DocumentsProvider { // docId format: root:path/to/file private static final String[] DEFAULT_ROOT_PROJECTION = new String[] { - Root.COLUMN_ROOT_ID, Root.COLUMN_ROOT_TYPE, Root.COLUMN_FLAGS, Root.COLUMN_ICON, - Root.COLUMN_TITLE, Root.COLUMN_SUMMARY, Root.COLUMN_DOCUMENT_ID, - Root.COLUMN_AVAILABLE_BYTES, + Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE, + Root.COLUMN_DOCUMENT_ID, Root.COLUMN_AVAILABLE_BYTES, }; private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] { @@ -59,7 +58,6 @@ public class ExternalStorageProvider extends DocumentsProvider { private static class RootInfo { public String rootId; - public int rootType; public int flags; public String title; public String docId; @@ -84,7 +82,6 @@ public class ExternalStorageProvider extends DocumentsProvider { final RootInfo root = new RootInfo(); root.rootId = rootId; - root.rootType = Root.ROOT_TYPE_DEVICE; root.flags = Root.FLAG_SUPPORTS_CREATE | Root.FLAG_LOCAL_ONLY | Root.FLAG_ADVANCED | Root.FLAG_SUPPORTS_SEARCH; root.title = getContext().getString(R.string.root_internal_storage); @@ -198,7 +195,6 @@ public class ExternalStorageProvider extends DocumentsProvider { final RowBuilder row = result.newRow(); row.add(Root.COLUMN_ROOT_ID, root.rootId); - row.add(Root.COLUMN_ROOT_TYPE, root.rootType); row.add(Root.COLUMN_FLAGS, root.flags); row.add(Root.COLUMN_TITLE, root.title); row.add(Root.COLUMN_DOCUMENT_ID, root.docId); diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/TestDocumentsProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/TestDocumentsProvider.java index e6fbb1b762e08..5a15cd2dacdc8 100644 --- a/packages/ExternalStorageProvider/src/com/android/externalstorage/TestDocumentsProvider.java +++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/TestDocumentsProvider.java @@ -65,7 +65,7 @@ public class TestDocumentsProvider extends DocumentsProvider { private static final String MY_DOC_NULL = "myNull"; private static final String[] DEFAULT_ROOT_PROJECTION = new String[] { - Root.COLUMN_ROOT_ID, Root.COLUMN_ROOT_TYPE, Root.COLUMN_FLAGS, Root.COLUMN_ICON, + Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE, Root.COLUMN_SUMMARY, Root.COLUMN_DOCUMENT_ID, Root.COLUMN_AVAILABLE_BYTES, }; @@ -114,7 +114,6 @@ public class TestDocumentsProvider extends DocumentsProvider { final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection)); final RowBuilder row = result.newRow(); row.add(Root.COLUMN_ROOT_ID, MY_ROOT_ID); - row.add(Root.COLUMN_ROOT_TYPE, Root.ROOT_TYPE_SERVICE); row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_RECENTS); row.add(Root.COLUMN_TITLE, "_Test title which is really long"); row.add(Root.COLUMN_SUMMARY,