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);
+ }
+}