Add a new developer options screen for shared data.

Add a new Storage section in the developer options menu which has a
new Shared Data preference. This preference screen shows all shared data
blobs on the device. There is also a new screen for each data blob which
shows all of the packages which currently have a leases on it. This
screen also has a button to delete the shared data blob.

Bug: 150626561
Test: make RunSettingsRoboTests ROBOTEST_FILTER=SharedDataPreferenceControllerTest
Test: manual (visual)
Change-Id: Id84a33dc7eeac493b2f81d3996ad24ee70557a07
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 17b498e..98789bd 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -1990,6 +1990,11 @@
                   android:label="Terms of Service"
                   android:theme="@android:style/Theme.DeviceDefault.Light.Dialog" />
 
+        <activity android:name=".development.storage.BlobInfoListView"
+                  android:label="@string/shared_data_title" />
+        <activity android:name=".development.storage.LeaseInfoListView"
+                  android:label="@string/accessor_info_title" />
+
         <activity android:name="Settings$WebViewAppPickerActivity"
                   android:label="@string/select_webview_provider_dialog_title" />
 
diff --git a/res/layout/blob_list_item_view.xml b/res/layout/blob_list_item_view.xml
new file mode 100644
index 0000000..897d19c
--- /dev/null
+++ b/res/layout/blob_list_item_view.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+ -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="?android:attr/selectableItemBackground"
+    android:gravity="center_vertical"
+    android:orientation="vertical"
+    android:minHeight="?android:attr/listPreferredItemHeightSmall"
+    android:padding="@dimen/list_preferred_item_padding">
+
+    <TextView
+        android:id="@+id/blob_label"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textDirection="locale"
+        android:ellipsize="marquee"
+        android:fadingEdge="horizontal"
+        android:singleLine="true"
+        android:textAppearance="?android:attr/textAppearanceListItem"/>
+
+    <TextView
+        android:id="@+id/blob_id"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:textAppearance="?android:attr/textAppearanceSmall"
+        android:textColor="?android:attr/textColorSecondary"/>
+
+    <TextView
+        android:id="@+id/blob_expiry"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:textAppearance="?android:attr/textAppearanceSmall"
+        android:textColor="?android:attr/textColorSecondary"/>
+</LinearLayout>
diff --git a/res/layout/lease_list_item_view.xml b/res/layout/lease_list_item_view.xml
new file mode 100644
index 0000000..5edd9e5
--- /dev/null
+++ b/res/layout/lease_list_item_view.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+ -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="?android:attr/selectableItemBackground"
+    android:gravity="center_vertical"
+    android:orientation="vertical"
+    android:minHeight="?android:attr/listPreferredItemHeightSmall"
+    android:padding="@dimen/list_preferred_item_padding">
+
+    <!-- TODO (varunshah@): add an image view for the app icon -->
+
+    <TextView
+        android:id="@+id/lease_package"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textDirection="locale"
+        android:ellipsize="marquee"
+        android:fadingEdge="horizontal"
+        android:singleLine="true"
+        android:textAppearance="?android:attr/textAppearanceListItem"/>
+
+    <TextView
+        android:id="@+id/lease_desc"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:textAppearance="?android:attr/textAppearanceSmall"
+        android:textColor="?android:attr/textColorSecondary"/>
+
+    <TextView
+        android:id="@+id/lease_expiry"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:textAppearance="?android:attr/textAppearanceSmall"
+        android:textColor="?android:attr/textColorSecondary"/>
+</LinearLayout>
diff --git a/res/layout/shared_data_empty_list_view.xml b/res/layout/shared_data_empty_list_view.xml
new file mode 100644
index 0000000..1bb338b
--- /dev/null
+++ b/res/layout/shared_data_empty_list_view.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:gravity="center"
+    android:background="@android:color/transparent"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+
+    <TextView
+        android:id="@+id/empty_view_text"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center"
+        android:textAppearance="?android:attr/textAppearanceLarge" />
+</LinearLayout>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 79071ed..7f4bb54 100755
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -417,4 +417,7 @@
     <dimen name="developer_option_dialog_margin_top">8dp</dimen>
     <dimen name="developer_option_dialog_min_height">48dp</dimen>
     <dimen name="developer_option_dialog_padding_start">16dp</dimen>
+
+    <!-- Developer options shared data screens related dimensions -->
+    <dimen name="list_preferred_item_padding">16dp</dimen>
 </resources>
diff --git a/res/xml/development_settings.xml b/res/xml/development_settings.xml
index 48a0850..c134763 100644
--- a/res/xml/development_settings.xml
+++ b/res/xml/development_settings.xml
@@ -652,4 +652,19 @@
             android:title="@string/autofill_reset_developer_options" />
 
     </com.android.settings.development.autofill.AutofillPreferenceCategory>
+
+    <PreferenceCategory
+        android:key="storage_category"
+        android:title="@string/storage_category"
+        android:order="1200">
+
+        <Preference
+            android:key="shared_data"
+            android:title="@string/shared_data_title"
+            android:summary="@string/shared_data_summary">
+            <intent
+                android:targetPackage="com.android.settings"
+                android:targetClass="com.android.settings.development.storage.BlobInfoListView" />
+        </Preference>
+    </PreferenceCategory>
 </PreferenceScreen>
diff --git a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
index c349de9..ef58c8a 100644
--- a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
+++ b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
@@ -52,6 +52,7 @@
 import com.android.settings.development.bluetooth.BluetoothHDAudioPreferenceController;
 import com.android.settings.development.bluetooth.BluetoothQualityDialogPreferenceController;
 import com.android.settings.development.bluetooth.BluetoothSampleRateDialogPreferenceController;
+import com.android.settings.development.storage.SharedDataPreferenceController;
 import com.android.settings.search.BaseSearchIndexProvider;
 import com.android.settings.widget.SwitchBar;
 import com.android.settingslib.core.AbstractPreferenceController;
@@ -531,6 +532,7 @@
                 bluetoothA2dpConfigStore));
         controllers.add(new BluetoothHDAudioPreferenceController(context, lifecycle,
                 bluetoothA2dpConfigStore, fragment));
+        controllers.add(new SharedDataPreferenceController(context));
 
         return controllers;
     }
diff --git a/src/com/android/settings/development/storage/BlobInfoListView.java b/src/com/android/settings/development/storage/BlobInfoListView.java
new file mode 100644
index 0000000..427e37f
--- /dev/null
+++ b/src/com/android/settings/development/storage/BlobInfoListView.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2020 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.settings.development.storage;
+
+import android.app.ListActivity;
+import android.app.blob.BlobInfo;
+import android.app.blob.BlobStoreManager;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.appcompat.app.AlertDialog;
+
+import com.android.internal.util.CollectionUtils;
+import com.android.settings.R;
+
+import java.io.IOException;
+import java.util.List;
+
+// TODO: have this class extend DashboardFragment for consistency
+public class BlobInfoListView extends ListActivity {
+    private static final String TAG = "BlobInfoListView";
+
+    private Context mContext;
+    private BlobStoreManager mBlobStoreManager;
+    private BlobListAdapter mAdapter;
+    private LayoutInflater mInflater;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mContext = this;
+
+        mBlobStoreManager = (BlobStoreManager) getSystemService(BlobStoreManager.class);
+        mInflater = (LayoutInflater) getSystemService(LayoutInflater.class);
+
+        mAdapter = new BlobListAdapter(this);
+        setListAdapter(mAdapter);
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        queryBlobsAndUpdateList();
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        super.onActivityResult(requestCode, resultCode, data);
+        if (requestCode == SharedDataUtils.LEASE_VIEW_REQUEST_CODE
+                && resultCode == SharedDataUtils.LEASE_VIEW_RESULT_CODE_FAILURE) {
+            Toast.makeText(this, R.string.shared_data_delete_failure_text, Toast.LENGTH_LONG)
+                    .show();
+        }
+        // do nothing on LEASE_VIEW_RESULT_CODE_SUCCESS since data is updated in onResume()
+    }
+
+    @Override
+    protected void onListItemClick(ListView l, View v, int position, long id) {
+        final BlobInfo blob = mAdapter.getItem(position);
+        if (CollectionUtils.isEmpty(blob.getLeases())) {
+            showDeleteBlobDialog(blob);
+        } else {
+            final Intent intent = new Intent(this, LeaseInfoListView.class);
+            intent.putExtra(SharedDataUtils.BLOB_KEY, blob);
+            startActivityForResult(intent, SharedDataUtils.LEASE_VIEW_REQUEST_CODE);
+        }
+    }
+
+    private View getEmptyView() {
+        final View emptyView = mInflater.inflate(R.layout.shared_data_empty_list_view,
+                (ViewGroup) getListView().getRootView());
+        final TextView emptyText = emptyView.findViewById(R.id.empty_view_text);
+        emptyText.setText(R.string.shared_data_no_blobs_text);
+        return emptyView;
+    }
+
+    private void showDeleteBlobDialog(BlobInfo blob) {
+        final AlertDialog dialog = new AlertDialog.Builder(mContext)
+                .setMessage(R.string.shared_data_no_accessors_dialog_text)
+                .setPositiveButton(android.R.string.ok, getDialogOnClickListener(blob))
+                .setNegativeButton(android.R.string.cancel, null)
+                .create();
+        dialog.show();
+    }
+
+    private DialogInterface.OnClickListener getDialogOnClickListener(BlobInfo blob) {
+        return (dialog, which) -> {
+            try {
+                mBlobStoreManager.deleteBlob(blob);
+            } catch (IOException e) {
+                Log.e(TAG, "Unable to delete blob: " + e.getMessage());
+                Toast.makeText(this, R.string.shared_data_delete_failure_text, Toast.LENGTH_LONG)
+                        .show();
+            }
+            queryBlobsAndUpdateList();
+        };
+    }
+
+    private void queryBlobsAndUpdateList() {
+        try {
+            mAdapter.updateList(mBlobStoreManager.queryBlobsForUser(UserHandle.CURRENT));
+        } catch (IOException e) {
+            Log.e(TAG, "Unable to fetch blobs for current user: " + e.getMessage());
+            Toast.makeText(this, R.string.shared_data_query_failure_text, Toast.LENGTH_LONG).show();
+            finish();
+        }
+    }
+
+    private class BlobListAdapter extends ArrayAdapter<BlobInfo> {
+        BlobListAdapter(Context context) {
+            super(context, 0);
+        }
+
+        void updateList(List<BlobInfo> blobs) {
+            clear();
+            if (blobs.isEmpty()) {
+                getListView().setEmptyView(getEmptyView());
+            } else {
+                addAll(blobs);
+            }
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            final BlobInfoViewHolder holder = BlobInfoViewHolder.createOrRecycle(
+                    mInflater, convertView);
+            convertView = holder.rootView;
+
+            final BlobInfo blob = getItem(position);
+            holder.blobLabel.setText(blob.getLabel());
+            holder.blobId.setText(getString(R.string.blob_id_text, blob.getId()));
+            holder.blobExpiry.setText(getString(R.string.blob_expires_text,
+                    SharedDataUtils.formatTime(blob.getExpiryTimeMs())));
+            return convertView;
+        }
+    }
+}
diff --git a/src/com/android/settings/development/storage/BlobInfoViewHolder.java b/src/com/android/settings/development/storage/BlobInfoViewHolder.java
new file mode 100644
index 0000000..de8c9a9
--- /dev/null
+++ b/src/com/android/settings/development/storage/BlobInfoViewHolder.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2020 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.settings.development.storage;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.TextView;
+
+import com.android.settings.R;
+
+/**
+ * View holder for {@link BlobInfoListView}.
+ */
+class BlobInfoViewHolder {
+    View rootView;
+    TextView blobLabel;
+    TextView blobId;
+    TextView blobExpiry;
+
+    static BlobInfoViewHolder createOrRecycle(LayoutInflater inflater, View convertView) {
+        if (convertView != null) {
+            return (BlobInfoViewHolder) convertView.getTag();
+        }
+        convertView = inflater.inflate(R.layout.blob_list_item_view, null);
+
+        final BlobInfoViewHolder holder = new BlobInfoViewHolder();
+        holder.rootView = convertView;
+        holder.blobLabel = convertView.findViewById(R.id.blob_label);
+        holder.blobId = convertView.findViewById(R.id.blob_id);
+        holder.blobExpiry = convertView.findViewById(R.id.blob_expiry);
+        convertView.setTag(holder);
+        return holder;
+    }
+}
diff --git a/src/com/android/settings/development/storage/LeaseInfoListView.java b/src/com/android/settings/development/storage/LeaseInfoListView.java
new file mode 100644
index 0000000..b9a3042
--- /dev/null
+++ b/src/com/android/settings/development/storage/LeaseInfoListView.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2020 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.settings.development.storage;
+
+import android.app.ListActivity;
+import android.app.blob.BlobInfo;
+import android.app.blob.BlobStoreManager;
+import android.app.blob.LeaseInfo;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.Resources;
+import android.graphics.Typeface;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AlertDialog;
+
+import com.android.internal.util.CollectionUtils;
+import com.android.settings.R;
+
+import java.io.IOException;
+import java.util.List;
+
+// TODO: have this class extend DashboardFragment for consistency
+public class LeaseInfoListView extends ListActivity {
+    private static final String TAG = "LeaseInfoListView";
+
+    private Context mContext;
+    private BlobStoreManager mBlobStoreManager;
+    private BlobInfo mBlobInfo;
+    private LeaseListAdapter mAdapter;
+    private LayoutInflater mInflater;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mContext = this;
+        mBlobStoreManager = (BlobStoreManager) getSystemService(BlobStoreManager.class);
+        mInflater = (LayoutInflater) getSystemService(LayoutInflater.class);
+
+        mBlobInfo = getIntent().getParcelableExtra(SharedDataUtils.BLOB_KEY);
+
+        mAdapter = new LeaseListAdapter(this);
+        if (mAdapter.isEmpty()) {
+            // this should never happen since we're checking the size in BlobInfoListView
+            Log.e(TAG, "Error fetching leases for shared data: " + mBlobInfo.toString());
+            finish();
+        }
+
+        setListAdapter(mAdapter);
+        getListView().addHeaderView(getHeaderView());
+        getListView().addFooterView(getFooterView());
+        getListView().setClickable(false);
+    }
+
+    private LinearLayout getHeaderView() {
+        final LinearLayout headerView = (LinearLayout) mInflater.inflate(
+                R.layout.blob_list_item_view , null);
+        final TextView blobLabel = headerView.findViewById(R.id.blob_label);
+        final TextView blobId = headerView.findViewById(R.id.blob_id);
+        final TextView blobExpiry = headerView.findViewById(R.id.blob_expiry);
+
+        blobLabel.setText(mBlobInfo.getLabel());
+        blobLabel.setTypeface(Typeface.DEFAULT_BOLD);
+        blobId.setText(getString(R.string.blob_id_text, mBlobInfo.getId()));
+        blobExpiry.setVisibility(View.GONE);
+        return headerView;
+    }
+
+    private Button getFooterView() {
+        final Button deleteButton = new Button(this);
+        deleteButton.setLayoutParams(
+                new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
+        deleteButton.setText(R.string.delete_blob_text);
+        deleteButton.setOnClickListener(getButtonOnClickListener());
+        return deleteButton;
+    }
+
+    private View.OnClickListener getButtonOnClickListener() {
+        return v -> {
+            final AlertDialog dialog = new AlertDialog.Builder(mContext)
+                    .setMessage(R.string.delete_blob_confirmation_text)
+                    .setPositiveButton(android.R.string.ok, getDialogOnClickListener())
+                    .setNegativeButton(android.R.string.cancel, null)
+                    .create();
+            dialog.show();
+        };
+    }
+
+    private DialogInterface.OnClickListener getDialogOnClickListener() {
+        return (dialog, which) -> {
+            try {
+                mBlobStoreManager.deleteBlob(mBlobInfo);
+                setResult(SharedDataUtils.LEASE_VIEW_RESULT_CODE_SUCCESS);
+            } catch (IOException e) {
+                Log.e(TAG, "Unable to delete blob: " + e.getMessage());
+                setResult(SharedDataUtils.LEASE_VIEW_RESULT_CODE_FAILURE);
+            }
+            finish();
+        };
+    }
+
+    private class LeaseListAdapter extends ArrayAdapter<LeaseInfo> {
+        LeaseListAdapter(Context context) {
+            super(context, 0);
+
+            final List<LeaseInfo> leases = mBlobInfo.getLeases();
+            if (CollectionUtils.isEmpty(leases)) {
+                return;
+            }
+            addAll(leases);
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            final LeaseInfoViewHolder holder = LeaseInfoViewHolder.createOrRecycle(
+                    mInflater, convertView);
+            convertView = holder.rootView;
+
+            final LeaseInfo lease = getItem(position);
+            holder.leasePackageName.setText(lease.getPackageName());
+            holder.leaseDescription.setText(getDescriptionString(lease));
+            holder.leaseExpiry.setText(getString(R.string.accessor_expires_text,
+                    SharedDataUtils.formatTime(lease.getExpiryTimeMillis())));
+            return convertView;
+        }
+
+        private String getDescriptionString(LeaseInfo lease) {
+            String description = null;
+            try {
+                description = getString(lease.getDescriptionResId());
+            } catch (Resources.NotFoundException ignored) {
+                if (lease.getDescription() != null) {
+                    description = lease.getDescription().toString();
+                }
+            } finally {
+                if (TextUtils.isEmpty(description)) {
+                    description = getString(R.string.accessor_no_description_text);
+                }
+            }
+            return description;
+        }
+    }
+}
diff --git a/src/com/android/settings/development/storage/LeaseInfoViewHolder.java b/src/com/android/settings/development/storage/LeaseInfoViewHolder.java
new file mode 100644
index 0000000..d74c929
--- /dev/null
+++ b/src/com/android/settings/development/storage/LeaseInfoViewHolder.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2020 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.settings.development.storage;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.TextView;
+
+import com.android.settings.R;
+
+/**
+ * View holder for {@link LeaseInfoListView}.
+ */
+class LeaseInfoViewHolder {
+    View rootView;
+    TextView leasePackageName;
+    TextView leaseDescription;
+    TextView leaseExpiry;
+
+    static LeaseInfoViewHolder createOrRecycle(LayoutInflater inflater, View convertView) {
+        if (convertView != null) {
+            return (LeaseInfoViewHolder) convertView.getTag();
+        }
+        convertView = inflater.inflate(R.layout.lease_list_item_view, null);
+
+        final LeaseInfoViewHolder holder = new LeaseInfoViewHolder();
+        holder.rootView = convertView;
+        holder.leasePackageName = convertView.findViewById(R.id.lease_package);
+        holder.leaseDescription = convertView.findViewById(R.id.lease_desc);
+        holder.leaseExpiry = convertView.findViewById(R.id.lease_expiry);
+        convertView.setTag(holder);
+        return holder;
+    }
+}
diff --git a/src/com/android/settings/development/storage/SharedDataPreferenceController.java b/src/com/android/settings/development/storage/SharedDataPreferenceController.java
new file mode 100644
index 0000000..1d5c3e4
--- /dev/null
+++ b/src/com/android/settings/development/storage/SharedDataPreferenceController.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2020 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.settings.development.storage;
+
+import android.app.blob.BlobStoreManager;
+import android.content.Context;
+
+import androidx.preference.Preference;
+
+import com.android.settingslib.development.DeveloperOptionsPreferenceController;
+
+public class SharedDataPreferenceController extends DeveloperOptionsPreferenceController {
+
+    private static final String SHARED_DATA = "shared_data";
+
+    private BlobStoreManager mBlobStoreManager;
+
+    public SharedDataPreferenceController(Context context) {
+        super(context);
+        mBlobStoreManager = (BlobStoreManager) context.getSystemService(BlobStoreManager.class);
+    }
+
+    @Override
+    public String getPreferenceKey() {
+        return SHARED_DATA;
+    }
+
+    @Override
+    public boolean isAvailable() {
+        return mBlobStoreManager != null;
+    }
+
+    @Override
+    public void updateState(Preference preference) {
+        preference.setEnabled(mBlobStoreManager != null);
+        // TODO: update summary to indicate why this preference isn't available
+    }
+}
diff --git a/src/com/android/settings/development/storage/SharedDataUtils.java b/src/com/android/settings/development/storage/SharedDataUtils.java
new file mode 100644
index 0000000..2f48f6d
--- /dev/null
+++ b/src/com/android/settings/development/storage/SharedDataUtils.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2020 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.settings.development.storage;
+
+import android.icu.text.SimpleDateFormat;
+import android.icu.util.Calendar;
+import android.icu.util.TimeZone;
+
+import java.util.Locale;
+
+class SharedDataUtils {
+    static final String BLOB_KEY = "BLOB_KEY";
+
+    static final int LEASE_VIEW_REQUEST_CODE = 8108;
+    static final int LEASE_VIEW_RESULT_CODE_SUCCESS = 1;
+    static final int LEASE_VIEW_RESULT_CODE_FAILURE = -1;
+
+    private static final String BLOB_EXPIRY_PATTERN = "MMM dd, yyyy HH:mm:ss z";
+
+    private static final SimpleDateFormat FORMATTER = new SimpleDateFormat(BLOB_EXPIRY_PATTERN);
+    private static final Calendar CALENDAR = Calendar.getInstance(
+            TimeZone.getDefault(), Locale.getDefault());
+
+    static String formatTime(long millis) {
+        CALENDAR.setTimeInMillis(millis);
+        return FORMATTER.format(CALENDAR.getTime());
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/development/storage/SharedDataPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/development/storage/SharedDataPreferenceControllerTest.java
new file mode 100644
index 0000000..86bb4a7
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/development/storage/SharedDataPreferenceControllerTest.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2020 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.settings.development.storage;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.blob.BlobStoreManager;
+import android.content.Context;
+
+import androidx.preference.Preference;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.R;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.util.ReflectionHelpers;
+
+// TODO: add more detailed tests for the shared data screens
+@RunWith(RobolectricTestRunner.class)
+public class SharedDataPreferenceControllerTest {
+
+    @Mock
+    private Context mContext;
+    @Mock
+    private BlobStoreManager mBlobStoreManager;
+    @Mock
+    private Preference mPreference;
+    @Mock
+    private PreferenceScreen mScreen;
+
+    private SharedDataPreferenceController mController;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mController = spy(new SharedDataPreferenceController(mContext));
+
+        when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn(mPreference);
+        mController.displayPreference(mScreen);
+    }
+
+    @Test
+    public void updateState_BlobManagerIsNotNull_preferenceIsEnabled() {
+        ReflectionHelpers.setField(mController, "mBlobStoreManager", mBlobStoreManager);
+        mController.updateState(mPreference);
+
+        verify(mPreference).setEnabled(true);
+        assertThat(mPreference.getSummary())
+                .isEqualTo(mContext.getString(R.string.shared_data_summary));
+    }
+
+    @Test
+    public void updateState_BlobManagerIsNull_preferenceIsDisabled() {
+        ReflectionHelpers.setField(mController, "mBlobStoreManager", null);
+        mController.updateState(mPreference);
+
+        verify(mPreference).setEnabled(false);
+    }
+}