Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit cb4fe77b authored by Sergey Nikolaienkov's avatar Sergey Nikolaienkov
Browse files

Make CDM UI more test(CTS)-friendly

Bug: 208307075
Test: atest CtsCompanionDevicesTestCases:AssociationEndToEndTest
Test: atest CtsCompanionDevicesTestCases
Change-Id: Ie79d74334c632c35a4c6041712fca65e03aedae9
parent c87eb5a4
Loading
Loading
Loading
Loading
+7 −3
Original line number Diff line number Diff line
@@ -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"
+39 −0
Original line number Diff line number Diff line
<?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>
 No newline at end of file
+94 −45
Original line number Diff line number Diff line
@@ -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 =
+24 −3
Original line number Diff line number Diff line
@@ -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();
+24 −55
Original line number Diff line number Diff line
@@ -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);
    }
}