Merge "Introduce Permission list for app_streaming and computer" into tm-dev

This commit is contained in:
Evan Chen
2022-03-07 23:45:47 +00:00
committed by Android (Google) Code Review
10 changed files with 345 additions and 35 deletions

View File

@@ -0,0 +1,34 @@
<!--
~ Copyright (C) 2022 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group android:scaleX="5.142857"
android:scaleY="5.142857"
android:translateY="2.5714285">
<path
android:pathData="M13.1235,1.01L5.1235,1C4.0235,1 3.1235,1.9 3.1235,3V17C3.1235,18.1 4.0235,19 5.1235,19H13.1235C14.2235,19 15.1235,18.1 15.1235,17V12.9765H13.1235V14H5.1235V6H13.1235V6.0034H15.1235V3C15.1235,1.9 14.2235,1.01 13.1235,1.01ZM13.1235,17H5.1235V16H13.1235V17ZM5.1235,4V3H13.1235V4H5.1235Z"
android:fillColor="#202124"
android:fillType="evenOdd"/>
<path
android:pathData="M10.6984,6.4016V13.3136L12.3596,12.3628H17.8731V6.4016H10.6984ZM16.3731,7.9016H12.1984V10.8628H16.3731V7.9016Z"
android:fillColor="#202124"
android:fillType="evenOdd"/>
</group>
</vector>

View File

@@ -0,0 +1,25 @@
<!--
~ Copyright (C) 2022 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path android:fillColor="@android:color/white"
android:pathData="M4,19V17H6V10Q6,7.925 7.25,6.312Q8.5,4.7 10.5,4.2V3.5Q10.5,2.875 10.938,2.438Q11.375,2 12,2Q12.625,2 13.062,2.438Q13.5,2.875 13.5,3.5V4.2Q15.5,4.7 16.75,6.312Q18,7.925 18,10V17H20V19ZM12,11.5Q12,11.5 12,11.5Q12,11.5 12,11.5Q12,11.5 12,11.5Q12,11.5 12,11.5ZM12,22Q11.175,22 10.588,21.413Q10,20.825 10,20H14Q14,20.825 13.413,21.413Q12.825,22 12,22ZM8,17H16V10Q16,8.35 14.825,7.175Q13.65,6 12,6Q10.35,6 9.175,7.175Q8,8.35 8,10Z"/>
</vector>

View File

@@ -0,0 +1,25 @@
<!--
~ Copyright (C) 2022 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path android:fillColor="@android:color/white"
android:pathData="M6,17H18L14.25,12L11.25,16L9,13ZM5,21Q4.175,21 3.587,20.413Q3,19.825 3,19V5Q3,4.175 3.587,3.587Q4.175,3 5,3H19Q19.825,3 20.413,3.587Q21,4.175 21,5V19Q21,19.825 20.413,20.413Q19.825,21 19,21ZM5,19H19Q19,19 19,19Q19,19 19,19V5Q19,5 19,5Q19,5 19,5H5Q5,5 5,5Q5,5 5,5V19Q5,19 5,19Q5,19 5,19ZM5,5Q5,5 5,5Q5,5 5,5V19Q5,19 5,19Q5,19 5,19Q5,19 5,19Q5,19 5,19V5Q5,5 5,5Q5,5 5,5Z"/>
</vector>

View File

@@ -51,6 +51,11 @@
android:scrollbars="vertical"
android:layout_height="200dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/permission_list"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</RelativeLayout>
<LinearLayout

View File

@@ -0,0 +1,55 @@
<!--
~ Copyright (C) 2022 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:id="@+id/list_item_permission"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="5dp">
<ImageView
android:id="@+id/permission_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginTop="7dp"
android:layout_marginEnd="12dp"
android:contentDescription="Permission Icon"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_vertical"
android:padding="6dp">
<TextView
android:id="@+id/permission_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textAppearance="?android:attr/textAppearanceListItemSmall"/>
<TextView
android:id="@+id/permission_summary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"/>
</LinearLayout>
</LinearLayout>

View File

@@ -38,6 +38,12 @@
<!-- ================= DEVICE_PROFILE_APP_STREAMING ================= -->
<!-- Apps permission will be granted of APP_STREAMING profile [CHAR LIMIT=30] -->
<string name="permission_apps">Apps</string>
<!-- Description of apps permission of APP_STREAMING profile [CHAR LIMIT=NONE] -->
<string name="permission_apps_summary">Stream your phone\u2019s apps</string>
<!-- Confirmation for associating an application with a companion device of APP_STREAMING profile (type) [CHAR LIMIT=NONE] -->
<string name="title_app_streaming">Allow &lt;strong&gt;<xliff:g id="app_name" example="Exo">%1$s</xliff:g>&lt;/strong&gt; to access this information for your phone</string>
@@ -72,6 +78,18 @@
<!-- Description of the privileges the application will get if associated with the companion device of COMPUTER profile (type) [CHAR LIMIT=NONE] -->
<string name="summary_computer"></string>
<!-- Notification permission will be granted of COMPUTER profile [CHAR LIMIT=30] -->
<string name="permission_notification">Notifications</string>
<!-- Description of notification permission of COMPUTER profile [CHAR LIMIT=NONE] -->
<string name="permission_notification_summary">Can read all notifications, including information like contracts, messages, and photos</string>
<!-- Storage permission will be granted of COMPUTER profile [CHAR LIMIT=30] -->
<string name="permission_storage">Photos and media</string>
<!-- Description of storage permission of COMPUTER profile [CHAR LIMIT=NONE] -->
<string name="permission_storage_summary"></string>
<!-- Title of the helper dialog for COMPUTER profile [CHAR LIMIT=30]. -->
<string name="helper_title_computer">Google Play services</string>

View File

@@ -24,6 +24,9 @@ import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTE
import static com.android.companiondevicemanager.CompanionDeviceDiscoveryService.DiscoveryState;
import static com.android.companiondevicemanager.CompanionDeviceDiscoveryService.DiscoveryState.FINISHED_TIMEOUT;
import static com.android.companiondevicemanager.PermissionListAdapter.TYPE_APPS;
import static com.android.companiondevicemanager.PermissionListAdapter.TYPE_NOTIFICATION;
import static com.android.companiondevicemanager.PermissionListAdapter.TYPE_STORAGE;
import static com.android.companiondevicemanager.Utils.getApplicationLabel;
import static com.android.companiondevicemanager.Utils.getHtmlFromResources;
import static com.android.companiondevicemanager.Utils.getVendorHeaderIcon;
@@ -61,6 +64,7 @@ import androidx.fragment.app.FragmentManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
/**
@@ -122,8 +126,12 @@ public class CompanionDeviceActivity extends FragmentActivity implements
// The recycler view is only shown for multiple-device regular association request, after
// at least one matching device is found.
private @Nullable RecyclerView mRecyclerView;
private @Nullable DeviceListAdapter mAdapter;
private @Nullable RecyclerView mDeviceListRecyclerView;
private @Nullable DeviceListAdapter mDeviceAdapter;
// The recycler view is only shown for selfManaged association request.
private @Nullable RecyclerView mPermissionListRecyclerView;
private @Nullable PermissionListAdapter mPermissionListAdapter;
// The flag used to prevent double taps, that may lead to sending several requests for creating
// an association to CDM.
@@ -133,6 +141,8 @@ public class CompanionDeviceActivity extends FragmentActivity implements
// onActivityResult() after the association is created.
private @Nullable DeviceFilterPair<?> mSelectedDevice;
private @Nullable List<Integer> mPermissionTypes;
@Override
public void onCreate(Bundle savedInstanceState) {
if (DEBUG) Log.d(TAG, "onCreate()");
@@ -242,8 +252,11 @@ public class CompanionDeviceActivity extends FragmentActivity implements
mVendorHeaderName = findViewById(R.id.vendor_header_name);
mVendorHeaderButton = findViewById(R.id.vendor_header_button);
mRecyclerView = findViewById(R.id.device_list);
mAdapter = new DeviceListAdapter(this, this::onListItemClick);
mDeviceListRecyclerView = findViewById(R.id.device_list);
mDeviceAdapter = new DeviceListAdapter(this, this::onListItemClick);
mPermissionListRecyclerView = findViewById(R.id.permission_list);
mPermissionListAdapter = new PermissionListAdapter(this);
mButtonAllow = findViewById(R.id.btn_positive);
mButtonNotAllow = findViewById(R.id.btn_negative);
@@ -359,7 +372,8 @@ public class CompanionDeviceActivity extends FragmentActivity implements
final Drawable vendorIcon;
final CharSequence vendorName;
final Spanned title;
final Spanned summary;
mPermissionTypes = new ArrayList<>();
try {
vendorIcon = getVendorHeaderIcon(this, packageName, userId);
@@ -372,33 +386,36 @@ public class CompanionDeviceActivity extends FragmentActivity implements
switch (deviceProfile) {
case DEVICE_PROFILE_APP_STREAMING:
title = getHtmlFromResources(this, R.string.title_app_streaming, appLabel);
summary = getHtmlFromResources(
this, R.string.summary_app_streaming, appLabel, deviceName);
title = getHtmlFromResources(this, R.string.title_app_streaming, deviceName);
mPermissionTypes.add(TYPE_APPS);
break;
case DEVICE_PROFILE_AUTOMOTIVE_PROJECTION:
title = getHtmlFromResources(this, R.string.title_automotive_projection, appLabel);
summary = getHtmlFromResources(
this, R.string.summary_automotive_projection, appLabel, deviceName);
title = getHtmlFromResources(
this, R.string.title_automotive_projection, deviceName);
break;
case DEVICE_PROFILE_COMPUTER:
title = getHtmlFromResources(this, R.string.title_computer, appLabel);
summary = getHtmlFromResources(
this, R.string.summary_computer, appLabel, deviceName);
title = getHtmlFromResources(this, R.string.title_computer, deviceName);
mPermissionTypes.add(TYPE_NOTIFICATION);
mPermissionTypes.add(TYPE_STORAGE);
break;
default:
throw new RuntimeException("Unsupported profile " + deviceProfile);
}
mSummary.setVisibility(View.GONE);
mPermissionListAdapter.setPermissionType(mPermissionTypes);
mPermissionListRecyclerView.setAdapter(mPermissionListAdapter);
mPermissionListRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mTitle.setText(title);
mSummary.setText(summary);
mVendorHeaderImage.setImageDrawable(vendorIcon);
mVendorHeaderName.setText(vendorName);
mRecyclerView.setVisibility(View.GONE);
mDeviceListRecyclerView.setVisibility(View.GONE);
mVendorHeader.setVisibility(View.VISIBLE);
}
@@ -411,7 +428,8 @@ public class CompanionDeviceActivity extends FragmentActivity implements
deviceFilterPairs -> updateSingleDeviceUi(
deviceFilterPairs, deviceProfile, appLabel));
mRecyclerView.setVisibility(View.GONE);
mPermissionListRecyclerView.setVisibility(View.GONE);
mDeviceListRecyclerView.setVisibility(View.GONE);
}
private void updateSingleDeviceUi(List<DeviceFilterPair<?>> deviceFilterPairs,
@@ -460,15 +478,15 @@ public class CompanionDeviceActivity extends FragmentActivity implements
mTitle.setText(title);
mSummary.setText(summary);
mAdapter = new DeviceListAdapter(this, this::onListItemClick);
mDeviceAdapter = new DeviceListAdapter(this, this::onListItemClick);
// TODO: hide the list and show a spinner until a first device matching device is found.
mRecyclerView.setAdapter(mAdapter);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mDeviceListRecyclerView.setAdapter(mDeviceAdapter);
mDeviceListRecyclerView.setLayoutManager(new LinearLayoutManager(this));
CompanionDeviceDiscoveryService.getScanResult().observe(
/* lifecycleOwner */ this,
/* observer */ mAdapter);
/* observer */ mDeviceAdapter);
// "Remove" consent button: users would need to click on the list item.
mButtonAllow.setVisibility(View.GONE);
@@ -477,14 +495,14 @@ public class CompanionDeviceActivity extends FragmentActivity implements
private void onListItemClick(int position) {
if (DEBUG) Log.d(TAG, "onListItemClick() " + position);
final DeviceFilterPair<?> selectedDevice = mAdapter.getItem(position);
final DeviceFilterPair<?> selectedDevice = mDeviceAdapter.getItem(position);
if (mSelectedDevice != null) {
if (DEBUG) Log.w(TAG, "Already selected.");
return;
}
// Notify the adapter to highlight the selected item.
mAdapter.setSelectedPosition(position);
mDeviceAdapter.setSelectedPosition(position);
mSelectedDevice = requireNonNull(selectedDevice);

View File

@@ -15,9 +15,10 @@
*/
package com.android.companiondevicemanager;
import static com.android.companiondevicemanager.Utils.getIcon;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -60,11 +61,11 @@ class DeviceListAdapter extends RecyclerView.Adapter<DeviceListAdapter.ViewHolde
R.layout.list_item_device, parent, false);
ViewHolder viewHolder = new ViewHolder(view);
if (viewType == TYPE_WIFI) {
viewHolder.mImageView.setImageDrawable(getIcon(
com.android.internal.R.drawable.ic_wifi_signal_3));
viewHolder.mImageView.setImageDrawable(
getIcon(mContext, com.android.internal.R.drawable.ic_wifi_signal_3));
} else {
viewHolder.mImageView.setImageDrawable(getIcon(
android.R.drawable.stat_sys_data_bluetooth));
viewHolder.mImageView.setImageDrawable(
getIcon(mContext, android.R.drawable.stat_sys_data_bluetooth));
}
return viewHolder;
}
@@ -115,12 +116,6 @@ class DeviceListAdapter extends RecyclerView.Adapter<DeviceListAdapter.ViewHolde
return mDevices.get(position).getDevice() instanceof android.net.wifi.ScanResult;
}
private Drawable getIcon(int resId) {
Drawable icon = mContext.getResources().getDrawable(resId, null);
icon.setTint(Color.DKGRAY);
return icon;
}
public interface OnItemClickListener {
void onItemClick(int position);
}

View File

@@ -0,0 +1,128 @@
/*
* Copyright (C) 2022 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.companiondevicemanager;
import static com.android.companiondevicemanager.Utils.getHtmlFromResources;
import static com.android.companiondevicemanager.Utils.getIcon;
import static java.util.Collections.unmodifiableMap;
import android.content.Context;
import android.text.Spanned;
import android.util.ArrayMap;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
import java.util.Map;
class PermissionListAdapter extends RecyclerView.Adapter<PermissionListAdapter.ViewHolder> {
private final Context mContext;
private List<Integer> mPermissions;
static final int TYPE_NOTIFICATION = 0;
static final int TYPE_STORAGE = 1;
static final int TYPE_APPS = 2;
private static final Map<Integer, Integer> sTitleMap;
static {
final Map<Integer, Integer> map = new ArrayMap<>();
map.put(TYPE_NOTIFICATION, R.string.permission_notification);
map.put(TYPE_STORAGE, R.string.permission_storage);
map.put(TYPE_APPS, R.string.permission_apps);
sTitleMap = unmodifiableMap(map);
}
private static final Map<Integer, Integer> sSummaryMap;
static {
final Map<Integer, Integer> map = new ArrayMap<>();
map.put(TYPE_NOTIFICATION, R.string.permission_notification_summary);
map.put(TYPE_STORAGE, R.string.permission_storage_summary);
map.put(TYPE_APPS, R.string.permission_apps_summary);
sSummaryMap = unmodifiableMap(map);
}
private static final Map<Integer, Integer> sIconMap;
static {
final Map<Integer, Integer> map = new ArrayMap<>();
map.put(TYPE_NOTIFICATION, R.drawable.ic_notifications);
map.put(TYPE_STORAGE, R.drawable.ic_storage);
map.put(TYPE_APPS, R.drawable.ic_apps);
sIconMap = unmodifiableMap(map);
}
PermissionListAdapter(Context context) {
mContext = context;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(
R.layout.list_item_permission, parent, false);
ViewHolder viewHolder = new ViewHolder(view);
viewHolder.mPermissionIcon.setImageDrawable(getIcon(mContext, sIconMap.get(viewType)));
return viewHolder;
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
int type = getItemViewType(position);
final Spanned title = getHtmlFromResources(mContext, sTitleMap.get(type));
final Spanned summary = getHtmlFromResources(mContext, sSummaryMap.get(type));
holder.mPermissionName.setText(title);
holder.mPermissionSummary.setText(summary);
}
@Override
public int getItemViewType(int position) {
return mPermissions.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public int getItemCount() {
return mPermissions != null ? mPermissions.size() : 0;
}
static class ViewHolder extends RecyclerView.ViewHolder {
private final TextView mPermissionName;
private final TextView mPermissionSummary;
private final ImageView mPermissionIcon;
ViewHolder(View itemView) {
super(itemView);
mPermissionName = itemView.findViewById(R.id.permission_name);
mPermissionSummary = itemView.findViewById(R.id.permission_summary);
mPermissionIcon = itemView.findViewById(R.id.permission_icon);
}
}
void setPermissionType(List<Integer> permissions) {
mPermissions = permissions;
}
}

View File

@@ -22,6 +22,7 @@ import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.ApplicationInfoFlags;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
@@ -121,6 +122,12 @@ class Utils {
return appInfo;
}
static @NonNull Drawable getIcon(@NonNull Context context, int resId) {
Drawable icon = context.getResources().getDrawable(resId, null);
icon.setTint(Color.DKGRAY);
return icon;
}
static void runOnMainThread(Runnable runnable) {
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
runnable.run();