Merge "Make CDM UI more test(CTS)-friendly"

This commit is contained in:
TreeHugger Robot
2021-12-22 17:16:16 +00:00
committed by Android (Google) Code Review
5 changed files with 188 additions and 106 deletions

View File

@@ -14,6 +14,7 @@
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_confirmation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/dialog_background"
@@ -23,6 +24,8 @@
android:padding="18dp"
android:layout_gravity="center">
<!-- Do NOT change the ID of the root LinearLayout above: it's referenced in CTS tests. -->
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
@@ -30,7 +33,6 @@
android:gravity="center"
android:paddingHorizontal="12dp"
style="@*android:style/TextAppearance.Widget.Toolbar.Title"/>
<!-- style="@*android:style/TextAppearance.Widget.Toolbar.Title" -->
<TextView
android:id="@+id/summary"
@@ -61,8 +63,10 @@
android:orientation="horizontal"
android:gravity="end">
<!-- Do NOT change the IDs of the buttons: they are referenced in CTS tests. -->
<Button
android:id="@+id/button_cancel"
android:id="@+id/btn_negative"
style="@android:style/Widget.Material.Button.Borderless.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -70,7 +74,7 @@
android:textColor="?android:attr/textColorSecondary" />
<Button
android:id="@+id/button_allow"
android:id="@+id/btn_positive"
style="@android:style/Widget.Material.Button.Borderless.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2021 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_device"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="12dp">
<!-- Do NOT change the ID of the root LinearLayout above: it's referenced in CTS tests. -->
<ImageView
android:id="@android:id/icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginRight="12dp"/>
<TextView
android:id="@android:id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceListItemSmall"/>
</LinearLayout>

View File

@@ -21,6 +21,8 @@ import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PRO
import static android.companion.AssociationRequest.DEVICE_PROFILE_WATCH;
import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
import static com.android.companiondevicemanager.CompanionDeviceDiscoveryService.SCAN_RESULTS_OBSERVABLE;
import static com.android.companiondevicemanager.CompanionDeviceDiscoveryService.TIMEOUT_OBSERVABLE;
import static com.android.companiondevicemanager.Utils.getApplicationLabel;
import static com.android.companiondevicemanager.Utils.getHtmlFromResources;
import static com.android.companiondevicemanager.Utils.prepareResultReceiverForIpc;
@@ -91,10 +93,13 @@ public class CompanionDeviceActivity extends Activity {
// The flag used to prevent double taps, that may lead to sending several requests for creating
// an association to CDM.
private boolean mAssociationApproved;
private boolean mApproved;
private boolean mCancelled;
@Override
public void onCreate(Bundle savedInstanceState) {
if (DEBUG) Log.d(TAG, "onCreate()");
super.onCreate(savedInstanceState);
getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
}
@@ -117,25 +122,12 @@ public class CompanionDeviceActivity extends Activity {
// Start discovery services if needed.
if (!mRequest.isSelfManaged()) {
CompanionDeviceDiscoveryService.startForRequest(this, mRequest);
TIMEOUT_OBSERVABLE.addObserver((o, arg) -> cancel(true));
}
// Init UI.
initUI();
}
@Override
protected void onStop() {
super.onStop();
if (DEBUG) Log.d(TAG, "onStop(), finishing=" + isFinishing());
// TODO: handle config changes without cancelling.
if (!isFinishing()) {
cancel(); // will finish()
}
// mAdapter may be observing - need to remove it.
CompanionDeviceDiscoveryService.SCAN_RESULTS_OBSERVABLE.deleteObservers();
}
@Override
protected void onNewIntent(Intent intent) {
// Handle another incoming request (while we are not done with the original - mRequest -
@@ -153,6 +145,39 @@ public class CompanionDeviceActivity extends Activity {
}
}
@Override
protected void onStop() {
super.onStop();
if (DEBUG) Log.d(TAG, "onStop(), finishing=" + isFinishing());
// TODO: handle config changes without cancelling.
if (!isDone()) {
cancel(false); // will finish()
}
TIMEOUT_OBSERVABLE.deleteObservers();
// mAdapter may also be observing - need to remove it.
SCAN_RESULTS_OBSERVABLE.deleteObservers();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (DEBUG) Log.d(TAG, "onDestroy()");
}
@Override
public void onBackPressed() {
if (DEBUG) Log.d(TAG, "onBackPressed()");
super.onBackPressed();
}
@Override
public void finish() {
if (DEBUG) Log.d(TAG, "finish()", new Exception("Stack Trace Dump"));
super.finish();
}
private void initUI() {
if (DEBUG) Log.d(TAG, "initUI(), request=" + mRequest);
@@ -164,10 +189,9 @@ public class CompanionDeviceActivity extends Activity {
mListView = findViewById(R.id.device_list);
mListView.setOnItemClickListener((av, iv, position, id) -> onListItemClick(position));
mButtonAllow = findViewById(R.id.button_allow);
mButtonAllow.setOnClickListener(this::onAllowButtonClick);
findViewById(R.id.button_cancel).setOnClickListener(v -> cancel());
mButtonAllow = findViewById(R.id.btn_positive);
mButtonAllow.setOnClickListener(this::onPositiveButtonClick);
findViewById(R.id.btn_negative).setOnClickListener(this::onNegativeButtonClick);
final CharSequence appLabel = getApplicationLabel(this, mRequest.getPackageName());
if (mRequest.isSelfManaged()) {
@@ -179,6 +203,33 @@ public class CompanionDeviceActivity extends Activity {
}
}
private void onAssociationApproved(@Nullable MacAddress macAddress) {
if (isDone()) {
if (DEBUG) Log.w(TAG, "Already done: " + (mApproved ? "Approved" : "Cancelled"));
return;
}
mApproved = true;
if (DEBUG) Log.i(TAG, "onAssociationApproved() macAddress=" + macAddress);
if (!mRequest.isSelfManaged()) {
requireNonNull(macAddress);
CompanionDeviceDiscoveryService.stop(this);
}
final Bundle data = new Bundle();
data.putParcelable(EXTRA_ASSOCIATION_REQUEST, mRequest);
data.putBinder(EXTRA_APPLICATION_CALLBACK, mAppCallback.asBinder());
if (macAddress != null) {
data.putParcelable(EXTRA_MAC_ADDRESS, macAddress);
}
data.putParcelable(EXTRA_RESULT_RECEIVER,
prepareResultReceiverForIpc(mOnAssociationCreatedReceiver));
mCdmServiceReceiver.send(RESULT_CODE_ASSOCIATION_APPROVED, data);
}
private void onAssociationCreated(@NonNull AssociationInfo association) {
if (DEBUG) Log.i(TAG, "onAssociationCreated(), association=" + association);
@@ -186,17 +237,26 @@ public class CompanionDeviceActivity extends Activity {
setResultAndFinish(association);
}
private void cancel() {
if (DEBUG) Log.i(TAG, "cancel()");
private void cancel(boolean discoveryTimeout) {
if (DEBUG) {
Log.i(TAG, "cancel(), discoveryTimeout=" + discoveryTimeout,
new Exception("Stack Trace Dump"));
}
if (isDone()) {
if (DEBUG) Log.w(TAG, "Already done: " + (mApproved ? "Approved" : "Cancelled"));
return;
}
mCancelled = true;
// Stop discovery service if it was used.
if (!mRequest.isSelfManaged()) {
if (!mRequest.isSelfManaged() || discoveryTimeout) {
CompanionDeviceDiscoveryService.stop(this);
}
// First send callback to the app directly...
try {
mAppCallback.onFailure("Cancelled.");
mAppCallback.onFailure(discoveryTimeout ? "Timeout." : "Cancelled.");
} catch (RemoteException ignore) {
}
@@ -297,7 +357,7 @@ public class CompanionDeviceActivity extends Activity {
mSummary.setText(summary);
mAdapter = new DeviceListAdapter(this);
CompanionDeviceDiscoveryService.SCAN_RESULTS_OBSERVABLE.addObserver(mAdapter);
SCAN_RESULTS_OBSERVABLE.addObserver(mAdapter);
// TODO: hide the list and show a spinner until a first device matching device is found.
mListView.setAdapter(mAdapter);
@@ -313,8 +373,8 @@ public class CompanionDeviceActivity extends Activity {
onAssociationApproved(macAddress);
}
private void onAllowButtonClick(View v) {
if (DEBUG) Log.d(TAG, "onAllowButtonClick()");
private void onPositiveButtonClick(View v) {
if (DEBUG) Log.d(TAG, "on_Positive_ButtonClick()");
// Disable the button, to prevent more clicks.
v.setEnabled(false);
@@ -330,28 +390,17 @@ public class CompanionDeviceActivity extends Activity {
onAssociationApproved(macAddress);
}
private void onAssociationApproved(@Nullable MacAddress macAddress) {
if (mAssociationApproved) return;
mAssociationApproved = true;
private void onNegativeButtonClick(View v) {
if (DEBUG) Log.d(TAG, "on_Negative_ButtonClick()");
if (DEBUG) Log.i(TAG, "onAssociationApproved() macAddress=" + macAddress);
// Disable the button, to prevent more clicks.
v.setEnabled(false);
if (!mRequest.isSelfManaged()) {
requireNonNull(macAddress);
CompanionDeviceDiscoveryService.stop(this);
}
cancel(false);
}
final Bundle data = new Bundle();
data.putParcelable(EXTRA_ASSOCIATION_REQUEST, mRequest);
data.putBinder(EXTRA_APPLICATION_CALLBACK, mAppCallback.asBinder());
if (macAddress != null) {
data.putParcelable(EXTRA_MAC_ADDRESS, macAddress);
}
data.putParcelable(EXTRA_RESULT_RECEIVER,
prepareResultReceiverForIpc(mOnAssociationCreatedReceiver));
mCdmServiceReceiver.send(RESULT_CODE_ASSOCIATION_APPROVED, data);
private boolean isDone() {
return mApproved || mCancelled;
}
private final ResultReceiver mOnAssociationCreatedReceiver =

View File

@@ -22,6 +22,8 @@ import static com.android.internal.util.CollectionUtils.filter;
import static com.android.internal.util.CollectionUtils.find;
import static com.android.internal.util.CollectionUtils.map;
import static java.lang.Math.max;
import static java.lang.Math.min;
import static java.util.Objects.requireNonNull;
import android.annotation.MainThread;
@@ -50,6 +52,7 @@ import android.net.wifi.WifiManager;
import android.os.Handler;
import android.os.IBinder;
import android.os.Parcelable;
import android.os.SystemProperties;
import android.text.TextUtils;
import android.util.Log;
@@ -64,13 +67,17 @@ public class CompanionDeviceDiscoveryService extends Service {
private static final boolean DEBUG = false;
private static final String TAG = CompanionDeviceDiscoveryService.class.getSimpleName();
private static final String SYS_PROP_DEBUG_TIMEOUT = "debug.cdm.discovery_timeout";
private static final long TIMEOUT_DEFAULT = 20_000L; // 20 seconds
private static final long TIMEOUT_MIN = 1_000L; // 1 sec
private static final long TIMEOUT_MAX = 60_000L; // 1 min
private static final String ACTION_START_DISCOVERY =
"com.android.companiondevicemanager.action.START_DISCOVERY";
private static final String ACTION_STOP_DISCOVERY =
"com.android.companiondevicemanager.action.ACTION_STOP_DISCOVERY";
private static final String EXTRA_ASSOCIATION_REQUEST = "association_request";
private static final long SCAN_TIMEOUT = 20_000L; // 20 seconds
// TODO: replace with LiveData-s?
static final Observable TIMEOUT_OBSERVABLE = new MyObservable();
@@ -180,8 +187,7 @@ public class CompanionDeviceDiscoveryService extends Service {
// Start BLE scanning (if needed)
mBleScanCallback = startBleScanningIfNeeded(bleFilters, forceStartScanningAll);
// Schedule a time-out.
Handler.getMain().postDelayed(mTimeoutRunnable, SCAN_TIMEOUT);
scheduleTimeout();
}
@MainThread
@@ -338,6 +344,21 @@ public class CompanionDeviceDiscoveryService extends Service {
});
}
private void scheduleTimeout() {
long timeout = SystemProperties.getLong(SYS_PROP_DEBUG_TIMEOUT, -1);
if (timeout <= 0) {
// 0 or negative values indicate that the sysprop was never set or should be ignored.
timeout = TIMEOUT_DEFAULT;
} else {
timeout = min(timeout, TIMEOUT_MAX); // should be <= 1 min (TIMEOUT_MAX)
timeout = max(timeout, TIMEOUT_MIN); // should be >= 1 sec (TIMEOUT_MIN)
}
if (DEBUG) Log.d(TAG, "scheduleTimeout(), timeout=" + timeout);
Handler.getMain().postDelayed(mTimeoutRunnable, timeout);
}
private void timeout() {
if (DEBUG) Log.i(TAG, "timeout()");
stopDiscoveryAndFinish();

View File

@@ -16,18 +16,14 @@
package com.android.companiondevicemanager;
import android.annotation.ColorInt;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.List;
@@ -39,51 +35,12 @@ import java.util.Observer;
*/
class DeviceListAdapter extends BaseAdapter implements Observer {
private final Context mContext;
private final Resources mResources;
private final Drawable mBluetoothIcon;
private final Drawable mWifiIcon;
private final @ColorInt int mTextColor;
// List if pairs (display name, address)
private List<DeviceFilterPair<?>> mDevices;
DeviceListAdapter(Context context) {
mContext = context;
mResources = context.getResources();
mBluetoothIcon = getTintedIcon(mResources, android.R.drawable.stat_sys_data_bluetooth);
mWifiIcon = getTintedIcon(mResources, com.android.internal.R.drawable.ic_wifi_signal_3);
mTextColor = getColor(context, android.R.attr.colorForeground);
}
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
final TextView view = convertView != null ? (TextView) convertView : newView();
bind(view, getItem(position));
return view;
}
private void bind(TextView textView, DeviceFilterPair<?> item) {
textView.setText(item.getDisplayName());
textView.setBackgroundColor(Color.TRANSPARENT);
/*
textView.setCompoundDrawablesWithIntrinsicBounds(
item.getDevice() instanceof android.net.wifi.ScanResult
? mWifiIcon
: mBluetoothIcon,
null, null, null);
textView.getCompoundDrawables()[0].setTint(mTextColor);
*/
}
private TextView newView() {
final TextView textView = new TextView(mContext);
textView.setTextColor(mTextColor);
final int padding = 24;
textView.setPadding(padding, padding, padding, padding);
//textView.setCompoundDrawablePadding(padding);
return textView;
}
@Override
@@ -107,17 +64,29 @@ class DeviceListAdapter extends BaseAdapter implements Observer {
notifyDataSetChanged();
}
private @ColorInt int getColor(Context context, int attr) {
final TypedArray a = context.obtainStyledAttributes(new TypedValue().data,
new int[] { attr });
final int color = a.getColor(0, 0);
a.recycle();
return color;
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
final View view = convertView != null
? convertView
: LayoutInflater.from(mContext).inflate(R.layout.list_item_device, parent, false);
final DeviceFilterPair<?> item = getItem(position);
bindView(view, item);
return view;
}
private static Drawable getTintedIcon(Resources resources, int drawableRes) {
Drawable icon = resources.getDrawable(drawableRes, null);
icon.setTint(Color.DKGRAY);
return icon;
private void bindView(@NonNull View view, DeviceFilterPair<?> item) {
final TextView textView = view.findViewById(android.R.id.text1);
textView.setText(item.getDisplayName());
final ImageView iconView = view.findViewById(android.R.id.icon);
// TODO(b/211417476): Set either Bluetooth or WiFi icon.
iconView.setVisibility(View.GONE);
// final int iconRes = isBt ? android.R.drawable.stat_sys_data_bluetooth
// : com.android.internal.R.drawable.ic_wifi_signal_3;
// final Drawable icon = getTintedIcon(mResources, iconRes);
// iconView.setImageDrawable(icon);
}
}