OSDN Git Service

Instance state, fix sharing, Durable objects.
authorJeff Sharkey <jsharkey@android.com>
Mon, 2 Sep 2013 01:41:04 +0000 (18:41 -0700)
committerJeff Sharkey <jsharkey@android.com>
Mon, 2 Sep 2013 01:59:38 +0000 (18:59 -0700)
Remember instance state across configuration changes, and enable
rotation.  This remembers current modes and in-progress traversals.

Always finish action modes after launching an action.  Fix sharing
by always putting Uris in extras, and always wrap in a chooser.  Find
common MIME types when sharing multiple documents.  Fix downloads
launching by following directory MIME type change.

Introduce "Durable" which is like Parcelable, but can be used for
both byte[] storage and Parcel transport.  Make both DocumentInfo
and DocumentStack durable.

Disable recents until new behavior is implemented.

Bug: 1046023610446265105336741045634410456702
Change-Id: I4eaf2b0b4cde611c69a1e7b5f1586f6b02019b27

12 files changed:
packages/DocumentsUI/AndroidManifest.xml
packages/DocumentsUI/res/values/strings.xml
packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java
packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java
packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
packages/DocumentsUI/src/com/android/documentsui/SortingCursorWrapper.java
packages/DocumentsUI/src/com/android/documentsui/model/DocumentInfo.java
packages/DocumentsUI/src/com/android/documentsui/model/DocumentStack.java
packages/DocumentsUI/src/com/android/documentsui/model/Durable.java [new file with mode: 0644]
packages/DocumentsUI/src/com/android/documentsui/model/DurableUtils.java [new file with mode: 0644]

index 6cc92e3..45e2650 100644 (file)
         <!-- TODO: allow rotation when state saving is in better shape -->
         <activity
             android:name=".DocumentsActivity"
-            android:finishOnCloseSystemDialogs="true"
-            android:excludeFromRecents="true"
-            android:theme="@android:style/Theme.Holo.Light"
-            android:screenOrientation="nosensor">
+            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" />
@@ -37,7 +34,7 @@
             <intent-filter>
                 <action android:name="android.provider.action.MANAGE_DOCUMENTS" />
                 <category android:name="android.intent.category.DEFAULT" />
-                <data android:mimeType="vnd.android.doc/dir" />
+                <data android:mimeType="vnd.android.document/directory" />
             </intent-filter>
         </activity>
 
index 928ba85..f4a822d 100644 (file)
@@ -63,4 +63,6 @@
     <string name="more">More</string>
     <string name="loading">Loading\u2026</string>
 
+    <string name="share_via">Share via</string>
+
 </resources>
index 79f846a..549e196 100644 (file)
@@ -17,9 +17,9 @@
 package com.android.documentsui;
 
 import static com.android.documentsui.DocumentsActivity.TAG;
-import static com.android.documentsui.DocumentsActivity.DisplayState.ACTION_MANAGE;
-import static com.android.documentsui.DocumentsActivity.DisplayState.MODE_GRID;
-import static com.android.documentsui.DocumentsActivity.DisplayState.MODE_LIST;
+import static com.android.documentsui.DocumentsActivity.State.ACTION_MANAGE;
+import static com.android.documentsui.DocumentsActivity.State.MODE_GRID;
+import static com.android.documentsui.DocumentsActivity.State.MODE_LIST;
 import static com.android.documentsui.model.DocumentInfo.getCursorInt;
 import static com.android.documentsui.model.DocumentInfo.getCursorLong;
 import static com.android.documentsui.model.DocumentInfo.getCursorString;
@@ -62,7 +62,7 @@ import android.widget.ListView;
 import android.widget.TextView;
 import android.widget.Toast;
 
-import com.android.documentsui.DocumentsActivity.DisplayState;
+import com.android.documentsui.DocumentsActivity.State;
 import com.android.documentsui.model.DocumentInfo;
 import com.android.internal.util.Predicate;
 import com.google.android.collect.Lists;
@@ -168,7 +168,7 @@ public class DirectoryFragment extends Fragment {
         mCallbacks = new LoaderCallbacks<DirectoryResult>() {
             @Override
             public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) {
-                final DisplayState state = getDisplayState(DirectoryFragment.this);
+                final State state = getDisplayState(DirectoryFragment.this);
 
                 Uri contentsUri;
                 if (mType == TYPE_NORMAL) {
@@ -196,7 +196,7 @@ public class DirectoryFragment extends Fragment {
     }
 
     public void updateDisplayState() {
-        final DisplayState state = getDisplayState(this);
+        final State state = getDisplayState(this);
 
         if (mLastSortOrder != state.sortOrder) {
             getLoaderManager().restartLoader(mLoaderId, null, mCallbacks);
@@ -263,7 +263,7 @@ public class DirectoryFragment extends Fragment {
 
         @Override
         public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
-            final DisplayState state = getDisplayState(DirectoryFragment.this);
+            final State state = getDisplayState(DirectoryFragment.this);
 
             final MenuItem open = menu.findItem(R.id.menu_open);
             final MenuItem share = menu.findItem(R.id.menu_share);
@@ -294,14 +294,17 @@ public class DirectoryFragment extends Fragment {
             final int id = item.getItemId();
             if (id == R.id.menu_open) {
                 DocumentsActivity.get(DirectoryFragment.this).onDocumentsPicked(docs);
+                mode.finish();
                 return true;
 
             } else if (id == R.id.menu_share) {
                 onShareDocuments(docs);
+                mode.finish();
                 return true;
 
             } else if (id == R.id.menu_delete) {
                 onDeleteDocuments(docs);
+                mode.finish();
                 return true;
 
             } else {
@@ -332,26 +335,36 @@ public class DirectoryFragment extends Fragment {
     };
 
     private void onShareDocuments(List<DocumentInfo> docs) {
-        final ArrayList<Uri> uris = Lists.newArrayList();
-        for (DocumentInfo doc : docs) {
-            uris.add(doc.uri);
-        }
+        Intent intent;
+        if (docs.size() == 1) {
+            final DocumentInfo doc = docs.get(0);
+
+            intent = new Intent(Intent.ACTION_SEND);
+            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+            intent.addCategory(Intent.CATEGORY_DEFAULT);
+            intent.setType(doc.mimeType);
+            intent.putExtra(Intent.EXTRA_STREAM, doc.uri);
 
-        final Intent intent;
-        if (uris.size() > 1) {
+        } else if (docs.size() > 1) {
             intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
             intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
             intent.addCategory(Intent.CATEGORY_DEFAULT);
-            // TODO: find common mimetype
-            intent.setType("*/*");
+
+            final ArrayList<String> mimeTypes = Lists.newArrayList();
+            final ArrayList<Uri> uris = Lists.newArrayList();
+            for (DocumentInfo doc : docs) {
+                mimeTypes.add(doc.mimeType);
+                uris.add(doc.uri);
+            }
+
+            intent.setType(findCommonMimeType(mimeTypes));
             intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
+
         } else {
-            intent = new Intent(Intent.ACTION_SEND);
-            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
-            intent.addCategory(Intent.CATEGORY_DEFAULT);
-            intent.setData(uris.get(0));
+            return;
         }
 
+        intent = Intent.createChooser(intent, getActivity().getText(R.string.share_via));
         startActivity(intent);
     }
 
@@ -383,7 +396,7 @@ public class DirectoryFragment extends Fragment {
         }
     }
 
-    private static DisplayState getDisplayState(Fragment fragment) {
+    private static State getDisplayState(Fragment fragment) {
         return ((DocumentsActivity) fragment.getActivity()).getDisplayState();
     }
 
@@ -411,7 +424,7 @@ public class DirectoryFragment extends Fragment {
         @Override
         public View getView(int position, View convertView, ViewGroup parent) {
             final Context context = parent.getContext();
-            final DisplayState state = getDisplayState(DirectoryFragment.this);
+            final State state = getDisplayState(DirectoryFragment.this);
 
             final RootsCache roots = DocumentsApplication.getRootsCache(context);
             final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache(
@@ -586,4 +599,28 @@ public class DirectoryFragment extends Fragment {
 
         return DateUtils.formatDateTime(context, when, flags);
     }
+
+    private String findCommonMimeType(List<String> mimeTypes) {
+        String[] commonType = mimeTypes.get(0).split("/");
+        if (commonType.length != 2) {
+            return "*/*";
+        }
+
+        for (int i = 1; i < mimeTypes.size(); i++) {
+            String[] type = mimeTypes.get(i).split("/");
+            if (type.length != 2) continue;
+
+            if (!commonType[1].equals(type[1])) {
+                commonType[1] = "*";
+            }
+
+            if (!commonType[0].equals(type[0])) {
+                commonType[0] = "*";
+                commonType[1] = "*";
+                break;
+            }
+        }
+
+        return commonType[0] + "/" + commonType[1];
+    }
 }
index b2be11b..fa674d5 100644 (file)
 
 package com.android.documentsui;
 
+import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_DISPLAY_NAME;
+import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_LAST_MODIFIED;
+import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_SIZE;
+
 import android.content.AsyncTaskLoader;
 import android.content.ContentProviderClient;
 import android.content.Context;
@@ -25,8 +29,6 @@ import android.os.CancellationSignal;
 import android.os.OperationCanceledException;
 import android.provider.DocumentsContract.Document;
 
-import com.android.documentsui.DocumentsActivity.DisplayState;
-
 import libcore.io.IoUtils;
 
 class DirectoryResult implements AutoCloseable {
@@ -149,11 +151,11 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> {
 
     private String getQuerySortOrder() {
         switch (mSortOrder) {
-            case DisplayState.SORT_ORDER_DISPLAY_NAME:
+            case SORT_ORDER_DISPLAY_NAME:
                 return Document.COLUMN_DISPLAY_NAME + " ASC";
-            case DisplayState.SORT_ORDER_LAST_MODIFIED:
+            case SORT_ORDER_LAST_MODIFIED:
                 return Document.COLUMN_LAST_MODIFIED + " DESC";
-            case DisplayState.SORT_ORDER_SIZE:
+            case SORT_ORDER_SIZE:
                 return Document.COLUMN_SIZE + " DESC";
             default:
                 return null;
index 912d6dc..da790cc 100644 (file)
 
 package com.android.documentsui;
 
-import static com.android.documentsui.DocumentsActivity.DisplayState.ACTION_CREATE;
-import static com.android.documentsui.DocumentsActivity.DisplayState.ACTION_GET_CONTENT;
-import static com.android.documentsui.DocumentsActivity.DisplayState.ACTION_MANAGE;
-import static com.android.documentsui.DocumentsActivity.DisplayState.ACTION_OPEN;
-import static com.android.documentsui.DocumentsActivity.DisplayState.MODE_GRID;
-import static com.android.documentsui.DocumentsActivity.DisplayState.MODE_LIST;
-import static com.android.documentsui.DocumentsActivity.DisplayState.SORT_ORDER_LAST_MODIFIED;
+import static com.android.documentsui.DocumentsActivity.State.ACTION_CREATE;
+import static com.android.documentsui.DocumentsActivity.State.ACTION_GET_CONTENT;
+import static com.android.documentsui.DocumentsActivity.State.ACTION_MANAGE;
+import static com.android.documentsui.DocumentsActivity.State.ACTION_OPEN;
+import static com.android.documentsui.DocumentsActivity.State.MODE_GRID;
+import static com.android.documentsui.DocumentsActivity.State.MODE_LIST;
+import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_LAST_MODIFIED;
 
 import android.app.ActionBar;
 import android.app.ActionBar.OnNavigationListener;
@@ -41,6 +41,7 @@ import android.database.Cursor;
 import android.graphics.drawable.ColorDrawable;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.Parcel;
 import android.provider.DocumentsContract;
 import android.support.v4.app.ActionBarDrawerToggle;
 import android.support.v4.view.GravityCompat;
@@ -61,31 +62,27 @@ import android.widget.Toast;
 
 import com.android.documentsui.model.DocumentInfo;
 import com.android.documentsui.model.DocumentStack;
+import com.android.documentsui.model.DurableUtils;
 import com.android.documentsui.model.RootInfo;
 
 import java.io.FileNotFoundException;
+import java.io.IOException;
 import java.util.Arrays;
 import java.util.List;
 
 public class DocumentsActivity extends Activity {
     public static final String TAG = "Documents";
 
-    private int mAction;
-
     private SearchView mSearchView;
 
     private View mRootsContainer;
     private DrawerLayout mDrawerLayout;
     private ActionBarDrawerToggle mDrawerToggle;
 
-    private final DisplayState mDisplayState = new DisplayState();
+    private static final String EXTRA_STATE = "state";
 
     private RootsCache mRoots;
-
-    /** Current user navigation stack; empty implies recents. */
-    private DocumentStack mStack = new DocumentStack();
-    /** Currently active search, overriding any stack. */
-    private String mCurrentSearch;
+    private State mState;
 
     @Override
     public void onCreate(Bundle icicle) {
@@ -93,72 +90,83 @@ public class DocumentsActivity extends Activity {
 
         mRoots = DocumentsApplication.getRootsCache(this);
 
-        final Intent intent = getIntent();
-        final String action = intent.getAction();
-        if (Intent.ACTION_OPEN_DOCUMENT.equals(action)) {
-            mAction = ACTION_OPEN;
-        } else if (Intent.ACTION_CREATE_DOCUMENT.equals(action)) {
-            mAction = ACTION_CREATE;
-        } else if (Intent.ACTION_GET_CONTENT.equals(action)) {
-            mAction = ACTION_GET_CONTENT;
-        } else if (DocumentsContract.ACTION_MANAGE_DOCUMENTS.equals(action)) {
-            mAction = ACTION_MANAGE;
-        }
+        setResult(Activity.RESULT_CANCELED);
+        setContentView(R.layout.activity);
 
-        // TODO: unify action in single place
-        mDisplayState.action = mAction;
+        mRootsContainer = findViewById(R.id.container_roots);
 
-        if (mAction == ACTION_OPEN || mAction == ACTION_GET_CONTENT) {
-            mDisplayState.allowMultiple = intent.getBooleanExtra(
-                    Intent.EXTRA_ALLOW_MULTIPLE, false);
-        }
+        mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
 
-        if (mAction == ACTION_MANAGE) {
-            mDisplayState.acceptMimes = new String[] { "*/*" };
-            mDisplayState.allowMultiple = true;
-        } else if (intent.hasExtra(Intent.EXTRA_MIME_TYPES)) {
-            mDisplayState.acceptMimes = intent.getStringArrayExtra(Intent.EXTRA_MIME_TYPES);
+        mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout,
+                R.drawable.ic_drawer, R.string.drawer_open, R.string.drawer_close);
+
+        mDrawerLayout.setDrawerListener(mDrawerListener);
+        mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START);
+
+        if (icicle != null) {
+            mState = icicle.getParcelable(EXTRA_STATE);
         } else {
-            mDisplayState.acceptMimes = new String[] { intent.getType() };
+            buildDefaultState();
         }
 
-        mDisplayState.localOnly = intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false);
-
-        setResult(Activity.RESULT_CANCELED);
-        setContentView(R.layout.activity);
+        if (mState.action == ACTION_MANAGE) {
+            mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
+        }
 
-        if (mAction == ACTION_CREATE) {
+        if (mState.action == ACTION_CREATE) {
             final String mimeType = getIntent().getType();
             final String title = getIntent().getStringExtra(Intent.EXTRA_TITLE);
             SaveFragment.show(getFragmentManager(), mimeType, title);
         }
 
-        if (mAction == ACTION_GET_CONTENT) {
+        if (mState.action == ACTION_GET_CONTENT) {
             final Intent moreApps = new Intent(getIntent());
             moreApps.setComponent(null);
             moreApps.setPackage(null);
             RootsFragment.show(getFragmentManager(), moreApps);
-        } else if (mAction == ACTION_OPEN || mAction == ACTION_CREATE) {
+        } else if (mState.action == ACTION_OPEN || mState.action == ACTION_CREATE) {
             RootsFragment.show(getFragmentManager(), null);
         }
 
-        if (mAction == ACTION_MANAGE) {
-            mDisplayState.sortOrder = SORT_ORDER_LAST_MODIFIED;
-        }
+        onCurrentDirectoryChanged();
+    }
 
-        mRootsContainer = findViewById(R.id.container_roots);
+    private void buildDefaultState() {
+        mState = new State();
 
-        mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
+        final Intent intent = getIntent();
+        final String action = intent.getAction();
+        if (Intent.ACTION_OPEN_DOCUMENT.equals(action)) {
+            mState.action = ACTION_OPEN;
+        } else if (Intent.ACTION_CREATE_DOCUMENT.equals(action)) {
+            mState.action = ACTION_CREATE;
+        } else if (Intent.ACTION_GET_CONTENT.equals(action)) {
+            mState.action = ACTION_GET_CONTENT;
+        } else if (DocumentsContract.ACTION_MANAGE_DOCUMENTS.equals(action)) {
+            mState.action = ACTION_MANAGE;
+        }
 
-        mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout,
-                R.drawable.ic_drawer, R.string.drawer_open, R.string.drawer_close);
+        if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
+            mState.allowMultiple = intent.getBooleanExtra(
+                    Intent.EXTRA_ALLOW_MULTIPLE, false);
+        }
 
-        mDrawerLayout.setDrawerListener(mDrawerListener);
-        mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START);
+        if (mState.action == ACTION_MANAGE) {
+            mState.acceptMimes = new String[] { "*/*" };
+            mState.allowMultiple = true;
+        } else if (intent.hasExtra(Intent.EXTRA_MIME_TYPES)) {
+            mState.acceptMimes = intent.getStringArrayExtra(Intent.EXTRA_MIME_TYPES);
+        } else {
+            mState.acceptMimes = new String[] { intent.getType() };
+        }
 
-        if (mAction == ACTION_MANAGE) {
-            mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
+        mState.localOnly = intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false);
 
+        if (mState.action == ACTION_MANAGE) {
+            mState.sortOrder = SORT_ORDER_LAST_MODIFIED;
+        }
+
+        if (mState.action == ACTION_MANAGE) {
             final Uri rootUri = intent.getData();
             final RootInfo root = mRoots.findRoot(rootUri);
             if (root != null) {
@@ -169,8 +177,6 @@ public class DocumentsActivity extends Activity {
             }
 
         } else {
-            mDrawerLayout.openDrawer(mRootsContainer);
-
             // Restore last stack for calling package
             // TODO: move into async loader
             final String packageName = getCallingPackage();
@@ -178,17 +184,17 @@ public class DocumentsActivity extends Activity {
                     .query(RecentsProvider.buildResume(packageName), null, null, null, null);
             try {
                 if (cursor.moveToFirst()) {
-                    final String raw = cursor.getString(
+                    final byte[] rawStack = cursor.getBlob(
                             cursor.getColumnIndex(RecentsProvider.COL_PATH));
-                    mStack = DocumentStack.deserialize(getContentResolver(), raw);
+                    DurableUtils.readFromArray(rawStack, mState.stack);
                 }
-            } catch (FileNotFoundException e) {
+            } catch (IOException e) {
                 Log.w(TAG, "Failed to resume", e);
             } finally {
                 cursor.close();
             }
 
-            onCurrentDirectoryChanged();
+            mDrawerLayout.openDrawer(mRootsContainer);
         }
     }
 
@@ -196,10 +202,10 @@ public class DocumentsActivity extends Activity {
     public void onStart() {
         super.onStart();
 
-        if (mAction == ACTION_MANAGE) {
-            mDisplayState.showSize = true;
+        if (mState.action == ACTION_MANAGE) {
+            mState.showSize = true;
         } else {
-            mDisplayState.showSize = SettingsActivity.getDisplayFileSize(this);
+            mState.showSize = SettingsActivity.getDisplayFileSize(this);
         }
     }
 
@@ -242,9 +248,9 @@ public class DocumentsActivity extends Activity {
             actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
             actionBar.setIcon(new ColorDrawable());
 
-            if (mAction == ACTION_OPEN || mAction == ACTION_GET_CONTENT) {
+            if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
                 actionBar.setTitle(R.string.title_open);
-            } else if (mAction == ACTION_CREATE) {
+            } else if (mState.action == ACTION_CREATE) {
                 actionBar.setTitle(R.string.title_save);
             }
 
@@ -262,13 +268,13 @@ public class DocumentsActivity extends Activity {
                 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
                 actionBar.setTitle(null);
                 actionBar.setListNavigationCallbacks(mSortAdapter, mSortListener);
-                actionBar.setSelectedNavigationItem(mDisplayState.sortOrder);
+                actionBar.setSelectedNavigationItem(mState.sortOrder);
             }
 
-            if (mStack.size() > 1) {
+            if (mState.stack.size() > 1) {
                 actionBar.setDisplayHomeAsUpEnabled(true);
                 mDrawerToggle.setDrawerIndicatorEnabled(false);
-            } else if (mAction == ACTION_MANAGE) {
+            } else if (mState.action == ACTION_MANAGE) {
                 actionBar.setDisplayHomeAsUpEnabled(false);
                 mDrawerToggle.setDrawerIndicatorEnabled(false);
             } else {
@@ -288,7 +294,7 @@ public class DocumentsActivity extends Activity {
         mSearchView.setOnQueryTextListener(new OnQueryTextListener() {
             @Override
             public boolean onQueryTextSubmit(String query) {
-                mCurrentSearch = query;
+                mState.currentSearch = query;
                 onCurrentDirectoryChanged();
                 mSearchView.setIconified(true);
                 return true;
@@ -303,7 +309,7 @@ public class DocumentsActivity extends Activity {
         mSearchView.setOnCloseListener(new OnCloseListener() {
             @Override
             public boolean onClose() {
-                mCurrentSearch = null;
+                mState.currentSearch = null;
                 onCurrentDirectoryChanged();
                 return false;
             }
@@ -325,11 +331,11 @@ public class DocumentsActivity extends Activity {
         final MenuItem list = menu.findItem(R.id.menu_list);
         final MenuItem settings = menu.findItem(R.id.menu_settings);
 
-        grid.setVisible(mDisplayState.mode != MODE_GRID);
-        list.setVisible(mDisplayState.mode != MODE_LIST);
+        grid.setVisible(mState.mode != MODE_GRID);
+        list.setVisible(mState.mode != MODE_LIST);
 
         final boolean searchVisible;
-        if (mAction == ACTION_CREATE) {
+        if (mState.action == ACTION_CREATE) {
             createDir.setVisible(cwd != null && cwd.isCreateSupported());
             searchVisible = false;
 
@@ -348,7 +354,7 @@ public class DocumentsActivity extends Activity {
         // TODO: close any search in-progress when hiding
         search.setVisible(searchVisible);
 
-        settings.setVisible(mAction != ACTION_MANAGE);
+        settings.setVisible(mState.action != ACTION_MANAGE);
 
         return true;
     }
@@ -370,13 +376,13 @@ public class DocumentsActivity extends Activity {
             return false;
         } else if (id == R.id.menu_grid) {
             // TODO: persist explicit user mode for cwd
-            mDisplayState.mode = MODE_GRID;
+            mState.mode = MODE_GRID;
             updateDisplayState();
             invalidateOptionsMenu();
             return true;
         } else if (id == R.id.menu_list) {
             // TODO: persist explicit user mode for cwd
-            mDisplayState.mode = MODE_LIST;
+            mState.mode = MODE_LIST;
             updateDisplayState();
             invalidateOptionsMenu();
             return true;
@@ -390,9 +396,9 @@ public class DocumentsActivity extends Activity {
 
     @Override
     public void onBackPressed() {
-        final int size = mStack.size();
+        final int size = mState.stack.size();
         if (size > 1) {
-            mStack.pop();
+            mState.stack.pop();
             onCurrentDirectoryChanged();
         } else if (size == 1 && !mDrawerLayout.isDrawerOpen(mRootsContainer)) {
             // TODO: open root drawer once we can capture back key
@@ -402,11 +408,23 @@ public class DocumentsActivity extends Activity {
         }
     }
 
+    @Override
+    protected void onSaveInstanceState(Bundle state) {
+        super.onSaveInstanceState(state);
+        state.putParcelable(EXTRA_STATE, mState);
+    }
+
+    @Override
+    protected void onRestoreInstanceState(Bundle state) {
+        super.onRestoreInstanceState(state);
+        updateActionBar();
+    }
+
     // TODO: support additional sort orders
     private BaseAdapter mSortAdapter = new BaseAdapter() {
         @Override
         public int getCount() {
-            return mDisplayState.showSize ? 3 : 2;
+            return mState.showSize ? 3 : 2;
         }
 
         @Override
@@ -438,8 +456,8 @@ public class DocumentsActivity extends Activity {
             final TextView title = (TextView) convertView.findViewById(android.R.id.title);
             final TextView summary = (TextView) convertView.findViewById(android.R.id.summary);
 
-            if (mStack.size() > 0) {
-                title.setText(mStack.getTitle(mRoots));
+            if (mState.stack.size() > 0) {
+                title.setText(mState.stack.getTitle(mRoots));
             } else {
                 // No directory means recents
                 title.setText(R.string.root_recent);
@@ -467,26 +485,26 @@ public class DocumentsActivity extends Activity {
     private OnNavigationListener mSortListener = new OnNavigationListener() {
         @Override
         public boolean onNavigationItemSelected(int itemPosition, long itemId) {
-            mDisplayState.sortOrder = itemPosition;
+            mState.sortOrder = itemPosition;
             updateDisplayState();
             return true;
         }
     };
 
     public RootInfo getCurrentRoot() {
-        if (mStack.size() > 0) {
-            return mStack.getRoot(mRoots);
+        if (mState.stack.size() > 0) {
+            return mState.stack.getRoot(mRoots);
         } else {
             return mRoots.getRecentsRoot();
         }
     }
 
     public DocumentInfo getCurrentDirectory() {
-        return mStack.peek();
+        return mState.stack.peek();
     }
 
-    public DisplayState getDisplayState() {
-        return mDisplayState;
+    public State getDisplayState() {
+        return mState;
     }
 
     private void onCurrentDirectoryChanged() {
@@ -495,15 +513,15 @@ public class DocumentsActivity extends Activity {
 
         if (cwd == null) {
             // No directory means recents
-            if (mAction == ACTION_CREATE) {
+            if (mState.action == ACTION_CREATE) {
                 RecentsCreateFragment.show(fm);
             } else {
                 DirectoryFragment.showRecentsOpen(fm);
             }
         } else {
-            if (mCurrentSearch != null) {
+            if (mState.currentSearch != null) {
                 // Ongoing search
-                DirectoryFragment.showSearch(fm, cwd.uri, mCurrentSearch);
+                DirectoryFragment.showSearch(fm, cwd.uri, mState.currentSearch);
             } else {
                 // Normal boring directory
                 DirectoryFragment.showNormal(fm, cwd.uri);
@@ -511,7 +529,7 @@ public class DocumentsActivity extends Activity {
         }
 
         // Forget any replacement target
-        if (mAction == ACTION_CREATE) {
+        if (mState.action == ACTION_CREATE) {
             final SaveFragment save = SaveFragment.get(fm);
             if (save != null) {
                 save.setReplaceTarget(null);
@@ -529,13 +547,13 @@ public class DocumentsActivity extends Activity {
     }
 
     public void onStackPicked(DocumentStack stack) {
-        mStack = stack;
+        mState.stack = stack;
         onCurrentDirectoryChanged();
     }
 
     public void onRootPicked(RootInfo root, boolean closeDrawer) {
         // Clear entire backstack and start in new root
-        mStack.clear();
+        mState.stack.clear();
 
         if (!mRoots.isRecentsRoot(root)) {
             try {
@@ -566,19 +584,19 @@ public class DocumentsActivity extends Activity {
         if (doc.isDirectory()) {
             // TODO: query display mode user preference for this dir
             if (doc.isGridPreferred()) {
-                mDisplayState.mode = MODE_GRID;
+                mState.mode = MODE_GRID;
             } else {
-                mDisplayState.mode = MODE_LIST;
+                mState.mode = MODE_LIST;
             }
-            mStack.push(doc);
+            mState.stack.push(doc);
             onCurrentDirectoryChanged();
-        } else if (mAction == ACTION_OPEN || mAction == ACTION_GET_CONTENT) {
+        } else if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
             // Explicit file picked, return
             onFinished(doc.uri);
-        } else if (mAction == ACTION_CREATE) {
+        } else if (mState.action == ACTION_CREATE) {
             // Replace selected file
             SaveFragment.get(fm).setReplaceTarget(doc);
-        } else if (mAction == ACTION_MANAGE) {
+        } else if (mState.action == ACTION_MANAGE) {
             // Open the document
             final Intent intent = new Intent(Intent.ACTION_VIEW);
             intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
@@ -592,7 +610,7 @@ public class DocumentsActivity extends Activity {
     }
 
     public void onDocumentsPicked(List<DocumentInfo> docs) {
-        if (mAction == ACTION_OPEN || mAction == ACTION_GET_CONTENT) {
+        if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
             final int size = docs.size();
             final Uri[] uris = new Uri[size];
             for (int i = 0; i < size; i++) {
@@ -629,14 +647,14 @@ public class DocumentsActivity extends Activity {
         final ContentResolver resolver = getContentResolver();
         final ContentValues values = new ContentValues();
 
-        final String rawStack = DocumentStack.serialize(mStack);
-        if (mAction == ACTION_CREATE) {
+        final byte[] rawStack = DurableUtils.writeToArrayOrNull(mState.stack);
+        if (mState.action == ACTION_CREATE) {
             // Remember stack for last create
             values.clear();
             values.put(RecentsProvider.COL_PATH, rawStack);
             resolver.insert(RecentsProvider.buildRecentCreate(), values);
 
-        } else if (mAction == ACTION_OPEN || mAction == ACTION_GET_CONTENT) {
+        } else if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
             // Remember opened items
             for (Uri uri : uris) {
                 values.clear();
@@ -656,14 +674,14 @@ public class DocumentsActivity extends Activity {
             intent.setData(uris[0]);
         } else if (uris.length > 1) {
             final ClipData clipData = new ClipData(
-                    null, mDisplayState.acceptMimes, new ClipData.Item(uris[0]));
+                    null, mState.acceptMimes, new ClipData.Item(uris[0]));
             for (int i = 1; i < uris.length; i++) {
                 clipData.addItem(new ClipData.Item(uris[i]));
             }
             intent.setClipData(clipData);
         }
 
-        if (mAction == ACTION_GET_CONTENT) {
+        if (mState.action == ACTION_GET_CONTENT) {
             intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
         } else {
             intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
@@ -675,7 +693,7 @@ public class DocumentsActivity extends Activity {
         finish();
     }
 
-    public static class DisplayState {
+    public static class State implements android.os.Parcelable {
         public int action;
         public int mode = MODE_LIST;
         public String[] acceptMimes;
@@ -684,6 +702,11 @@ public class DocumentsActivity extends Activity {
         public boolean showSize = false;
         public boolean localOnly = false;
 
+        /** Current user navigation stack; empty implies recents. */
+        public DocumentStack stack = new DocumentStack();
+        /** Currently active search, overriding any stack. */
+        public String currentSearch;
+
         public static final int ACTION_OPEN = 1;
         public static final int ACTION_CREATE = 2;
         public static final int ACTION_GET_CONTENT = 3;
@@ -695,11 +718,51 @@ public class DocumentsActivity extends Activity {
         public static final int SORT_ORDER_DISPLAY_NAME = 0;
         public static final int SORT_ORDER_LAST_MODIFIED = 1;
         public static final int SORT_ORDER_SIZE = 2;
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, int flags) {
+            out.writeInt(action);
+            out.writeInt(mode);
+            out.writeStringArray(acceptMimes);
+            out.writeInt(sortOrder);
+            out.writeInt(allowMultiple ? 1 : 0);
+            out.writeInt(showSize ? 1 : 0);
+            out.writeInt(localOnly ? 1 : 0);
+            DurableUtils.writeToParcel(out, stack);
+            out.writeString(currentSearch);
+        }
+
+        public static final Creator<State> CREATOR = new Creator<State>() {
+            @Override
+            public State createFromParcel(Parcel in) {
+                final State state = new State();
+                state.action = in.readInt();
+                state.mode = in.readInt();
+                state.acceptMimes = in.readStringArray();
+                state.sortOrder = in.readInt();
+                state.allowMultiple = in.readInt() != 0;
+                state.showSize = in.readInt() != 0;
+                state.localOnly = in.readInt() != 0;
+                DurableUtils.readFromParcel(in, state.stack);
+                state.currentSearch = in.readString();
+                return state;
+            }
+
+            @Override
+            public State[] newArray(int size) {
+                return new State[size];
+            }
+        };
     }
 
     private void dumpStack() {
         Log.d(TAG, "Current stack:");
-        for (DocumentInfo doc : mStack) {
+        for (DocumentInfo doc : mState.stack) {
             Log.d(TAG, "--> " + doc);
         }
     }
index f5d87a7..fd7293d 100644 (file)
@@ -47,7 +47,9 @@ import com.google.android.collect.Lists;
 
 import libcore.io.IoUtils;
 
-import java.io.FileNotFoundException;
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -138,12 +140,13 @@ public class RecentsCreateFragment extends Fragment {
                     uri, null, null, null, RecentsProvider.COL_TIMESTAMP + " DESC", signal);
             try {
                 while (cursor != null && cursor.moveToNext()) {
-                    final String rawStack = cursor.getString(
+                    final byte[] raw = cursor.getBlob(
                             cursor.getColumnIndex(RecentsProvider.COL_PATH));
                     try {
-                        final DocumentStack stack = DocumentStack.deserialize(resolver, rawStack);
+                        final DocumentStack stack = new DocumentStack();
+                        stack.read(new DataInputStream(new ByteArrayInputStream(raw)));
                         result.add(stack);
-                    } catch (FileNotFoundException e) {
+                    } catch (IOException e) {
                         Log.w(TAG, "Failed to resolve stack: " + e);
                     }
                 }
index 880a92b..f67c309 100644 (file)
@@ -50,6 +50,8 @@ public class RootsCache {
     // TODO: cache roots in local provider to avoid spinning up backends
     // TODO: root updates should trigger UI refresh
 
+    private static final boolean RECENTS_ENABLED = false;
+
     private final Context mContext;
 
     public List<RootInfo> mRoots = Lists.newArrayList();
@@ -68,7 +70,7 @@ public class RootsCache {
     public void update() {
         mRoots.clear();
 
-        {
+        if (RECENTS_ENABLED) {
             // Create special root for recents
             final RootInfo root = new RootInfo();
             root.rootType = Root.ROOT_TYPE_SHORTCUT;
index 2d73732..257c106 100644 (file)
 
 package com.android.documentsui;
 
+import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_DISPLAY_NAME;
+import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_LAST_MODIFIED;
+import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_SIZE;
+
 import android.database.AbstractCursor;
 import android.database.Cursor;
 import android.provider.DocumentsContract.Document;
 
-import com.android.documentsui.DocumentsActivity.DisplayState;
-
 /**
  * Cursor wrapper that presents a sorted view of the underlying cursor. Handles
  * common {@link Document} sorting modes, such as ordering directories first.
@@ -39,12 +41,12 @@ public class SortingCursorWrapper extends AbstractCursor {
         final int count = cursor.getCount();
         mPosition = new int[count];
         switch (sortOrder) {
-            case DisplayState.SORT_ORDER_DISPLAY_NAME:
+            case SORT_ORDER_DISPLAY_NAME:
                 mValueString = new String[count];
                 mValueLong = null;
                 break;
-            case DisplayState.SORT_ORDER_LAST_MODIFIED:
-            case DisplayState.SORT_ORDER_SIZE:
+            case SORT_ORDER_LAST_MODIFIED:
+            case SORT_ORDER_SIZE:
                 mValueString = null;
                 mValueLong = new long[count];
                 break;
@@ -63,7 +65,7 @@ public class SortingCursorWrapper extends AbstractCursor {
             mPosition[i] = i;
 
             switch (sortOrder) {
-                case DisplayState.SORT_ORDER_DISPLAY_NAME:
+                case SORT_ORDER_DISPLAY_NAME:
                     final String mimeType = cursor.getString(mimeTypeIndex);
                     final String displayName = cursor.getString(displayNameIndex);
                     if (Document.MIME_TYPE_DIR.equals(mimeType)) {
@@ -72,24 +74,24 @@ public class SortingCursorWrapper extends AbstractCursor {
                         mValueString[i] = displayName;
                     }
                     break;
-                case DisplayState.SORT_ORDER_LAST_MODIFIED:
+                case SORT_ORDER_LAST_MODIFIED:
                     mValueLong[i] = cursor.getLong(lastModifiedIndex);
                     break;
-                case DisplayState.SORT_ORDER_SIZE:
+                case SORT_ORDER_SIZE:
                     mValueLong[i] = cursor.getLong(sizeIndex);
                     break;
             }
         }
 
         switch (sortOrder) {
-            case DisplayState.SORT_ORDER_DISPLAY_NAME:
+            case SORT_ORDER_DISPLAY_NAME:
                 synchronized (SortingCursorWrapper.class) {
 
                     binarySort(mPosition, mValueString);
                 }
                 break;
-            case DisplayState.SORT_ORDER_LAST_MODIFIED:
-            case DisplayState.SORT_ORDER_SIZE:
+            case SORT_ORDER_LAST_MODIFIED:
+            case SORT_ORDER_SIZE:
                 binarySort(mPosition, mValueLong);
                 break;
         }
index d571971..feccadc 100644 (file)
@@ -30,13 +30,19 @@ import com.android.documentsui.RecentsProvider;
 
 import libcore.io.IoUtils;
 
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
 import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.net.ProtocolException;
 import java.util.Comparator;
 
 /**
  * Representation of a {@link Document}.
  */
-public class DocumentInfo {
+public class DocumentInfo implements Durable {
+    private static final int VERSION_INIT = 1;
+
     public Uri uri;
     public String mimeType;
     public String displayName;
@@ -46,6 +52,55 @@ public class DocumentInfo {
     public long size;
     public int icon;
 
+    public DocumentInfo() {
+        reset();
+    }
+
+    @Override
+    public void reset() {
+        uri = null;
+        mimeType = null;
+        displayName = null;
+        lastModified = -1;
+        flags = 0;
+        summary = null;
+        size = -1;
+        icon = 0;
+    }
+
+    @Override
+    public void read(DataInputStream in) throws IOException {
+        final int version = in.readInt();
+        switch (version) {
+            case VERSION_INIT:
+                final String rawUri = DurableUtils.readNullableString(in);
+                uri = rawUri != null ? Uri.parse(rawUri) : null;
+                mimeType = DurableUtils.readNullableString(in);
+                displayName = DurableUtils.readNullableString(in);
+                lastModified = in.readLong();
+                flags = in.readInt();
+                summary = DurableUtils.readNullableString(in);
+                size = in.readLong();
+                icon = in.readInt();
+                break;
+            default:
+                throw new ProtocolException("Unknown version " + version);
+        }
+    }
+
+    @Override
+    public void write(DataOutputStream out) throws IOException {
+        out.writeInt(VERSION_INIT);
+        DurableUtils.writeNullableString(out, uri.toString());
+        DurableUtils.writeNullableString(out, mimeType);
+        DurableUtils.writeNullableString(out, displayName);
+        out.writeLong(lastModified);
+        out.writeInt(flags);
+        DurableUtils.writeNullableString(out, summary);
+        out.writeLong(size);
+        out.writeInt(icon);
+    }
+
     public static DocumentInfo fromDirectoryCursor(Uri parent, Cursor cursor) {
         final DocumentInfo doc = new DocumentInfo();
         final String authority = parent.getAuthority();
index b123a46..64631ab 100644 (file)
 
 package com.android.documentsui.model;
 
-import static com.android.documentsui.DocumentsActivity.TAG;
-import static com.android.documentsui.model.DocumentInfo.asFileNotFoundException;
-
-import android.content.ContentResolver;
-import android.net.Uri;
-import android.util.Log;
-
 import com.android.documentsui.RootsCache;
 
-import org.json.JSONArray;
-import org.json.JSONException;
-
-import java.io.FileNotFoundException;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.net.ProtocolException;
 import java.util.LinkedList;
 
 /**
  * Representation of a stack of {@link DocumentInfo}, usually the result of a
  * user-driven traversal.
  */
-public class DocumentStack extends LinkedList<DocumentInfo> {
-
-    public static String serialize(DocumentStack stack) {
-        final JSONArray json = new JSONArray();
-        for (int i = 0; i < stack.size(); i++) {
-            json.put(stack.get(i).uri);
-        }
-        return json.toString();
-    }
-
-    public static DocumentStack deserialize(ContentResolver resolver, String raw)
-            throws FileNotFoundException {
-        Log.d(TAG, "deserialize: " + raw);
-
-        final DocumentStack stack = new DocumentStack();
-        try {
-            final JSONArray json = new JSONArray(raw);
-            for (int i = 0; i < json.length(); i++) {
-                final Uri uri = Uri.parse(json.getString(i));
-                final DocumentInfo doc = DocumentInfo.fromUri(resolver, uri);
-                stack.add(doc);
-            }
-        } catch (JSONException e) {
-            throw asFileNotFoundException(e);
-        }
-
-        // TODO: handle roots that have gone missing
-        return stack;
-    }
+public class DocumentStack extends LinkedList<DocumentInfo> implements Durable {
+    private static final int VERSION_INIT = 1;
 
     public RootInfo getRoot(RootsCache roots) {
         return roots.findRoot(getLast().uri);
@@ -78,4 +44,37 @@ public class DocumentStack extends LinkedList<DocumentInfo> {
             return null;
         }
     }
+
+    @Override
+    public void reset() {
+        clear();
+    }
+
+    @Override
+    public void read(DataInputStream in) throws IOException {
+        final int version = in.readInt();
+        switch (version) {
+            case VERSION_INIT:
+                final int size = in.readInt();
+                for (int i = 0; i < size; i++) {
+                    final DocumentInfo doc = new DocumentInfo();
+                    doc.read(in);
+                    add(doc);
+                }
+                break;
+            default:
+                throw new ProtocolException("Unknown version " + version);
+        }
+    }
+
+    @Override
+    public void write(DataOutputStream out) throws IOException {
+        out.writeInt(VERSION_INIT);
+        final int size = size();
+        out.writeInt(size);
+        for (int i = 0; i < size; i++) {
+            final DocumentInfo doc = get(i);
+            doc.write(out);
+        }
+    }
 }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/Durable.java b/packages/DocumentsUI/src/com/android/documentsui/model/Durable.java
new file mode 100644 (file)
index 0000000..01633ed
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+ * 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.model;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+public interface Durable {
+    public void reset();
+    public void read(DataInputStream in) throws IOException;
+    public void write(DataOutputStream out) throws IOException;
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/DurableUtils.java b/packages/DocumentsUI/src/com/android/documentsui/model/DurableUtils.java
new file mode 100644 (file)
index 0000000..214fb14
--- /dev/null
@@ -0,0 +1,100 @@
+/*
+ * 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.model;
+
+import static com.android.documentsui.DocumentsActivity.TAG;
+
+import android.os.BadParcelableException;
+import android.os.Parcel;
+import android.util.Log;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+public class DurableUtils {
+    public static <D extends Durable> byte[] writeToArray(D d) throws IOException {
+        final ByteArrayOutputStream out = new ByteArrayOutputStream();
+        d.write(new DataOutputStream(out));
+        return out.toByteArray();
+    }
+
+    public static <D extends Durable> D readFromArray(byte[] data, D d) throws IOException {
+        final ByteArrayInputStream in = new ByteArrayInputStream(data);
+        d.reset();
+        try {
+            d.read(new DataInputStream(in));
+        } catch (IOException e) {
+            d.reset();
+            throw e;
+        }
+        return d;
+    }
+
+    public static <D extends Durable> byte[] writeToArrayOrNull(D d) {
+        try {
+            return writeToArray(d);
+        } catch (IOException e) {
+            Log.w(TAG, "Failed to write", e);
+            return null;
+        }
+    }
+
+    public static <D extends Durable> D readFromArrayOrNull(byte[] data, D d) {
+        try {
+            return readFromArray(data, d);
+        } catch (IOException e) {
+            Log.w(TAG, "Failed to read", e);
+            return null;
+        }
+    }
+
+    public static <D extends Durable> void writeToParcel(Parcel parcel, D d) {
+        try {
+            parcel.writeByteArray(writeToArray(d));
+        } catch (IOException e) {
+            throw new BadParcelableException(e);
+        }
+    }
+
+    public static <D extends Durable> D readFromParcel(Parcel parcel, D d) {
+        try {
+            return readFromArray(parcel.createByteArray(), d);
+        } catch (IOException e) {
+            throw new BadParcelableException(e);
+        }
+    }
+
+    public static void writeNullableString(DataOutputStream out, String value) throws IOException {
+        if (value != null) {
+            out.write(1);
+            out.writeUTF(value);
+        } else {
+            out.write(0);
+        }
+    }
+
+    public static String readNullableString(DataInputStream in) throws IOException {
+        if (in.read() != 0) {
+            return in.readUTF();
+        } else {
+            return null;
+        }
+    }
+}