diff options
author | Bill Yi <byi@google.com> | 2018-11-28 18:35:00 -0800 |
---|---|---|
committer | Bill Yi <byi@google.com> | 2018-11-28 18:35:00 -0800 |
commit | 0f8320f6b95736ea4b703764a7d11a8a6ca13674 (patch) | |
tree | d7b300e81f05e45345ada1ffcaf39b2512dd8f6f /tests | |
parent | f02b56678700a4035d0ad8882f7d20371bb96ee2 (diff) | |
parent | 911e6566751a60c29eada6ad0679694bed11be4f (diff) | |
download | Car-pie-platform-release.tar.gz |
Merge pi-qpr1-release PQ1A.181105.017.A1 to pi-platform-releasepie-platform-releasepie-cuttlefish-testing
Change-Id: Ibafbc25e1d704d7e84a168b32d35a165dd41e06f
Diffstat (limited to 'tests')
27 files changed, 2107 insertions, 336 deletions
diff --git a/tests/CarTrustAgentClientApp/Android.mk b/tests/CarTrustAgentClientApp/Android.mk new file mode 100644 index 0000000000..7945ee5fa8 --- /dev/null +++ b/tests/CarTrustAgentClientApp/Android.mk @@ -0,0 +1,15 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_PACKAGE_NAME := CarTrustAgentClient + +LOCAL_USE_AAPT2 := true +LOCAL_SRC_FILES := $(call all-java-files-under, src) +LOCAL_STATIC_ANDROID_LIBRARIES := androidx.legacy_legacy-support-v4 + +LOCAL_CERTIFICATE := platform +LOCAL_MODULE_TAGS := optional +LOCAL_MIN_SDK_VERSION := 23 +LOCAL_SDK_VERSION := current + +include $(BUILD_PACKAGE) diff --git a/tests/CarTrustAgentClientApp/AndroidManifest.xml b/tests/CarTrustAgentClientApp/AndroidManifest.xml new file mode 100644 index 0000000000..35a9a6dbd0 --- /dev/null +++ b/tests/CarTrustAgentClientApp/AndroidManifest.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.car.trust.client"> + + <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="23"/> + + <!-- Need Bluetooth LE --> + <uses-feature android:name="android.hardware.bluetooth_le" android:required="true" /> + + <uses-permission android:name="android.permission.BLUETOOTH" /> + <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> + <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> + <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> + + <!-- Needed to unlock user --> + <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" /> + <uses-permission android:name="android.permission.MANAGE_USERS" /> + <uses-permission android:name="android.permission.CONTROL_KEYGUARD" /> + <uses-permission android:name="android.permission.PROVIDE_TRUST_AGENT" /> + <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> + + <application android:label="@string/app_name"> + <activity + android:name=".PhoneEnrolmentActivity" + android:label="@string/app_name" + android:exported="true" + android:launchMode="singleInstance"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/tests/CarTrustAgentClientApp/README.txt b/tests/CarTrustAgentClientApp/README.txt new file mode 100644 index 0000000000..bf6c4444b9 --- /dev/null +++ b/tests/CarTrustAgentClientApp/README.txt @@ -0,0 +1,2 @@ +IMPORTANT NOTE: This is a reference app to smart unlock paired HU during development. +Consider moving the functionality to a more proper place. diff --git a/tests/CarTrustAgentClientApp/res/layout/phone_enrolment_activity.xml b/tests/CarTrustAgentClientApp/res/layout/phone_enrolment_activity.xml new file mode 100644 index 0000000000..620e04e81a --- /dev/null +++ b/tests/CarTrustAgentClientApp/res/layout/phone_enrolment_activity.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:weightSum="1"> + <ScrollView + android:id="@+id/scroll" + android:layout_width="match_parent" + android:layout_height="0dp" + android:scrollbars="vertical" + android:layout_weight="0.80"> + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:id="@+id/output"/> + </ScrollView> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="0.10" + android:orientation="horizontal"> + <Button + android:id="@+id/enroll_scan" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="2" + android:text="@string/enroll_scan"/> + <Button + android:id="@+id/enroll_button" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="3" + android:text="@string/enroll_button"/> + </LinearLayout> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="0.10" + android:orientation="horizontal"> + <Button + android:id="@+id/unlock_scan" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="2" + android:text="@string/unlock_scan"/> + <Button + android:id="@+id/unlock_button" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="3" + android:text="@string/unlock_button"/> + </LinearLayout> +</LinearLayout> diff --git a/tests/CarTrustAgentClientApp/res/values/strings.xml b/tests/CarTrustAgentClientApp/res/values/strings.xml new file mode 100644 index 0000000000..5c9b4db705 --- /dev/null +++ b/tests/CarTrustAgentClientApp/res/values/strings.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="app_name">CarTrustAgentClient</string> + + <!-- service/characteristics uuid for unlocking a device --> + <string name="unlock_service_uuid">5e2a68a1-27be-43f9-8d1e-4546976fabd7</string> + <string name="unlock_escrow_token_uiid">5e2a68a2-27be-43f9-8d1e-4546976fabd7</string> + <string name="unlock_handle_uiid">5e2a68a3-27be-43f9-8d1e-4546976fabd7</string> + + <!-- service/characteristics uuid for adding new escrow token --> + <string name="enrollment_service_uuid">5e2a68a4-27be-43f9-8d1e-4546976fabd7</string> + <string name="enrollment_handle_uuid">5e2a68a5-27be-43f9-8d1e-4546976fabd7</string> + <string name="enrollment_token_uuid">5e2a68a6-27be-43f9-8d1e-4546976fabd7</string> + + <string name="pref_key_token_handle">token-handle-key</string> + <string name="pref_key_escrow_token">escrow-token-key</string> + + <string name="enroll_button">Enroll new token</string> + <string name="enroll_scan">Scan to enroll</string> + <string name="unlock_button">Unlock</string> + <string name="unlock_scan">Scan to unlock</string> +</resources> diff --git a/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/PhoneEnrolmentActivity.java b/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/PhoneEnrolmentActivity.java new file mode 100644 index 0000000000..c1d30c1872 --- /dev/null +++ b/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/PhoneEnrolmentActivity.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2018 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.car.trust.client; + +import android.Manifest; +import android.app.Activity; +import android.content.pm.PackageManager; +import android.os.Bundle; + +/** + * Activity to allow the user to add an escrow token to a remote device. <p/> + * + * For this to work properly, the correct permissions must be set in the system config. In AOSP, + * this config is in frameworks/base/core/res/res/values/config.xml <p/> + * + * The config must set config_allowEscrowTokenForTrustAgent to true. For the desired car + * experience, the config should also set config_strongAuthRequiredOnBoot to false. + */ +public class PhoneEnrolmentActivity extends Activity { + + private static final int FINE_LOCATION_REQUEST_CODE = 42; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.phone_enrolment_activity); + + PhoneEnrolmentController enrolmentController = new PhoneEnrolmentController(this); + enrolmentController.bind(findViewById(R.id.output), findViewById(R.id.enroll_scan), + findViewById(R.id.enroll_button)); + + PhoneUnlockController unlockController = new PhoneUnlockController(this); + unlockController.bind(findViewById(R.id.output), findViewById(R.id.unlock_scan), + findViewById(R.id.unlock_button)); + } + + @Override + protected void onResume() { + super.onResume(); + + if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) + != PackageManager.PERMISSION_GRANTED) { + requestPermissions( + new String[] { android.Manifest.permission.ACCESS_FINE_LOCATION }, + FINE_LOCATION_REQUEST_CODE); + } + } +} diff --git a/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/PhoneEnrolmentController.java b/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/PhoneEnrolmentController.java new file mode 100644 index 0000000000..030e3d2a4b --- /dev/null +++ b/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/PhoneEnrolmentController.java @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2018 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.car.trust.client; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattService; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Handler; +import android.os.ParcelUuid; +import android.preference.PreferenceManager; +import android.util.Base64; +import android.util.Log; +import android.widget.Button; +import android.widget.TextView; + +import java.nio.ByteBuffer; +import java.util.Random; +import java.util.UUID; + +/** + * A controller that sets up a {@link SimpleBleClient} to connect to the BLE enrollment service. + * It also binds the UI components to control the enrollment process. + */ +public class PhoneEnrolmentController { + + private final SimpleBleClient.ClientCallback mCallback = new SimpleBleClient.ClientCallback() { + @Override + public void onDeviceConnected(BluetoothDevice device) { + appendOutputText("Device connected: " + device.getName() + + " addr: " + device.getAddress()); + } + + @Override + public void onDeviceDisconnected() { + appendOutputText("Device disconnected"); + } + + @Override + public void onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic) { + + Log.d(Utils.LOG_TAG, "onCharacteristicChanged: " + + Utils.getLong(characteristic.getValue())); + if (characteristic.getUuid().equals(mEnrolmentTokenHandle.getUuid())) { + // Store the new token handle that the BLE server is sending us. This required + // to unlock the device. + long handle = Utils.getLong(characteristic.getValue()); + storeHandle(handle); + appendOutputText("Token handle received: " + handle); + } + } + + @Override + public void onServiceDiscovered(BluetoothGattService service) { + if (!service.getUuid().equals(mEnrolmentServiceUuid.getUuid())) { + Log.d(Utils.LOG_TAG, "Service UUID: " + service.getUuid() + + " does not match Enrolment UUID " + mEnrolmentServiceUuid.getUuid()); + return; + } + + Log.d(Utils.LOG_TAG, "Enrolment Service # characteristics: " + + service.getCharacteristics().size()); + mEnrolmentEscrowToken = Utils.getCharacteristic( + R.string.enrollment_token_uuid, service, mContext); + mEnrolmentTokenHandle = Utils.getCharacteristic( + R.string.enrollment_handle_uuid, service, mContext); + mClient.setCharacteristicNotification(mEnrolmentTokenHandle, true /* enable */); + appendOutputText("Enrolment BLE client successfully connected"); + + mHandler.post(() -> { + // Services are now set up, allow users to enrol new escrow tokens. + mEnrolButton.setEnabled(true); + mEnrolButton.setAlpha(1.0f); + }); + } + }; + + private String mTokenHandleKey; + private String mEscrowTokenKey; + + // BLE characteristics associated with the enrollment/add escrow token service. + private BluetoothGattCharacteristic mEnrolmentTokenHandle; + private BluetoothGattCharacteristic mEnrolmentEscrowToken; + + private ParcelUuid mEnrolmentServiceUuid; + + private SimpleBleClient mClient; + private Context mContext; + + private TextView mTextView; + private Handler mHandler; + + private Button mEnrolButton; + + public PhoneEnrolmentController(Context context) { + mContext = context; + + mTokenHandleKey = context.getString(R.string.pref_key_token_handle); + mEscrowTokenKey = context.getString(R.string.pref_key_escrow_token); + + mClient = new SimpleBleClient(context); + mEnrolmentServiceUuid = new ParcelUuid( + UUID.fromString(mContext.getString(R.string.enrollment_service_uuid))); + mClient.addCallback(mCallback /* callback */); + + mHandler = new Handler(mContext.getMainLooper()); + } + + /** + * Binds the views to the actions that can be performed by this controller. + * + * @param textView A text view used to display results from various BLE actions + * @param scanButton Button used to start scanning for available BLE devices. + * @param enrolButton Button used to send new escrow token to remote device. + */ + public void bind(TextView textView, Button scanButton, Button enrolButton) { + mTextView = textView; + mEnrolButton = enrolButton; + + scanButton.setOnClickListener((view) -> mClient.start(mEnrolmentServiceUuid)); + + mEnrolButton.setEnabled(false); + mEnrolButton.setAlpha(0.3f); + mEnrolButton.setOnClickListener((view) -> { + appendOutputText("Sending new escrow token to remote device"); + + byte[] token = generateEscrowToken(); + sendEnrolmentRequest(token); + + // WARNING: Store the token so it can be used later for unlocking. This token + // should NEVER be stored on the device that is being unlocked. It should + // always be securely stored on a remote device that will trigger the unlock. + storeToken(token); + }); + } + + /** + * @return A random byte array that is used as the escrow token for remote device unlock. + */ + private byte[] generateEscrowToken() { + Random random = new Random(); + ByteBuffer buffer = ByteBuffer.allocate(Long.SIZE / Byte.SIZE); + buffer.putLong(0, random.nextLong()); + return buffer.array(); + } + + private void sendEnrolmentRequest(byte[] token) { + mEnrolmentEscrowToken.setValue(token); + mClient.writeCharacteristic(mEnrolmentEscrowToken); + storeToken(token); + } + + private void storeHandle(long handle) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); + prefs.edit().putLong(mTokenHandleKey, handle).apply(); + } + + private void storeToken(byte[] token) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); + String byteArray = Base64.encodeToString(token, Base64.DEFAULT); + prefs.edit().putString(mEscrowTokenKey, byteArray).apply(); + } + + private void appendOutputText(final String text) { + mHandler.post(() -> mTextView.append("\n" + text)); + } +} diff --git a/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/PhoneUnlockController.java b/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/PhoneUnlockController.java new file mode 100644 index 0000000000..78e50b41a6 --- /dev/null +++ b/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/PhoneUnlockController.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2018 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.car.trust.client; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattService; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Handler; +import android.os.ParcelUuid; +import android.preference.PreferenceManager; +import android.util.Base64; +import android.util.Log; +import android.widget.Button; +import android.widget.TextView; + +import java.util.UUID; + +/** + * A controller that sets up a {@link SimpleBleClient} to connect to the BLE unlock service. + */ +public class PhoneUnlockController { + + private final SimpleBleClient.ClientCallback mCallback = new SimpleBleClient.ClientCallback() { + @Override + public void onDeviceConnected(BluetoothDevice device) { + appendOutputText("Device connected: " + device.getName() + + " addr: " + device.getAddress()); + } + + @Override + public void onDeviceDisconnected() { + appendOutputText("Device disconnected"); + } + + @Override + public void onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic) { + // Not expecting any characteristics changes for the unlocking client. + } + + @Override + public void onServiceDiscovered(BluetoothGattService service) { + if (!service.getUuid().equals(mUnlockServiceUuid.getUuid())) { + Log.d(Utils.LOG_TAG, "Service UUID: " + service.getUuid() + + " does not match Enrolment UUID " + mUnlockServiceUuid.getUuid()); + return; + } + + Log.d(Utils.LOG_TAG, "Unlock Service # characteristics: " + + service.getCharacteristics().size()); + mUnlockEscrowToken = Utils.getCharacteristic( + R.string.unlock_escrow_token_uiid, service, mContext); + mUnlockTokenHandle = Utils.getCharacteristic( + R.string.unlock_handle_uiid, service, mContext); + appendOutputText("Unlock BLE client successfully connected"); + + mHandler.post(() -> { + // Services are now set up, allow users to enrol new escrow tokens. + mUnlockButton.setEnabled(true); + mUnlockButton.setAlpha(1.0f); + }); + } + }; + + private String mTokenHandleKey; + private String mEscrowTokenKey; + + // BLE characteristics associated with the enrolment/add escrow token service. + private BluetoothGattCharacteristic mUnlockTokenHandle; + private BluetoothGattCharacteristic mUnlockEscrowToken; + + private ParcelUuid mUnlockServiceUuid; + + private SimpleBleClient mClient; + private Context mContext; + + private TextView mTextView; + private Handler mHandler; + + private Button mUnlockButton; + + public PhoneUnlockController(Context context) { + mContext = context; + + mTokenHandleKey = context.getString(R.string.pref_key_token_handle); + mEscrowTokenKey = context.getString(R.string.pref_key_escrow_token); + + mClient = new SimpleBleClient(context); + mUnlockServiceUuid = new ParcelUuid( + UUID.fromString(mContext.getString(R.string.unlock_service_uuid))); + mClient.addCallback(mCallback /* callback */); + + mHandler = new Handler(mContext.getMainLooper()); + } + + /** + * Binds the views to the actions that can be performed by this controller. + * + * @param textView A text view used to display results from various BLE actions + * @param scanButton Button used to start scanning for available BLE devices. + * @param enrolButton Button used to send new escrow token to remote device. + */ + public void bind(TextView textView, Button scanButton, Button enrolButton) { + mTextView = textView; + mUnlockButton = enrolButton; + + scanButton.setOnClickListener((view) -> mClient.start(mUnlockServiceUuid)); + + mUnlockButton.setEnabled(false); + mUnlockButton.setAlpha(0.3f); + mUnlockButton.setOnClickListener((view) -> { + appendOutputText("Sending unlock token and handle to remote device"); + sendUnlockRequest(); + }); + } + + private void sendUnlockRequest() { + // Retrieve stored token and handle and write to remote device. + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); + long handle = prefs.getLong(mTokenHandleKey, -1); + byte[] token = Base64.decode(prefs.getString(mEscrowTokenKey, null), Base64.DEFAULT); + + mUnlockEscrowToken.setValue(token); + mUnlockTokenHandle.setValue(Utils.getBytes(handle)); + + mClient.writeCharacteristic(mUnlockEscrowToken); + mClient.writeCharacteristic(mUnlockTokenHandle); + } + + private void appendOutputText(final String text) { + mHandler.post(() -> mTextView.append("\n" + text)); + } +} diff --git a/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/SimpleBleClient.java b/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/SimpleBleClient.java new file mode 100644 index 0000000000..c0fecb31d4 --- /dev/null +++ b/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/SimpleBleClient.java @@ -0,0 +1,355 @@ +/* + * Copyright (C) 2018 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.car.trust.client; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCallback; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattService; +import android.bluetooth.BluetoothManager; +import android.bluetooth.BluetoothProfile; +import android.bluetooth.le.BluetoothLeScanner; +import android.bluetooth.le.ScanCallback; +import android.bluetooth.le.ScanFilter; +import android.bluetooth.le.ScanResult; +import android.bluetooth.le.ScanSettings; +import android.content.Context; +import android.os.Handler; +import android.os.ParcelUuid; +import android.util.Log; + +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * A simple client that supports the scanning and connecting to available BLE devices. Should be + * used along with {@link SimpleBleServer}. + */ +public class SimpleBleClient { + public interface ClientCallback { + /** + * Called when a device that has a matching service UUID is found. + **/ + void onDeviceConnected(BluetoothDevice device); + + void onDeviceDisconnected(); + + void onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic); + + /** + * Called for each {@link BluetoothGattService} that is discovered on the + * {@link BluetoothDevice} after a matching scan result and connection. + * + * @param service {@link BluetoothGattService} that has been discovered. + */ + void onServiceDiscovered(BluetoothGattService service); + } + + /** + * Wrapper class to allow queuing of BLE actions. The BLE stack allows only one action to be + * executed at a time. + */ + public static class BleAction { + public static final int ACTION_WRITE = 0; + public static final int ACTION_READ = 1; + + private int mAction; + private BluetoothGattCharacteristic mCharacteristic; + + public BleAction(BluetoothGattCharacteristic characteristic, int action) { + mAction = action; + mCharacteristic = characteristic; + } + + public int getAction() { + return mAction; + } + + public BluetoothGattCharacteristic getCharacteristic() { + return mCharacteristic; + } + } + + private static final long SCAN_TIME_MS = 10000; + + private final Queue<BleAction> mBleActionQueue = new ConcurrentLinkedQueue<BleAction>(); + private final List<ClientCallback> mCallbacks = new ArrayList<>(); + private final Context mContext; + private final BluetoothLeScanner mScanner; + + private BluetoothGatt mBtGatt; + private ParcelUuid mServiceUuid; + + public SimpleBleClient(@NonNull Context context) { + mContext = context; + BluetoothManager btManager = (BluetoothManager) mContext.getSystemService( + Context.BLUETOOTH_SERVICE); + mScanner = btManager.getAdapter().getBluetoothLeScanner(); + } + + /** + * Start scanning for a BLE devices with the specified service uuid. + * + * @param parcelUuid {@link ParcelUuid} used to identify the device that should be used for + * this client. This uuid should be the same as the one that is set in the + * {@link android.bluetooth.le.AdvertiseData.Builder} by the advertising + * device. + */ + public void start(ParcelUuid parcelUuid) { + mServiceUuid = parcelUuid; + + // We only want to scan for devices that have the correct uuid set in its advertise data. + List<ScanFilter> filters = new ArrayList<ScanFilter>(); + ScanFilter.Builder serviceFilter = new ScanFilter.Builder(); + serviceFilter.setServiceUuid(mServiceUuid); + filters.add(serviceFilter.build()); + + ScanSettings.Builder settings = new ScanSettings.Builder(); + settings.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY); + + Log.d(Utils.LOG_TAG, "Start scanning for uuid: " + mServiceUuid.getUuid()); + mScanner.startScan(filters, settings.build(), mScanCallback); + + Handler handler = new Handler(); + handler.postDelayed(new Runnable() { + @Override + public void run() { + mScanner.stopScan(mScanCallback); + Log.d(Utils.LOG_TAG, "Stopping Scanner"); + } + }, SCAN_TIME_MS); + } + + private boolean hasServiceUuid(ScanResult result) { + if (result.getScanRecord() == null + || result.getScanRecord().getServiceUuids() == null + || result.getScanRecord().getServiceUuids().size() == 0) { + return false; + } + return true; + } + + /** + * Writes to a {@link BluetoothGattCharacteristic} if possible, or queues the action until + * other actions are complete. + * + * @param characteristic {@link BluetoothGattCharacteristic} to be written + */ + public void writeCharacteristic(BluetoothGattCharacteristic characteristic) { + processAction(new BleAction(characteristic, BleAction.ACTION_WRITE)); + } + + /** + * Reads a {@link BluetoothGattCharacteristic} if possible, or queues the read action until + * other actions are complete. + * + * @param characteristic {@link BluetoothGattCharacteristic} to be read. + */ + public void readCharacteristic(BluetoothGattCharacteristic characteristic) { + processAction(new BleAction(characteristic, BleAction.ACTION_READ)); + } + + /** + * Enable or disable notification for specified {@link BluetoothGattCharacteristic}. + * + * @param characteristic The {@link BluetoothGattCharacteristic} for which to enable + * notifications. + * @param enabled True if notifications should be enabled, false otherwise. + */ + public void setCharacteristicNotification(BluetoothGattCharacteristic characteristic, + boolean enabled) { + mBtGatt.setCharacteristicNotification(characteristic, enabled); + } + + /** + * Add a {@link ClientCallback} to listen for updates from BLE components + */ + public void addCallback(ClientCallback callback) { + mCallbacks.add(callback); + } + + public void removeCallback(ClientCallback callback) { + mCallbacks.remove(callback); + } + + private void processAction(BleAction action) { + // Only execute actions if the queue is empty. + if (mBleActionQueue.size() > 0) { + mBleActionQueue.add(action); + return; + } + + mBleActionQueue.add(action); + executeAction(mBleActionQueue.peek()); + } + + private void processNextAction() { + mBleActionQueue.poll(); + executeAction(mBleActionQueue.peek()); + } + + private void executeAction(BleAction action) { + if (action == null) { + return; + } + + Log.d(Utils.LOG_TAG, "Executing BLE Action type: " + action.getAction()); + int actionType = action.getAction(); + switch (actionType) { + case BleAction.ACTION_WRITE: + mBtGatt.writeCharacteristic(action.getCharacteristic()); + break; + case BleAction.ACTION_READ: + mBtGatt.readCharacteristic(action.getCharacteristic()); + break; + default: + } + } + + private String getStatus(int status) { + switch (status) { + case BluetoothGatt.GATT_FAILURE: + return "Failure"; + case BluetoothGatt.GATT_SUCCESS: + return "GATT_SUCCESS"; + case BluetoothGatt.GATT_READ_NOT_PERMITTED: + return "GATT_READ_NOT_PERMITTED"; + case BluetoothGatt.GATT_WRITE_NOT_PERMITTED: + return "GATT_WRITE_NOT_PERMITTED"; + case BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION: + return "GATT_INSUFFICIENT_AUTHENTICATION"; + case BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED: + return "GATT_REQUEST_NOT_SUPPORTED"; + case BluetoothGatt.GATT_INVALID_OFFSET: + return "GATT_INVALID_OFFSET"; + case BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH: + return "GATT_INVALID_ATTRIBUTE_LENGTH"; + case BluetoothGatt.GATT_CONNECTION_CONGESTED: + return "GATT_CONNECTION_CONGESTED"; + default: + return "unknown"; + } + } + + private ScanCallback mScanCallback = new ScanCallback() { + @Override + public void onScanResult(int callbackType, ScanResult result) { + BluetoothDevice device = result.getDevice(); + Log.d(Utils.LOG_TAG, "Scan result found: " + result.getScanRecord().getServiceUuids()); + + if (!hasServiceUuid(result)) { + return; + } + + for (ParcelUuid uuid : result.getScanRecord().getServiceUuids()) { + Log.d(Utils.LOG_TAG, "Scan result UUID: " + uuid); + if (uuid.equals(mServiceUuid)) { + // This client only supports connecting to one service. + // Once we find one, stop scanning and open a GATT connection to the device. + mScanner.stopScan(mScanCallback); + mBtGatt = device.connectGatt(mContext, false /* autoConnect */, mGattCallback); + return; + } + } + } + + @Override + public void onBatchScanResults(List<ScanResult> results) { + for (ScanResult r : results) { + Log.d(Utils.LOG_TAG, "Batch scanResult: " + r.getDevice().getName() + + " " + r.getDevice().getAddress()); + } + } + + @Override + public void onScanFailed(int errorCode) { + Log.e(Utils.LOG_TAG, "Scan failed: " + errorCode); + } + }; + + private BluetoothGattCallback mGattCallback = new BluetoothGattCallback() { + @Override + public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { + super.onConnectionStateChange(gatt, status, newState); + + String state = ""; + + if (newState == BluetoothProfile.STATE_CONNECTED) { + state = "Connected"; + mBtGatt.discoverServices(); + for (ClientCallback callback : mCallbacks) { + callback.onDeviceConnected(gatt.getDevice()); + } + + } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { + state = "Disconnected"; + for (ClientCallback callback : mCallbacks) { + callback.onDeviceDisconnected(); + } + } + Log.d(Utils.LOG_TAG, "Gatt connection status: " + getStatus(status) + + " newState: " + state); + } + + @Override + public void onServicesDiscovered(BluetoothGatt gatt, int status) { + super.onServicesDiscovered(gatt, status); + Log.d(Utils.LOG_TAG, "onServicesDiscovered: " + status); + + List<BluetoothGattService> services = gatt.getServices(); + if (services == null || services.size() <= 0) { + return; + } + + // Notify clients of newly discovered services. + for (BluetoothGattService service : mBtGatt.getServices()) { + Log.d(Utils.LOG_TAG, "Found service: " + service.getUuid() + " notifying clients"); + for (ClientCallback callback : mCallbacks) { + callback.onServiceDiscovered(service); + } + } + } + + @Override + public void onCharacteristicWrite(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic, int status) { + Log.d(Utils.LOG_TAG, "onCharacteristicWrite: " + status); + processNextAction(); + } + + @Override + public void onCharacteristicRead(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic, int status) { + Log.d(Utils.LOG_TAG, "onCharacteristicRead:" + new String(characteristic.getValue())); + processNextAction(); + } + + @Override + public void onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic) { + for (ClientCallback callback : mCallbacks) { + callback.onCharacteristicChanged(gatt, characteristic); + } + processNextAction(); + } + }; +} diff --git a/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/Utils.java b/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/Utils.java new file mode 100644 index 0000000000..003a86cc44 --- /dev/null +++ b/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/Utils.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2018 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.car.trust.client; + +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattService; +import android.content.Context; + +import java.nio.ByteBuffer; +import java.util.UUID; + +public class Utils { + + public static final String LOG_TAG = "CarTrustAgentClient"; + + public static byte[] getBytes(long l) { + ByteBuffer buffer = ByteBuffer.allocate(Long.SIZE / Byte.SIZE); + buffer.putLong(0, l); + return buffer.array(); + } + + public static long getLong(byte[] bytes) { + ByteBuffer buffer = ByteBuffer.allocate(Long.SIZE / Byte.SIZE); + buffer.put(bytes); + buffer.flip(); + return buffer.getLong(); + } + + public static BluetoothGattCharacteristic getCharacteristic(int uuidRes, + BluetoothGattService service, Context context) { + return service.getCharacteristic(UUID.fromString(context.getString(uuidRes))); + } +} diff --git a/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml b/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml index bc89ad4311..6e8e80f73a 100644 --- a/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml +++ b/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml @@ -42,6 +42,8 @@ <uses-permission android:name="android.permission.READ_SMS"/> <uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> + <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" /> + <uses-permission android:name="android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS" /> <uses-permission android:name="android.permission.INJECT_EVENTS" /> <application android:label="@string/app_title" diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/connectivity_fragment.xml b/tests/EmbeddedKitchenSinkApp/res/layout/connectivity_fragment.xml new file mode 100644 index 0000000000..15422b1116 --- /dev/null +++ b/tests/EmbeddedKitchenSinkApp/res/layout/connectivity_fragment.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 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:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <LinearLayout + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + <ListView + android:id="@+id/networks" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + </ListView> + </LinearLayout> + <LinearLayout + android:orientation="horizontal" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="4dp"> + <Button android:id="@+id/networksRefresh" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Refresh"/> + <Button android:id="@+id/networkRequestOemPaid" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Request OEM-paid"/> + <Button android:id="@+id/networkRequestEth1" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Request eth1"/> + <Button android:id="@+id/networkReleaseNetwork" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Release Request"/> + </LinearLayout> + +</LinearLayout>
\ No newline at end of file diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/list_item.xml b/tests/EmbeddedKitchenSinkApp/res/layout/list_item.xml new file mode 100644 index 0000000000..f517913acb --- /dev/null +++ b/tests/EmbeddedKitchenSinkApp/res/layout/list_item.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 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. +--> +<TextView xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@android:id/text1" + android:paddingTop="2dip" + android:paddingBottom="3dip" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:textSize="24sp" />
\ No newline at end of file diff --git a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml index 74c7c2637b..452fec8d81 100644 --- a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml +++ b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml @@ -211,14 +211,14 @@ <!-- sensors test --> <string name="sensor_na">N/A</string> - <string name="sensor_environment">Environment[%1$s]: temperature=%2$s, pressure=%3$s</string> + <string name="sensor_environment">Environment[%1$s]: temperature=%2$s</string> <string name="sensor_night">Night[%1$s]: isNight=%2$s</string> <string name="sensor_gear">Gear[%1$s]: gear=%2$s</string> <string name="sensor_parking_brake">Parking brake[%1$s]: isEngaged=%2$s</string> <string name="sensor_odometer">Odometer[%1$s]: kms=%2$s</string> <string name="sensor_rpm">RPM[%1$s]: rpm=%2$s</string> <string name="sensor_speed">Speed[%1$s]: speed=%2$s</string> - <string name="sensor_driving_status">Driving status[%1$s]: status=%2$s [bin=%3$s]</string> + <string name="sensor_ignition_status">Ignition status[%1$s]: status=%2$s</string> <string name="sensor_compass">Compass[%1$s]: bear=%2$s, pitch=%3$s, roll=%4$s</string> <string name="sensor_accelerometer">Accelerometer[%1$s]: x=%2$s, y=%3$s, z=%4$s</string> <string name="sensor_gyroscope">Gyroscope[%1$s]: x=%2$s, y=%3$s, z=%4$s</string> @@ -231,6 +231,7 @@ <string name="sensor_traction_control_is_active">Traction Control[%1$s]: isActive=%2$s</string> <string name="sensor_fuel_level">Fuel Level[%1$s]: %2$s</string> <string name="sensor_fuel_door_open">Fuel Door Open[%1$s]: %2$s</string> + <string name="sensor_engine_oil_level">Engine Oil Level[%1$s]: %2$s</string> <string name="sensor_engine_is_on">Engine Is On[%1$s]: %2$s</string> <string name="sensor_ev_battery_level">EV Battery Level[%1$s]: %2$s</string> <string name="sensor_ev_charge_port_is_open">EV Charge Port Is Open[%1$s]: %2$s</string> diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java index 7837f2e775..caca03af01 100644 --- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java +++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java @@ -23,7 +23,9 @@ import android.car.hardware.power.CarPowerManager; import android.car.hardware.property.CarPropertyManager; import android.content.Intent; import android.content.pm.PackageManager; +import android.os.AsyncTask; import android.os.Bundle; +import android.os.Handler; import android.support.car.Car; import android.support.car.CarAppFocusManager; import android.support.car.CarConnectionCallback; @@ -42,6 +44,7 @@ import com.google.android.car.kitchensink.audio.AudioTestFragment; import com.google.android.car.kitchensink.bluetooth.BluetoothHeadsetFragment; import com.google.android.car.kitchensink.bluetooth.MapMceTestFragment; import com.google.android.car.kitchensink.cluster.InstrumentClusterFragment; +import com.google.android.car.kitchensink.connectivity.ConnectivityFragment; import com.google.android.car.kitchensink.cube.CubesTestFragment; import com.google.android.car.kitchensink.diagnostic.DiagnosticTestFragment; import com.google.android.car.kitchensink.displayinfo.DisplayInfoFragment; @@ -167,6 +170,7 @@ public class KitchenSinkActivity extends CarDrawerActivity { startActivity(intent); }); add("activity view", ActivityViewTestFragment.class); + add("connectivity", ConnectivityFragment.class); add("quit", KitchenSinkActivity.this::finish); } @@ -183,6 +187,7 @@ public class KitchenSinkActivity extends CarDrawerActivity { private CarPropertyManager mPropertyManager; private CarSensorManager mSensorManager; private CarAppFocusManager mCarAppFocusManager; + private Object mPropertyManagerReady = new Object(); public CarHvacManager getHvacManager() { return mHvacManager; @@ -212,12 +217,20 @@ public class KitchenSinkActivity extends CarDrawerActivity { setMainContent(R.layout.kitchen_content); // Connection to Car Service does not work for non-automotive yet. if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) { - mCarApi = Car.createCar(this, mCarConnectionCallback); - mCarApi.connect(); + initCarApi(); } Log.i(TAG, "onCreate"); } + private void initCarApi() { + if (mCarApi != null && mCarApi.isConnected()) { + mCarApi.disconnect(); + mCarApi = null; + } + mCarApi = Car.createCar(this, mCarConnectionCallback); + mCarApi.connect(); + } + @Override protected void onStart() { super.onStart(); @@ -268,18 +281,22 @@ public class KitchenSinkActivity extends CarDrawerActivity { @Override public void onConnected(Car car) { Log.d(TAG, "Connected to Car Service"); - try { - mHvacManager = (CarHvacManager) mCarApi.getCarManager(android.car.Car.HVAC_SERVICE); - mPowerManager = (CarPowerManager) mCarApi.getCarManager( - android.car.Car.POWER_SERVICE); - mPropertyManager = (CarPropertyManager) mCarApi.getCarManager( - android.car.Car.PROPERTY_SERVICE); - mSensorManager = (CarSensorManager) mCarApi.getCarManager( - android.car.Car.SENSOR_SERVICE); - mCarAppFocusManager = - (CarAppFocusManager) mCarApi.getCarManager(Car.APP_FOCUS_SERVICE); - } catch (CarNotConnectedException e) { - Log.e(TAG, "Car is not connected!", e); + synchronized (mPropertyManagerReady) { + try { + mHvacManager = (CarHvacManager) mCarApi.getCarManager( + android.car.Car.HVAC_SERVICE); + mPowerManager = (CarPowerManager) mCarApi.getCarManager( + android.car.Car.POWER_SERVICE); + mPropertyManager = (CarPropertyManager) mCarApi.getCarManager( + android.car.Car.PROPERTY_SERVICE); + mSensorManager = (CarSensorManager) mCarApi.getCarManager( + android.car.Car.SENSOR_SERVICE); + mCarAppFocusManager = + (CarAppFocusManager) mCarApi.getCarManager(Car.APP_FOCUS_SERVICE); + mPropertyManagerReady.notifyAll(); + } catch (CarNotConnectedException e) { + Log.e(TAG, "Car is not connected!", e); + } } } @@ -322,4 +339,29 @@ public class KitchenSinkActivity extends CarDrawerActivity { getDrawerController().closeDrawer(); } } + + // Use AsyncTask to refresh Car*Manager after car service connected + public void requestRefreshManager(final Runnable r, final Handler h) { + final AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... unused) { + synchronized (mPropertyManagerReady) { + while (!mCarApi.isConnected()) { + try { + mPropertyManagerReady.wait(); + } catch (InterruptedException e) { + return null; + } + } + } + return null; + } + + @Override + protected void onPostExecute(Void unused) { + h.post(r); + } + }; + task.execute(); + } } diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/connectivity/ConnectivityFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/connectivity/ConnectivityFragment.java new file mode 100644 index 0000000000..0ffa6bf37e --- /dev/null +++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/connectivity/ConnectivityFragment.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2018 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.google.android.car.kitchensink.connectivity; + +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.net.ConnectivityManager; +import android.net.ConnectivityManager.NetworkCallback; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.net.NetworkRequest; +import android.os.Bundle; +import android.os.Handler; +import android.support.v4.app.Fragment; +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.Toast; + +import com.google.android.car.kitchensink.R; + +import java.util.ArrayList; + +@SuppressLint("SetTextI18n") +public class ConnectivityFragment extends Fragment { + private static final String TAG = ConnectivityFragment.class.getSimpleName(); + + private final Handler mHandler = new Handler(); + private final ArrayList<String> mNetworks = new ArrayList<>(); + + private ConnectivityManager mConnectivityManager; + private ArrayAdapter<String> mNetworksAdapter; + + private final NetworkCallback mNetworkCallback = new NetworkCallback() { + @Override + public void onAvailable(Network network) { + showToast("onAvailable, netId: " + network); + refreshNetworks(); + } + + @Override + public void onLost(Network network) { + showToast("onLost, netId: " + network); + refreshNetworks(); + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mConnectivityManager = getActivity().getSystemService(ConnectivityManager.class); + + mConnectivityManager.addDefaultNetworkActiveListener(() -> refreshNetworks()); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.connectivity_fragment, container, false); + + ListView networksView = view.findViewById(R.id.networks); + mNetworksAdapter = new ArrayAdapter<>(getActivity(), R.layout.list_item, mNetworks); + networksView.setAdapter(mNetworksAdapter); + + setClickAction(view, R.id.networksRefresh, this::refreshNetworks); + setClickAction(view, R.id.networkRequestOemPaid, this::requestOemPaid); + setClickAction(view, R.id.networkRequestEth1, this::requestEth1); + setClickAction(view, R.id.networkReleaseNetwork, this::releaseNetworkRequest); + + return view; + } + + private void releaseNetworkRequest() { + mConnectivityManager.unregisterNetworkCallback(mNetworkCallback); + showToast("Release request sent"); + } + + private void requestEth1() { + NetworkRequest request = new NetworkRequest.Builder() + .clearCapabilities() + .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET) + .setNetworkSpecifier("eth1") + .build(); + mConnectivityManager.requestNetwork(request, mNetworkCallback, mHandler); + } + + private void requestOemPaid() { + NetworkRequest request = new NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PAID) + .build(); + + mConnectivityManager.requestNetwork(request, mNetworkCallback, mHandler); + } + + @Override + public void onResume() { + super.onResume(); + refreshNetworks(); + } + + private void setClickAction(View view, int id, Runnable action) { + view.findViewById(id).setOnClickListener(v -> action.run()); + } + + private void refreshNetworks() { + mNetworks.clear(); + + for (Network network : mConnectivityManager.getAllNetworks()) { + boolean isDefault = sameNetworkId(network, mConnectivityManager.getActiveNetwork()); + NetworkCapabilities nc = mConnectivityManager.getNetworkCapabilities(network); + boolean isOemPaid = nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PAID); + boolean isInternet = nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); + + NetworkInfo networkInfo = mConnectivityManager.getNetworkInfo(network); + + mNetworks.add("netId: " + network.netId + + (isInternet ? " [INTERNET]" : "") + + (isDefault ? " [DEFAULT]" : "") + + (isOemPaid ? " [OEM-paid]" : "") + nc + " " + networkInfo); + } + + mNetworksAdapter.notifyDataSetChanged(); + } + + private void showToast(String text) { + Log.d(TAG, "showToast: " + text); + Toast.makeText(getContext(), text, Toast.LENGTH_LONG).show(); + } + + private static boolean sameNetworkId(Network net1, Network net2) { + return net1 != null && net2 != null && net1.netId == net2.netId; + + } +} diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/hvac/HvacTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/hvac/HvacTestFragment.java index a1f8e1dece..c7b80e8462 100644 --- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/hvac/HvacTestFragment.java +++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/hvac/HvacTestFragment.java @@ -25,6 +25,7 @@ import android.car.hardware.hvac.CarHvacManager; import android.hardware.automotive.vehicle.V2_0.VehicleAreaSeat; import android.hardware.automotive.vehicle.V2_0.VehicleAreaWindow; import android.os.Bundle; +import android.os.Handler; import android.support.v4.app.Fragment; import android.util.Log; import android.view.LayoutInflater; @@ -76,6 +77,8 @@ public class HvacTestFragment extends Fragment { private int mZoneForSetTempP; private int mZoneForFanSpeed; private int mZoneForFanPosition; + private List<CarPropertyConfig> mCarPropertyConfigs; + private View mHvacView; private final CarHvacManager.CarHvacEventCallback mHvacCallback = new CarHvacManager.CarHvacEventCallback () { @@ -171,13 +174,9 @@ public class HvacTestFragment extends Fragment { @Override public void onCreate(Bundle savedInstanceState) { - mCarHvacManager = ((KitchenSinkActivity)getActivity()).getHvacManager(); + super.onCreate(savedInstanceState); - try { - mCarHvacManager.registerCallback(mHvacCallback); - } catch (CarNotConnectedException e) { - Log.e(TAG, "Car is not connected!"); - } + } @Override @@ -188,77 +187,85 @@ public class HvacTestFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstance) { - View v = inflater.inflate(R.layout.hvac_test, container, false); - - List<CarPropertyConfig> props; - try { - props = mCarHvacManager.getPropertyList(); - } catch (CarNotConnectedException e) { - Log.e(TAG, "Failed to get list of properties", e); - props = new ArrayList<>(); - } + mHvacView = inflater.inflate(R.layout.hvac_test, container, false); + final Runnable r = () -> { + mCarHvacManager = ((KitchenSinkActivity) getActivity()).getHvacManager(); + try { + mCarHvacManager.registerCallback(mHvacCallback); + } catch (CarNotConnectedException e) { + Log.e(TAG, "Car is not connected!"); + } + try { + mCarPropertyConfigs = mCarHvacManager.getPropertyList(); + } catch (CarNotConnectedException e) { + Log.e(TAG, "Failed to get list of properties", e); + mCarPropertyConfigs = new ArrayList<>(); + } + for (CarPropertyConfig prop : mCarPropertyConfigs) { + int propId = prop.getPropertyId(); - for(CarPropertyConfig prop : props) { - int propId = prop.getPropertyId(); + if (DBG) { + Log.d(TAG, prop.toString()); + } - if(DBG) { - Log.d(TAG, prop.toString()); + switch(propId) { + case CarHvacManager.ID_OUTSIDE_AIR_TEMP: + configureOutsideTemp(mHvacView, prop); + break; + case CarHvacManager.ID_ZONED_DUAL_ZONE_ON: + configureDualOn(mHvacView, prop); + break; + case CarHvacManager.ID_ZONED_AC_ON: + configureAcOn(mHvacView, prop); + break; + case CarHvacManager.ID_ZONED_FAN_DIRECTION: + configureFanPosition(mHvacView, prop); + break; + case CarHvacManager.ID_ZONED_FAN_SPEED_SETPOINT: + configureFanSpeed(mHvacView, prop); + break; + case CarHvacManager.ID_ZONED_TEMP_SETPOINT: + configureTempSetpoint(mHvacView, prop); + break; + case CarHvacManager.ID_ZONED_AUTOMATIC_MODE_ON: + configureAutoModeOn(mHvacView, prop); + break; + case CarHvacManager.ID_ZONED_AIR_RECIRCULATION_ON: + configureRecircOn(mHvacView, prop); + break; + case CarHvacManager.ID_ZONED_MAX_AC_ON: + configureMaxAcOn(mHvacView, prop); + break; + case CarHvacManager.ID_ZONED_MAX_DEFROST_ON: + configureMaxDefrostOn(mHvacView, prop); + break; + case CarHvacManager.ID_WINDOW_DEFROSTER_ON: + configureDefrosterOn(mHvacView, prop); + break; + default: + Log.w(TAG, "propertyId " + propId + " is not handled"); + break; + } } - switch(propId) { - case CarHvacManager.ID_OUTSIDE_AIR_TEMP: - configureOutsideTemp(v, prop); - break; - case CarHvacManager.ID_ZONED_DUAL_ZONE_ON: - configureDualOn(v, prop); - break; - case CarHvacManager.ID_ZONED_AC_ON: - configureAcOn(v, prop); - break; - case CarHvacManager.ID_ZONED_FAN_DIRECTION: - configureFanPosition(v, prop); - break; - case CarHvacManager.ID_ZONED_FAN_SPEED_SETPOINT: - configureFanSpeed(v, prop); - break; - case CarHvacManager.ID_ZONED_TEMP_SETPOINT: - configureTempSetpoint(v, prop); - break; - case CarHvacManager.ID_ZONED_AUTOMATIC_MODE_ON: - configureAutoModeOn(v, prop); - break; - case CarHvacManager.ID_ZONED_AIR_RECIRCULATION_ON: - configureRecircOn(v, prop); - break; - case CarHvacManager.ID_ZONED_MAX_AC_ON: - configureMaxAcOn(v, prop); - break; - case CarHvacManager.ID_ZONED_MAX_DEFROST_ON: - configureMaxDefrostOn(v, prop); - break; - case CarHvacManager.ID_WINDOW_DEFROSTER_ON: - configureDefrosterOn(v, prop); - break; - default: - Log.w(TAG, "propertyId " + propId + " is not handled"); - break; - } - } + mTvFanSpeed = (TextView) mHvacView.findViewById(R.id.tvFanSpeed); + mTvFanSpeed.setText(String.valueOf(mCurFanSpeed)); + mTvDTemp = (TextView) mHvacView.findViewById(R.id.tvDTemp); + mTvDTemp.setText(String.valueOf(mCurDTemp)); + mTvPTemp = (TextView) mHvacView.findViewById(R.id.tvPTemp); + mTvPTemp.setText(String.valueOf(mCurPTemp)); + mTvOutsideTemp = (TextView) mHvacView.findViewById(R.id.tvOutsideTemp); + mTvOutsideTemp.setText("N/A"); + }; - mTvFanSpeed = (TextView) v.findViewById(R.id.tvFanSpeed); - mTvFanSpeed.setText(String.valueOf(mCurFanSpeed)); - mTvDTemp = (TextView) v.findViewById(R.id.tvDTemp); - mTvDTemp.setText(String.valueOf(mCurDTemp)); - mTvPTemp = (TextView) v.findViewById(R.id.tvPTemp); - mTvPTemp.setText(String.valueOf(mCurPTemp)); - mTvOutsideTemp = (TextView) v.findViewById(R.id.tvOutsideTemp); - mTvOutsideTemp.setText("N/A"); + ((KitchenSinkActivity) getActivity()).requestRefreshManager(r, + new Handler(getContext().getMainLooper())); if(DBG) { Log.d(TAG, "Starting HvacTestFragment"); } - return v; + return mHvacView; } private void configureOutsideTemp(View v, CarPropertyConfig prop) { diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/power/PowerTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/power/PowerTestFragment.java index fed1fbd322..9a6c2b92cf 100644 --- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/power/PowerTestFragment.java +++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/power/PowerTestFragment.java @@ -20,6 +20,7 @@ import android.car.CarNotConnectedException; import android.car.hardware.power.CarPowerManager; import android.content.Context; import android.os.Bundle; +import android.os.Handler; import android.os.PowerManager; import android.os.SystemClock; import android.support.v4.app.Fragment; @@ -58,16 +59,20 @@ public class PowerTestFragment extends Fragment { @Override public void onCreate(Bundle savedInstanceState) { - mCarPowerManager = ((KitchenSinkActivity)getActivity()).getPowerManager(); - mExecutor = new ThreadPerTaskExecutor(); + final Runnable r = () -> { + mCarPowerManager = ((KitchenSinkActivity) getActivity()).getPowerManager(); + mExecutor = new ThreadPerTaskExecutor(); + try { + mCarPowerManager.setListener(mPowerListener, mExecutor); + } catch (CarNotConnectedException e) { + Log.e(TAG, "Car is not connected!"); + } catch (IllegalStateException e) { + Log.e(TAG, "CarPowerManager listener was not cleared"); + } + }; + ((KitchenSinkActivity) getActivity()).requestRefreshManager(r, + new Handler(getContext().getMainLooper())); super.onCreate(savedInstanceState); - try { - mCarPowerManager.setListener(mPowerListener, mExecutor); - } catch (CarNotConnectedException e) { - Log.e(TAG, "Car is not connected!"); - } catch (IllegalStateException e) { - Log.e(TAG, "CarPowerManager listener was not cleared"); - } } @Override diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/property/PropertyTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/property/PropertyTestFragment.java index fc6621a7ca..ff1c402fea 100644 --- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/property/PropertyTestFragment.java +++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/property/PropertyTestFragment.java @@ -26,6 +26,7 @@ import android.content.DialogInterface.OnClickListener; import android.hardware.automotive.vehicle.V2_0.VehicleProperty; import android.hardware.automotive.vehicle.V2_0.VehiclePropertyType; import android.os.Bundle; +import android.os.Handler; import android.support.v4.app.Fragment; import android.util.Log; import android.view.LayoutInflater; @@ -85,20 +86,23 @@ public class PropertyTestFragment extends Fragment implements OnItemSelectedList mPropertyId = view.findViewById(R.id.sPropertyId); mScrollView = view.findViewById(R.id.svEventLog); mSetValue = view.findViewById(R.id.etSetPropertyValue); - - populateConfigList(); - mListView.setAdapter(new PropertyListAdapter(mPropInfo, mMgr, mEventLog, mScrollView, - mActivity)); - - // Configure dropdown menu for propertyId spinner - ArrayAdapter<PropertyInfo> adapter = - new ArrayAdapter<PropertyInfo>(mActivity, android.R.layout.simple_spinner_item, - mPropInfo); - adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - mPropertyId.setAdapter(adapter); - mPropertyId.setOnItemSelectedListener(this); - - + mActivity = (KitchenSinkActivity) getActivity(); + + final Runnable r = () -> { + mMgr = mActivity.getPropertyManager(); + populateConfigList(); + mListView.setAdapter(new PropertyListAdapter(mPropInfo, mMgr, mEventLog, mScrollView, + mActivity)); + + // Configure dropdown menu for propertyId spinner + ArrayAdapter<PropertyInfo> adapter = + new ArrayAdapter<PropertyInfo>(mActivity, android.R.layout.simple_spinner_item, + mPropInfo); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mPropertyId.setAdapter(adapter); + mPropertyId.setOnItemSelectedListener(this); + }; + mActivity.requestRefreshManager(r, new Handler(getContext().getMainLooper())); // Configure listeners for buttons Button b = view.findViewById(R.id.bGetProperty); diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/sensor/SensorsTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/sensor/SensorsTestFragment.java index abc2c10b49..1440ff0571 100644 --- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/sensor/SensorsTestFragment.java +++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/sensor/SensorsTestFragment.java @@ -99,17 +99,19 @@ public class SensorsTestFragment extends Fragment { View view = inflater.inflate(R.layout.sensors, container, false); mActivity = (KitchenSinkActivity) getHost(); - mSensorInfo = (TextView) view.findViewById(R.id.sensor_info); mNaString = getContext().getString(R.string.sensor_na); - return view; } @Override public void onResume() { super.onResume(); - initPermissions(); + final Runnable r = () -> { + initPermissions(); + }; + ((KitchenSinkActivity) getActivity()).requestRefreshManager(r, + new Handler(getContext().getMainLooper())); } @Override @@ -207,6 +209,12 @@ public class SensorsTestFragment extends Fragment { case CarSensorManager.SENSOR_TYPE_FUEL_DOOR_OPEN: summary.add(getFuelDoorOpen(event)); break; + case CarSensorManager.SENSOR_TYPE_IGNITION_STATE: + summary.add(getContext().getString(R.string.sensor_ignition_status, + getTimestamp(event), + event == null ? mNaString : + event.getIgnitionStateData(null).ignitionState)); + break; case CarSensorManager.SENSOR_TYPE_PARKING_BRAKE: summary.add(getContext().getString(R.string.sensor_parking_brake, getTimestamp(event), @@ -223,18 +231,15 @@ public class SensorsTestFragment extends Fragment { getTimestamp(event), event == null ? mNaString : event.getNightData(null).isNightMode)); break; - case CarSensorManager.SENSOR_TYPE_ENVIRONMENT: + case CarSensorManager.SENSOR_TYPE_ENV_OUTSIDE_TEMPERATURE: String temperature = mNaString; - String pressure = mNaString; if (event != null) { CarSensorEvent.EnvironmentData env = event.getEnvironmentData(null); temperature = Float.isNaN(env.temperature) ? temperature : String.valueOf(env.temperature); - pressure = Float.isNaN(env.pressure) ? pressure : - String.valueOf(env.pressure); } summary.add(getContext().getString(R.string.sensor_environment, - getTimestamp(event), temperature, pressure)); + getTimestamp(event), temperature)); break; case CarSensorManager.SENSOR_TYPE_WHEEL_TICK_DISTANCE: if(event != null) { diff --git a/tests/carservice_test/AndroidManifest.xml b/tests/carservice_test/AndroidManifest.xml index 2be5a8302e..19a5552e2f 100644 --- a/tests/carservice_test/AndroidManifest.xml +++ b/tests/carservice_test/AndroidManifest.xml @@ -19,7 +19,10 @@ <uses-permission android:name="android.Manifest.permission.MODIFY_AUDIO_ROUTING" /> <uses-permission android:name="android.car.permission.CONTROL_CAR_CLIMATE" /> - <uses-permission android:name="android.car.permission.ADJUST_CAR_CABIN" /> + <uses-permission android:name="android.car.permission.CONTROL_CAR_DOORS" /> + <uses-permission android:name="android.car.permission.CONTROL_CAR_WINDOWS" /> + <uses-permission android:name="android.car.permission.CONTROL_CAR_MIRRORS" /> + <uses-permission android:name="android.car.permission.CONTROL_CAR_SEATS" /> <uses-permission android:name="android.car.permission.CAR_ENERGY" /> <uses-permission android:name="android.car.permission.CONTROL_APP_BLOCKING" /> <uses-permission android:name="android.car.permission.CAR_CONTROL_AUDIO_VOLUME" /> diff --git a/tests/carservice_test/src/com/android/car/MockedCarTestBase.java b/tests/carservice_test/src/com/android/car/MockedCarTestBase.java index a57d800b8d..c63c980fa4 100644 --- a/tests/carservice_test/src/com/android/car/MockedCarTestBase.java +++ b/tests/carservice_test/src/com/android/car/MockedCarTestBase.java @@ -173,7 +173,7 @@ public class MockedCarTestBase { protected MockContext getCarServiceContext() throws NameNotFoundException { if (mMockContext == null) { mMockContext = new MockContext(getContext() - .createPackageContext("com.android.car", Context.CONTEXT_IGNORE_SECURITY)); + .createPackageContext("com.android.car.test", Context.CONTEXT_IGNORE_SECURITY)); } return mMockContext; } @@ -393,6 +393,8 @@ public class MockedCarTestBase { switch (name) { case BLUETOOTH_SERVICE: return CarServiceTestApp.getAppContext().getSystemService(name); + case AUDIO_SERVICE: + return CarServiceTestApp.getAppContext().getSystemService(name); default: return super.getSystemService(name); } diff --git a/tests/carservice_test/src/com/android/car/GarageModeTest.java b/tests/carservice_test/src/com/android/car/garagemode/GarageModeServiceTest.java index 23153fc7a4..8a9a90d125 100644 --- a/tests/carservice_test/src/com/android/car/GarageModeTest.java +++ b/tests/carservice_test/src/com/android/car/garagemode/GarageModeServiceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2018 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. @@ -13,12 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.car; -import static org.junit.Assert.assertArrayEquals; +package com.android.car.garagemode; + import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import android.car.settings.CarSettings; @@ -31,16 +29,19 @@ import android.support.test.annotation.UiThreadTest; import android.support.test.filters.MediumTest; import android.support.test.runner.AndroidJUnit4; -import com.android.car.GarageModeService.GarageModePolicy; -import com.android.car.GarageModeService.WakeupTime; +import com.android.car.CarPowerManagementService; +import com.android.car.DeviceIdleControllerWrapper; +import com.android.car.R; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.List; + @RunWith(AndroidJUnit4.class) @MediumTest -public class GarageModeTest { - private static final int WAIT_FOR_COMPLETION_TIME = 3000;//ms +public class GarageModeServiceTest { + private static final int WAIT_FOR_COMPLETION_TIME_MS = 3000; @Test @UiThreadTest @@ -93,7 +94,7 @@ public class GarageModeTest { powerManagementService.doNotifyPrepareShutdown(false); assertTrue(garageMode.getGarageModeIndex() > 0); powerManagementService.doNotifyPowerOn(true); - assertEquals(0,garageMode.getGarageModeIndex()); + assertEquals(0, garageMode.getGarageModeIndex()); } @Test @@ -111,11 +112,11 @@ public class GarageModeTest { powerManagementService, controller, thread.getLooper()); - String[] policy = { + GarageModePolicy policy = new GarageModePolicy(new String[] { "15m,1", "6h,8", "1d,5", - }; + }); SharedPreferences prefs = getContext().getSharedPreferences("testPolicy", Context.MODE_PRIVATE); prefs.edit().putInt("garage_mode_index", 0).apply(); @@ -125,7 +126,7 @@ public class GarageModeTest { garageMode.onPrepareShutdown(false); garageMode.onShutdown(); assertEquals(6 * 60 * 60, garageMode.getWakeupTime()); - Thread.sleep(WAIT_FOR_COMPLETION_TIME); + Thread.sleep(WAIT_FOR_COMPLETION_TIME_MS); assertEquals(1, prefs.getInt("garage_mode_index", 0)); garageMode = new GarageModeServiceForTest(getContext(), @@ -141,84 +142,78 @@ public class GarageModeTest { garageMode.onPrepareShutdown(false); garageMode.onShutdown(); assertEquals(24 * 60 * 60, garageMode.getWakeupTime()); - Thread.sleep(WAIT_FOR_COMPLETION_TIME); + Thread.sleep(WAIT_FOR_COMPLETION_TIME_MS); assertEquals(9, prefs.getInt("garage_mode_index", 0)); } @Test public void testPolicyParserValid() throws Exception { - WakeupTime expected[] = new WakeupTime[]{ - new WakeupTime(15 * 60, 1), - new WakeupTime(6 * 60 * 60, 8), - new WakeupTime(24 * 60 * 60, 5), + WakeupInterval[] expected = new WakeupInterval[] { + new WakeupInterval(15 * 60, 1), + new WakeupInterval(6 * 60 * 60, 8), + new WakeupInterval(24 * 60 * 60, 5), }; - WakeupTime received[] = new GarageModePolicy(new String[] { + List<WakeupInterval> received = new GarageModePolicy(new String[] { "15m,1", "6h,8", "1d,5", - }).mWakeupTime; + }).getWakeupIntervals(); - assertEquals(expected.length, received.length); + assertEquals(expected.length, received.size()); for (int i = 0; i < expected.length; i++) { - assertEquals(expected[i].mWakeupTime, received[i].mWakeupTime); - assertEquals(expected[i].mNumAttempts, received[i].mNumAttempts); + assertEquals(expected[i].getWakeupInterval(), received.get(i).getWakeupInterval()); + assertEquals(expected[i].getNumAttempts(), received.get(i).getNumAttempts()); } } - @Test(expected=RuntimeException.class) - public void testPolicyParserNull() { - new GarageModePolicy(null); - } - @Test(expected=RuntimeException.class) - public void testPolicyParserEmptyArray() { - new GarageModePolicy(new String[] {}); - } - @Test(expected=RuntimeException.class) - public void testPolicyParserEmptyString() { - new GarageModePolicy(new String[] {""}); - } - @Test(expected=RuntimeException.class) - public void testPolicyParserMissingUnits() { - new GarageModePolicy(new String[] {"15,1"}); - } - @Test(expected=RuntimeException.class) - public void testPolicyParserInvalidUnits() { - new GarageModePolicy(new String[] {"15y,1"}); - } - @Test(expected=RuntimeException.class) - public void testPolicyParserNoCount() { - new GarageModePolicy(new String[] {"15m"}); - } - @Test(expected=RuntimeException.class) - public void testPolicyParserBadCount() { - new GarageModePolicy(new String[] {"15m,Q"}); - } - @Test(expected=RuntimeException.class) - public void testPolicyParserNegativeCount() { - new GarageModePolicy(new String[] {"15m,-1"}); - } - @Test(expected=RuntimeException.class) - public void testPolicyParserNoTime() { - new GarageModePolicy(new String[] {",1"}); - } - @Test(expected=RuntimeException.class) - public void testPolicyParserNoTimeValue() { - new GarageModePolicy(new String[] {"m,1"}); - } - @Test(expected=RuntimeException.class) - public void testPolicyParserBadTime() { - new GarageModePolicy(new String[] {"Qm,1"}); - } - @Test(expected=RuntimeException.class) - public void testPolicyParserNegativeTime() { - new GarageModePolicy(new String[] {"-10m,1"}); + @Test + public void testPolicyParser() { + GarageModePolicy policy; + + policy = new GarageModePolicy(null); + assertEquals(0, policy.getWakeupIntervals().size()); + + policy = new GarageModePolicy(new String[] {}); + assertEquals(0, policy.getWakeupIntervals().size()); + + policy = new GarageModePolicy(new String[] {""}); + assertEquals(0, policy.getWakeupIntervals().size()); + + policy = new GarageModePolicy(new String[] {"15,1"}); + assertEquals(0, policy.getWakeupIntervals().size()); + + policy = new GarageModePolicy(new String[] {"15y,1"}); + assertEquals(0, policy.getWakeupIntervals().size()); + + policy = new GarageModePolicy(new String[] {"15m"}); + assertEquals(0, policy.getWakeupIntervals().size()); + + policy = new GarageModePolicy(new String[] {"15m,Q"}); + assertEquals(0, policy.getWakeupIntervals().size()); + + policy = new GarageModePolicy(new String[] {"15m,-1"}); + assertEquals(0, policy.getWakeupIntervals().size()); + + policy = new GarageModePolicy(new String[] {",1"}); + assertEquals(0, policy.getWakeupIntervals().size()); + + policy = new GarageModePolicy(new String[] {"m,1"}); + assertEquals(0, policy.getWakeupIntervals().size()); + + policy = new GarageModePolicy(new String[] {"Qm,1"}); + assertEquals(0, policy.getWakeupIntervals().size()); + + policy = new GarageModePolicy(new String[] {"-10m,1"}); + assertEquals(0, policy.getWakeupIntervals().size()); + } @Test public void testPolicyInResource() throws Exception { // Test that the policy in the resource file parses fine. - assertNotNull(new GarageModePolicy(getContext().getResources().getStringArray( - R.array.config_garageModeCadence)).mWakeupTime); + GarageModePolicy policy = new GarageModePolicy(getContext().getResources().getStringArray( + R.array.config_garageModeCadence)); + assertTrue(policy.getWakeupIntervals().size() > 0); } private static class MockCarPowerManagementService extends CarPowerManagementService { @@ -232,14 +227,14 @@ public class GarageModeTest { } private static class GarageModeServiceForTest extends GarageModeService { - public GarageModeServiceForTest(Context context, + GarageModeServiceForTest(Context context, CarPowerManagementService powerManagementService, DeviceIdleControllerWrapper controllerWrapper, Looper looper) { super(context, powerManagementService, controllerWrapper, looper); } - public GarageModeServiceForTest(Context context, + GarageModeServiceForTest(Context context, CarPowerManagementService powerManagementService, DeviceIdleControllerWrapper controllerWrapper) { super(context, powerManagementService, controllerWrapper, Looper.myLooper()); diff --git a/tests/carservice_unit_test/src/com/android/car/CarLocationServiceTest.java b/tests/carservice_unit_test/src/com/android/car/CarLocationServiceTest.java index 8ac7d9b1db..b071e9947d 100644 --- a/tests/carservice_unit_test/src/com/android/car/CarLocationServiceTest.java +++ b/tests/carservice_unit_test/src/com/android/car/CarLocationServiceTest.java @@ -31,6 +31,7 @@ import android.car.hardware.CarSensorEvent; import android.car.hardware.CarSensorManager; import android.car.hardware.property.CarPropertyEvent; import android.car.hardware.property.ICarPropertyEventListener; +import android.car.user.CarUserManagerHelper; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -72,7 +73,9 @@ import java.util.stream.Collectors; * The following mocks are used: * 1. {@link Context} provides files and a mocked {@link LocationManager}. * 2. {@link LocationManager} provides dummy {@link Location}s. - * 3. {@link CarSensorService} registers a handler for sensor events and sends ignition-off events. + * 3. {@link CarPropertyService} registers a listener for ignition state events. + * 3. {@link CarPowerManagementService} registers a handler for power events. + * 4. {@link CarUserManagerHelper} tells whether or not the system user is headless. */ @RunWith(AndroidJUnit4.class) public class CarLocationServiceTest { @@ -85,6 +88,7 @@ public class CarLocationServiceTest { @Mock private LocationManager mMockLocationManager; @Mock private CarPropertyService mMockCarPropertyService; @Mock private CarPowerManagementService mMockCarPowerManagementService; + @Mock private CarUserManagerHelper mMockCarUserManagerHelper; /** * Initialize all of the objects with the @Mock annotation. @@ -95,7 +99,7 @@ public class CarLocationServiceTest { mContext = InstrumentationRegistry.getTargetContext(); mLatch = new CountDownLatch(1); mCarLocationService = new CarLocationService(mMockContext, mMockCarPowerManagementService, - mMockCarPropertyService) { + mMockCarPropertyService, mMockCarUserManagerHelper) { @Override void asyncOperation(Runnable operation) { super.asyncOperation(() -> { @@ -143,12 +147,13 @@ public class CarLocationServiceTest { mCarLocationService); verify(mMockContext).registerReceiver(eq(mCarLocationService), argument.capture()); IntentFilter intentFilter = argument.getValue(); - assertEquals(3, intentFilter.countActions()); + assertEquals(4, intentFilter.countActions()); String[] actions = {intentFilter.getAction(0), intentFilter.getAction(1), - intentFilter.getAction(2)}; + intentFilter.getAction(2), intentFilter.getAction(3)}; assertTrue(ArrayUtils.contains(actions, Intent.ACTION_LOCKED_BOOT_COMPLETED)); assertTrue(ArrayUtils.contains(actions, LocationManager.MODE_CHANGED_ACTION)); assertTrue(ArrayUtils.contains(actions, LocationManager.GPS_ENABLED_CHANGE_ACTION)); + assertTrue(ArrayUtils.contains(actions, Intent.ACTION_USER_SWITCHED)); verify(mMockCarPropertyService).registerListener( eq(CarSensorManager.SENSOR_TYPE_IGNITION_STATE), eq(0.0f), any()); } @@ -166,10 +171,11 @@ public class CarLocationServiceTest { /** * Test that the {@link CarLocationService} parses a location from a JSON serialization and then - * injects it into the {@link LocationManager} upon boot complete. + * injects it into the {@link LocationManager} upon boot complete if the system user is not + * headless. */ @Test - public void testLoadsLocation() throws IOException, InterruptedException { + public void testLoadsLocationOnLockedBootComplete() throws IOException, InterruptedException { long currentTime = System.currentTimeMillis(); long elapsedTime = SystemClock.elapsedRealtimeNanos(); long pastTime = currentTime - 60000; @@ -181,6 +187,7 @@ public class CarLocationServiceTest { when(mMockLocationManager.injectLocation(argument.capture())).thenReturn(true); when(mMockContext.getFileStreamPath("location_cache.json")) .thenReturn(mContext.getFileStreamPath(TEST_FILENAME)); + when(mMockCarUserManagerHelper.isHeadlessSystemUser()).thenReturn(false); mCarLocationService.onReceive(mMockContext, new Intent(Intent.ACTION_LOCKED_BOOT_COMPLETED)); @@ -196,6 +203,39 @@ public class CarLocationServiceTest { } /** + * Test that the {@link CarLocationService} parses a location from a JSON seialization and then + * injects it into the {@link LocationManager} upon user switch if the system user is headless. + */ + @Test + public void testLoadsLocationWithHeadlessSystemUser() throws IOException, InterruptedException { + long currentTime = System.currentTimeMillis(); + long elapsedTime = SystemClock.elapsedRealtimeNanos(); + long pastTime = currentTime - 60000; + writeCacheFile("{\"provider\": \"gps\", \"latitude\": 16.7666, \"longitude\": 3.0026," + + "\"accuracy\":12.3, \"captureTime\": " + pastTime + "}"); + ArgumentCaptor<Location> argument = ArgumentCaptor.forClass(Location.class); + when(mMockContext.getSystemService(Context.LOCATION_SERVICE)) + .thenReturn(mMockLocationManager); + when(mMockLocationManager.injectLocation(argument.capture())).thenReturn(true); + when(mMockContext.getFileStreamPath("location_cache.json")) + .thenReturn(mContext.getFileStreamPath(TEST_FILENAME)); + when(mMockCarUserManagerHelper.isHeadlessSystemUser()).thenReturn(true); + + Intent userSwitchedIntent = new Intent(Intent.ACTION_USER_SWITCHED); + userSwitchedIntent.putExtra(Intent.EXTRA_USER_HANDLE, 11); + mCarLocationService.onReceive(mMockContext, userSwitchedIntent); + mLatch.await(); + + Location location = argument.getValue(); + assertEquals("gps", location.getProvider()); + assertEquals(16.7666, location.getLatitude()); + assertEquals(3.0026, location.getLongitude()); + assertEquals(12.3f, location.getAccuracy()); + assertTrue(location.getTime() >= currentTime); + assertTrue(location.getElapsedRealtimeNanos() >= elapsedTime); + } + + /** * Test that the {@link CarLocationService} does not inject a location if there is no location * cache file. */ diff --git a/tests/carservice_unit_test/src/com/android/car/CarUserManagerHelperTest.java b/tests/carservice_unit_test/src/com/android/car/CarUserManagerHelperTest.java index 44cc0d6ae5..7f53da5655 100644 --- a/tests/carservice_unit_test/src/com/android/car/CarUserManagerHelperTest.java +++ b/tests/carservice_unit_test/src/com/android/car/CarUserManagerHelperTest.java @@ -18,8 +18,12 @@ package com.android.car; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; import android.app.ActivityManager; import android.car.user.CarUserManagerHelper; @@ -30,11 +34,13 @@ import android.content.IntentFilter; import android.content.pm.UserInfo; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; +import android.os.Bundle; import android.os.Handler; import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; import org.junit.Before; @@ -42,6 +48,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import java.util.ArrayList; @@ -54,10 +61,11 @@ import java.util.List; * The following mocks are used: * 1. {@link Context} provides system services and resources. * 2. {@link UserManager} provides dummy users and user info. - * 3. {@link ActivityManager} provides dummy current process user. + * 3. {@link ActivityManager} to verify user switch is invoked. * 4. {@link CarUserManagerHelper.OnUsersUpdateListener} registers a listener for user updates. */ @RunWith(AndroidJUnit4.class) +@SmallTest public class CarUserManagerHelperTest { @Mock private Context mContext; @@ -68,25 +76,37 @@ public class CarUserManagerHelperTest { @Mock private CarUserManagerHelper.OnUsersUpdateListener mTestListener; - private CarUserManagerHelper mHelper; + private CarUserManagerHelper mCarUserManagerHelper; private UserInfo mCurrentProcessUser; private UserInfo mSystemUser; private String mGuestUserName = "testGuest"; private String mTestUserName = "testUser"; + private int mForegroundUserId; + private UserInfo mForegroundUser; @Before public void setUpMocksAndVariables() throws Exception { MockitoAnnotations.initMocks(this); - when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager); - when(mContext.getSystemService(Context.ACTIVITY_SERVICE)).thenReturn(mActivityManager); - when(mContext.getResources()) - .thenReturn(InstrumentationRegistry.getTargetContext().getResources()); - when(mContext.getApplicationContext()).thenReturn(mContext); - mHelper = new CarUserManagerHelper(mContext); + doReturn(mUserManager).when(mContext).getSystemService(Context.USER_SERVICE); + doReturn(mActivityManager).when(mContext).getSystemService(Context.ACTIVITY_SERVICE); + doReturn(InstrumentationRegistry.getTargetContext().getResources()) + .when(mContext).getResources(); + doReturn(mContext).when(mContext).getApplicationContext(); + mCarUserManagerHelper = new CarUserManagerHelper(mContext); mCurrentProcessUser = createUserInfoForId(UserHandle.myUserId()); mSystemUser = createUserInfoForId(UserHandle.USER_SYSTEM); - when(mUserManager.getUserInfo(UserHandle.myUserId())).thenReturn(mCurrentProcessUser); + doReturn(mCurrentProcessUser).when(mUserManager).getUserInfo(UserHandle.myUserId()); + + // Get the ID of the foreground user running this test. + // We cannot mock the foreground user since getCurrentUser is static. + // We cannot rely on foreground_id != system_id, they could be the same user. + mForegroundUserId = ActivityManager.getCurrentUser(); + mForegroundUser = createUserInfoForId(mForegroundUserId); + + // Restore the non-headless state before every test. Individual tests can set the property + // to true to test the headless system user scenario. + SystemProperties.set("android.car.systemuser.headless", "false"); } @Test @@ -94,10 +114,10 @@ public class CarUserManagerHelperTest { UserInfo testInfo = new UserInfo(); testInfo.id = UserHandle.USER_SYSTEM; - assertThat(mHelper.isSystemUser(testInfo)).isTrue(); + assertThat(mCarUserManagerHelper.isSystemUser(testInfo)).isTrue(); testInfo.id = UserHandle.USER_SYSTEM + 2; // Make it different than system id. - assertThat(mHelper.isSystemUser(testInfo)).isFalse(); + assertThat(mCarUserManagerHelper.isSystemUser(testInfo)).isFalse(); } // System user will not be returned when calling get all users. @@ -108,92 +128,67 @@ public class CarUserManagerHelperTest { UserInfo otherUser2 = createUserInfoForId(11); UserInfo otherUser3 = createUserInfoForId(12); - List<UserInfo> testUsers = new ArrayList<>(); - testUsers.add(mSystemUser); - testUsers.add(otherUser1); - testUsers.add(otherUser2); - testUsers.add(otherUser3); + mockGetUsers(mSystemUser, otherUser1, otherUser2, otherUser3); - when(mUserManager.getUsers(true)).thenReturn(testUsers); - - // Should return 3 users that don't have SYSTEM USER id. - assertThat(mHelper.getAllUsers()).hasSize(3); - assertThat(mHelper.getAllUsers()) - .containsExactly(otherUser1, otherUser2, otherUser3); + assertThat(mCarUserManagerHelper.getAllUsers()) + .containsExactly(otherUser1, otherUser2, otherUser3); } @Test - public void testHeadlessUser0GetAllUsersWithActiveForegroundUser_NotReturnSystemUser() { - SystemProperties.set("android.car.systemuser.headless", "true"); - mCurrentProcessUser = createUserInfoForId(10); - - UserInfo otherUser1 = createUserInfoForId(11); - UserInfo otherUser2 = createUserInfoForId(12); - UserInfo otherUser3 = createUserInfoForId(13); + public void testGetAllSwitchableUsers() { + // Create two non-foreground users. + UserInfo user1 = createUserInfoForId(mForegroundUserId + 1); + UserInfo user2 = createUserInfoForId(mForegroundUserId + 2); - List<UserInfo> testUsers = new ArrayList<>(); - testUsers.add(mSystemUser); - testUsers.add(mCurrentProcessUser); - testUsers.add(otherUser1); - testUsers.add(otherUser2); - testUsers.add(otherUser3); + mockGetUsers(mForegroundUser, user1, user2); - when(mUserManager.getUsers(true)).thenReturn(testUsers); + // Should return all non-foreground users. + assertThat(mCarUserManagerHelper.getAllSwitchableUsers()).containsExactly(user1, user2); + } - assertThat(mHelper.getAllUsers().size()).isEqualTo(4); - assertThat(mHelper.getAllUsers()) - .containsExactly(mCurrentProcessUser, otherUser1, otherUser2, otherUser3); + @Test + public void testGetAllPersistentUsers() { + // Create two non-ephemeral users. + UserInfo user1 = createUserInfoForId(mForegroundUserId); + UserInfo user2 = createUserInfoForId(mForegroundUserId + 1); + // Create two ephemeral users. + UserInfo user3 = new UserInfo( + /* id= */mForegroundUserId + 2, /* name = */ "user3", UserInfo.FLAG_EPHEMERAL); + UserInfo user4 = new UserInfo( + /* id= */mForegroundUserId + 3, /* name = */ "user4", UserInfo.FLAG_EPHEMERAL); + + mockGetUsers(user1, user2, user3, user4); + + // Should return all non-ephemeral users. + assertThat(mCarUserManagerHelper.getAllPersistentUsers()).containsExactly(user1, user2); } @Test - public void testGetAllSwitchableUsers() { - UserInfo user1 = createUserInfoForId(10); + public void testGetAllAdminUsers() { + // Create two admin, and two non-admin users. + UserInfo user1 = new UserInfo(/* id= */ 10, /* name = */ "user10", UserInfo.FLAG_ADMIN); UserInfo user2 = createUserInfoForId(11); UserInfo user3 = createUserInfoForId(12); + UserInfo user4 = new UserInfo(/* id= */ 13, /* name = */ "user13", UserInfo.FLAG_ADMIN); - List<UserInfo> testUsers = new ArrayList<>(); - testUsers.add(mSystemUser); - testUsers.add(user1); - testUsers.add(user2); - testUsers.add(user3); - - when(mUserManager.getUsers(true)).thenReturn(new ArrayList<>(testUsers)); - - // Should return all 3 non-system users. - assertThat(mHelper.getAllUsers().size()) - .isEqualTo(3); + mockGetUsers(user1, user2, user3, user4); - when(mUserManager.getUserInfo(UserHandle.myUserId())).thenReturn(user1); - // Should return user 10, 11 and 12. - assertThat(mHelper.getAllSwitchableUsers().size()) - .isEqualTo(3); - assertThat(mHelper.getAllSwitchableUsers()).contains(user1); - assertThat(mHelper.getAllSwitchableUsers()).contains(user2); - assertThat(mHelper.getAllSwitchableUsers()).contains(user3); + // Should return only admin users. + assertThat(mCarUserManagerHelper.getAllAdminUsers()).containsExactly(user1, user4); } - // Get all users for headless user 0 model should exclude system user by default. @Test - public void testHeadlessUser0GetAllSwitchableUsers() { - SystemProperties.set("android.car.systemuser.headless", "true"); + public void testGetAllUsersExceptGuests() { + // Create two users and a guest user. UserInfo user1 = createUserInfoForId(10); - UserInfo user2 = createUserInfoForId(11); - UserInfo user3 = createUserInfoForId(12); - - List<UserInfo> testUsers = new ArrayList<>(); - testUsers.add(mSystemUser); - testUsers.add(user1); - testUsers.add(user2); - testUsers.add(user3); + UserInfo user2 = createUserInfoForId(12); + UserInfo user3 = new UserInfo(/* id= */ 13, /* name = */ "user13", UserInfo.FLAG_GUEST); - when(mUserManager.getUsers(true)).thenReturn(new ArrayList<>(testUsers)); + mockGetUsers(user1, user2, user3); - // Should return all 3 non-system users. - assertThat(mHelper.getAllUsers()).hasSize(3); - - when(mUserManager.getUserInfo(UserHandle.myUserId())).thenReturn(user1); - // Should return user 10, 11 and 12. - assertThat(mHelper.getAllSwitchableUsers()).containsExactly(user1, user2, user3); + // Should not return guests. + assertThat(mCarUserManagerHelper.getAllUsersExceptGuests()) + .containsExactly(user1, user2); } @Test @@ -202,127 +197,304 @@ public class CarUserManagerHelperTest { // System user cannot be removed. testInfo.id = UserHandle.USER_SYSTEM; - assertThat(mHelper.canUserBeRemoved(testInfo)).isFalse(); + assertThat(mCarUserManagerHelper.canUserBeRemoved(testInfo)).isFalse(); testInfo.id = UserHandle.USER_SYSTEM + 2; // Make it different than system id. - assertThat(mHelper.canUserBeRemoved(testInfo)).isTrue(); + assertThat(mCarUserManagerHelper.canUserBeRemoved(testInfo)).isTrue(); } @Test public void testCurrentProcessCanAddUsers() { - when(mUserManager.hasUserRestriction(UserManager.DISALLOW_ADD_USER)).thenReturn(false); - assertThat(mHelper.canCurrentProcessAddUsers()).isTrue(); + doReturn(false).when(mUserManager) + .hasUserRestriction(UserManager.DISALLOW_ADD_USER); + assertThat(mCarUserManagerHelper.canCurrentProcessAddUsers()).isTrue(); - when(mUserManager.hasUserRestriction(UserManager.DISALLOW_ADD_USER)).thenReturn(true); - assertThat(mHelper.canCurrentProcessAddUsers()).isFalse(); + doReturn(true).when(mUserManager) + .hasUserRestriction(UserManager.DISALLOW_ADD_USER); + assertThat(mCarUserManagerHelper.canCurrentProcessAddUsers()).isFalse(); } @Test public void testCurrentProcessCanRemoveUsers() { - when(mUserManager.hasUserRestriction(UserManager.DISALLOW_REMOVE_USER)).thenReturn(false); - assertThat(mHelper.canCurrentProcessRemoveUsers()).isTrue(); + doReturn(false).when(mUserManager) + .hasUserRestriction(UserManager.DISALLOW_REMOVE_USER); + assertThat(mCarUserManagerHelper.canCurrentProcessRemoveUsers()).isTrue(); - when(mUserManager.hasUserRestriction(UserManager.DISALLOW_REMOVE_USER)).thenReturn(true); - assertThat(mHelper.canCurrentProcessRemoveUsers()).isFalse(); + doReturn(true).when(mUserManager) + .hasUserRestriction(UserManager.DISALLOW_REMOVE_USER); + assertThat(mCarUserManagerHelper.canCurrentProcessRemoveUsers()).isFalse(); } @Test public void testCurrentProcessCanSwitchUsers() { - when(mUserManager.hasUserRestriction(UserManager.DISALLOW_USER_SWITCH)).thenReturn(false); - assertThat(mHelper.canCurrentProcessSwitchUsers()).isTrue(); + doReturn(false).when(mUserManager) + .hasUserRestriction(UserManager.DISALLOW_USER_SWITCH); + assertThat(mCarUserManagerHelper.canCurrentProcessSwitchUsers()).isTrue(); - when(mUserManager.hasUserRestriction(UserManager.DISALLOW_USER_SWITCH)).thenReturn(true); - assertThat(mHelper.canCurrentProcessSwitchUsers()).isFalse(); + doReturn(true).when(mUserManager) + .hasUserRestriction(UserManager.DISALLOW_USER_SWITCH); + assertThat(mCarUserManagerHelper.canCurrentProcessSwitchUsers()).isFalse(); } @Test public void testCurrentGuestProcessCannotModifyAccounts() { - assertThat(mHelper.canCurrentProcessModifyAccounts()).isTrue(); + assertThat(mCarUserManagerHelper.canCurrentProcessModifyAccounts()).isTrue(); + + doReturn(true).when(mUserManager).isGuestUser(); - when(mUserManager.isGuestUser()).thenReturn(true); - assertThat(mHelper.canCurrentProcessModifyAccounts()).isFalse(); + assertThat(mCarUserManagerHelper.canCurrentProcessModifyAccounts()).isFalse(); } @Test public void testCurrentDemoProcessCannotModifyAccounts() { - assertThat(mHelper.canCurrentProcessModifyAccounts()).isTrue(); + assertThat(mCarUserManagerHelper.canCurrentProcessModifyAccounts()).isTrue(); + + doReturn(true).when(mUserManager).isDemoUser(); - when(mUserManager.isDemoUser()).thenReturn(true); - assertThat(mHelper.canCurrentProcessModifyAccounts()).isFalse(); + assertThat(mCarUserManagerHelper.canCurrentProcessModifyAccounts()).isFalse(); } @Test public void testCurrentDisallowModifyAccountsProcessIsEnforced() { - assertThat(mHelper.canCurrentProcessModifyAccounts()).isTrue(); + assertThat(mCarUserManagerHelper.canCurrentProcessModifyAccounts()).isTrue(); + + doReturn(true).when(mUserManager) + .hasUserRestriction(UserManager.DISALLOW_MODIFY_ACCOUNTS); + + assertThat(mCarUserManagerHelper.canCurrentProcessModifyAccounts()).isFalse(); + } + + @Test + public void testGetMaxSupportedUsers() { + SystemProperties.set("fw.max_users", "11"); + + assertThat(mCarUserManagerHelper.getMaxSupportedUsers()).isEqualTo(11); + + // In headless user 0 model, we want to exclude the system user. + SystemProperties.set("android.car.systemuser.headless", "true"); + assertThat(mCarUserManagerHelper.getMaxSupportedUsers()).isEqualTo(10); + } + + @Test + public void testGetMaxSupportedRealUsers() { + SystemProperties.set("fw.max_users", "7"); + + // Create three managed profiles, and two normal users. + UserInfo user1 = createUserInfoForId(10); + UserInfo user2 = + new UserInfo(/* id= */ 11, /* name = */ "user11", UserInfo.FLAG_MANAGED_PROFILE); + UserInfo user3 = + new UserInfo(/* id= */ 12, /* name = */ "user12", UserInfo.FLAG_MANAGED_PROFILE); + UserInfo user4 = createUserInfoForId(13); + UserInfo user5 = + new UserInfo(/* id= */ 14, /* name = */ "user14", UserInfo.FLAG_MANAGED_PROFILE); + + mockGetUsers(user1, user2, user3, user4, user5); + + // Max users - # managed profiles. + assertThat(mCarUserManagerHelper.getMaxSupportedRealUsers()).isEqualTo(4); + } + + @Test + public void testIsUserLimitReached() { + UserInfo user1 = createUserInfoForId(10); + UserInfo user2 = + new UserInfo(/* id= */ 11, /* name = */ "user11", UserInfo.FLAG_MANAGED_PROFILE); + UserInfo user3 = + new UserInfo(/* id= */ 12, /* name = */ "user12", UserInfo.FLAG_MANAGED_PROFILE); + UserInfo user4 = createUserInfoForId(13); - when(mUserManager.hasUserRestriction(UserManager.DISALLOW_MODIFY_ACCOUNTS)) - .thenReturn(true); - assertThat(mHelper.canCurrentProcessModifyAccounts()).isFalse(); + mockGetUsers(user1, user2, user3, user4); + + SystemProperties.set("fw.max_users", "5"); + assertThat(mCarUserManagerHelper.isUserLimitReached()).isFalse(); + + SystemProperties.set("fw.max_users", "4"); + assertThat(mCarUserManagerHelper.isUserLimitReached()).isTrue(); + } + + @Test + public void testHeadlessSystemUser_IsUserLimitReached() { + SystemProperties.set("android.car.systemuser.headless", "true"); + UserInfo user1 = createUserInfoForId(10); + UserInfo user2 = + new UserInfo(/* id= */ 11, /* name = */ "user11", UserInfo.FLAG_MANAGED_PROFILE); + UserInfo user3 = + new UserInfo(/* id= */ 12, /* name = */ "user12", UserInfo.FLAG_MANAGED_PROFILE); + UserInfo user4 = createUserInfoForId(13); + + mockGetUsers(mSystemUser, user1, user2, user3, user4); + + SystemProperties.set("fw.max_users", "6"); + assertThat(mCarUserManagerHelper.isUserLimitReached()).isFalse(); + + SystemProperties.set("fw.max_users", "5"); + assertThat(mCarUserManagerHelper.isUserLimitReached()).isTrue(); + } + + @Test + public void testIsUserLimitReachedIgnoresGuests() { + SystemProperties.set("fw.max_users", "5"); + + UserInfo user1 = createUserInfoForId(10); + UserInfo user2 = + new UserInfo(/* id= */ 11, /* name = */ "user11", UserInfo.FLAG_MANAGED_PROFILE); + UserInfo user3 = + new UserInfo(/* id= */ 12, /* name = */ "user12", UserInfo.FLAG_MANAGED_PROFILE); + UserInfo user4 = createUserInfoForId(13); + UserInfo user5 = new UserInfo(/* id= */ 14, /* name = */ "user14", UserInfo.FLAG_GUEST); + UserInfo user6 = createUserInfoForId(15); + + mockGetUsers(user1, user2, user3, user4); + assertThat(mCarUserManagerHelper.isUserLimitReached()).isFalse(); + + // Add guest user. Verify it doesn't affect the limit. + mockGetUsers(user1, user2, user3, user4, user5); + assertThat(mCarUserManagerHelper.isUserLimitReached()).isFalse(); + + // Add normal user. Limit is reached + mockGetUsers(user1, user2, user3, user4, user5, user6); + assertThat(mCarUserManagerHelper.isUserLimitReached()).isTrue(); } @Test public void testCreateNewAdminUser() { + // Make sure current user is admin, since only admins can create other admins. + doReturn(true).when(mUserManager).isAdminUser(); + // Verify createUser on UserManager gets called. - mHelper.createNewAdminUser(mTestUserName); + mCarUserManagerHelper.createNewAdminUser(mTestUserName); verify(mUserManager).createUser(mTestUserName, UserInfo.FLAG_ADMIN); - when(mUserManager.createUser(mTestUserName, UserInfo.FLAG_ADMIN)).thenReturn(null); - assertThat(mHelper.createNewAdminUser(mTestUserName)).isNull(); + doReturn(null).when(mUserManager).createUser(mTestUserName, UserInfo.FLAG_ADMIN); + assertThat(mCarUserManagerHelper.createNewAdminUser(mTestUserName)).isNull(); UserInfo newUser = new UserInfo(); newUser.name = mTestUserName; - when(mUserManager.createUser(mTestUserName, UserInfo.FLAG_ADMIN)).thenReturn(newUser); - assertThat(mHelper.createNewAdminUser(mTestUserName)).isEqualTo(newUser); + doReturn(newUser).when(mUserManager).createUser(mTestUserName, UserInfo.FLAG_ADMIN); + assertThat(mCarUserManagerHelper.createNewAdminUser(mTestUserName)).isEqualTo(newUser); + } + + @Test + public void testAdminsCanCreateAdmins() { + String newAdminName = "Test new admin"; + UserInfo expectedAdmin = new UserInfo(); + expectedAdmin.name = newAdminName; + doReturn(expectedAdmin).when(mUserManager).createUser(newAdminName, UserInfo.FLAG_ADMIN); + + // Admins can create other admins. + doReturn(true).when(mUserManager).isAdminUser(); + UserInfo actualAdmin = mCarUserManagerHelper.createNewAdminUser(newAdminName); + assertThat(actualAdmin).isEqualTo(expectedAdmin); + } + + @Test + public void testNonAdminsCanNotCreateAdmins() { + String newAdminName = "Test new admin"; + UserInfo expectedAdmin = new UserInfo(); + expectedAdmin.name = newAdminName; + doReturn(expectedAdmin).when(mUserManager).createUser(newAdminName, UserInfo.FLAG_ADMIN); + + // Test that non-admins cannot create new admins. + doReturn(false).when(mUserManager).isAdminUser(); // Current user non-admin. + assertThat(mCarUserManagerHelper.createNewAdminUser(newAdminName)).isNull(); + } + + @Test + public void testSystemUserCanCreateAdmins() { + String newAdminName = "Test new admin"; + UserInfo expectedAdmin = new UserInfo(); + expectedAdmin.name = newAdminName; + + doReturn(expectedAdmin).when(mUserManager).createUser(newAdminName, UserInfo.FLAG_ADMIN); + + // System user can create admins. + doReturn(true).when(mUserManager).isSystemUser(); + UserInfo actualAdmin = mCarUserManagerHelper.createNewAdminUser(newAdminName); + assertThat(actualAdmin).isEqualTo(expectedAdmin); } @Test public void testCreateNewNonAdminUser() { // Verify createUser on UserManager gets called. - mHelper.createNewNonAdminUser(mTestUserName); + mCarUserManagerHelper.createNewNonAdminUser(mTestUserName); verify(mUserManager).createUser(mTestUserName, 0); - when(mUserManager.createUser(mTestUserName, 0)).thenReturn(null); - assertThat(mHelper.createNewNonAdminUser(mTestUserName)).isNull(); + doReturn(null).when(mUserManager).createUser(mTestUserName, 0); + assertThat(mCarUserManagerHelper.createNewNonAdminUser(mTestUserName)).isNull(); UserInfo newUser = new UserInfo(); newUser.name = mTestUserName; - when(mUserManager.createUser(mTestUserName, 0)).thenReturn(newUser); - assertThat(mHelper.createNewNonAdminUser(mTestUserName)).isEqualTo(newUser); + doReturn(newUser).when(mUserManager).createUser(mTestUserName, 0); + assertThat(mCarUserManagerHelper.createNewNonAdminUser(mTestUserName)).isEqualTo(newUser); + } + + @Test + public void testCannotRemoveSystemUser() { + assertThat(mCarUserManagerHelper.removeUser(mSystemUser, mGuestUserName)).isFalse(); + } + + @Test + public void testAdminsCanRemoveOtherUsers() { + int idToRemove = mCurrentProcessUser.id + 2; + UserInfo userToRemove = createUserInfoForId(idToRemove); + + doReturn(true).when(mUserManager).removeUser(idToRemove); + + // If Admin is removing non-current, non-system user, simply calls removeUser. + doReturn(true).when(mUserManager).isAdminUser(); + assertThat(mCarUserManagerHelper.removeUser(userToRemove, mGuestUserName)).isTrue(); + verify(mUserManager).removeUser(idToRemove); + } + + @Test + public void testNonAdminsCanNotRemoveOtherUsers() { + UserInfo otherUser = createUserInfoForId(mCurrentProcessUser.id + 2); + + // Make current user non-admin. + doReturn(false).when(mUserManager).isAdminUser(); + + // Mock so that removeUser always pretends it's successful. + doReturn(true).when(mUserManager).removeUser(anyInt()); + + // If Non-Admin is trying to remove someone other than themselves, they should fail. + assertThat(mCarUserManagerHelper.removeUser(otherUser, mGuestUserName)).isFalse(); + verify(mUserManager, never()).removeUser(otherUser.id); } @Test - public void testRemoveUser() { + public void testRemoveLastActiveUser() { // Cannot remove system user. - assertThat(mHelper.removeUser(mSystemUser, mGuestUserName)).isFalse(); + assertThat(mCarUserManagerHelper.removeUser(mSystemUser, mGuestUserName)).isFalse(); - // Removing non-current, non-system user, simply calls removeUser. - UserInfo userToRemove = createUserInfoForId(mCurrentProcessUser.id + 2); + UserInfo adminInfo = new UserInfo(/* id= */10, "admin", UserInfo.FLAG_ADMIN); + mockGetUsers(adminInfo); - mHelper.removeUser(userToRemove, mGuestUserName); - verify(mUserManager).removeUser(mCurrentProcessUser.id + 2); + assertThat(mCarUserManagerHelper.removeUser(adminInfo, mGuestUserName)) + .isEqualTo(false); } @Test public void testSwitchToGuest() { - mHelper.startNewGuestSession(mGuestUserName); + mCarUserManagerHelper.startNewGuestSession(mGuestUserName); verify(mUserManager).createGuest(mContext, mGuestUserName); - UserInfo guestInfo = new UserInfo(21, mGuestUserName, UserInfo.FLAG_GUEST); - when(mUserManager.createGuest(mContext, mGuestUserName)).thenReturn(guestInfo); - mHelper.startNewGuestSession(mGuestUserName); + UserInfo guestInfo = new UserInfo(/* id= */21, mGuestUserName, UserInfo.FLAG_GUEST); + doReturn(guestInfo).when(mUserManager).createGuest(mContext, mGuestUserName); + mCarUserManagerHelper.startNewGuestSession(mGuestUserName); verify(mActivityManager).switchUser(21); } @Test public void testGetUserIcon() { - mHelper.getUserIcon(mCurrentProcessUser); + mCarUserManagerHelper.getUserIcon(mCurrentProcessUser); verify(mUserManager).getUserIcon(mCurrentProcessUser.id); } @Test public void testScaleUserIcon() { Bitmap fakeIcon = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); - Drawable scaledIcon = mHelper.scaleUserIcon(fakeIcon, 300); + Drawable scaledIcon = mCarUserManagerHelper.scaleUserIcon(fakeIcon, 300); assertThat(scaledIcon.getIntrinsicWidth()).isEqualTo(300); assertThat(scaledIcon.getIntrinsicHeight()).isEqualTo(300); } @@ -331,13 +503,107 @@ public class CarUserManagerHelperTest { public void testSetUserName() { UserInfo testInfo = createUserInfoForId(mCurrentProcessUser.id + 3); String newName = "New Test Name"; - mHelper.setUserName(testInfo, newName); + mCarUserManagerHelper.setUserName(testInfo, newName); verify(mUserManager).setUserName(mCurrentProcessUser.id + 3, newName); } @Test + public void testIsCurrentProcessSystemUser() { + doReturn(true).when(mUserManager).isAdminUser(); + assertThat(mCarUserManagerHelper.isCurrentProcessAdminUser()).isTrue(); + + doReturn(false).when(mUserManager).isAdminUser(); + assertThat(mCarUserManagerHelper.isCurrentProcessAdminUser()).isFalse(); + } + + @Test + public void testAssignAdminPrivileges() { + int userId = 30; + UserInfo testInfo = createUserInfoForId(userId); + + // Test that non-admins cannot assign admin privileges. + doReturn(false).when(mUserManager).isAdminUser(); // Current user non-admin. + mCarUserManagerHelper.assignAdminPrivileges(testInfo); + verify(mUserManager, never()).setUserAdmin(userId); + + // Admins can assign admin privileges. + doReturn(true).when(mUserManager).isAdminUser(); + mCarUserManagerHelper.assignAdminPrivileges(testInfo); + verify(mUserManager).setUserAdmin(userId); + } + + @Test + public void testSetUserRestriction() { + int userId = 20; + UserInfo testInfo = createUserInfoForId(userId); + + mCarUserManagerHelper.setUserRestriction( + testInfo, UserManager.DISALLOW_ADD_USER, /* enable= */ true); + verify(mUserManager).setUserRestriction( + UserManager.DISALLOW_ADD_USER, true, UserHandle.of(userId)); + + mCarUserManagerHelper.setUserRestriction( + testInfo, UserManager.DISALLOW_REMOVE_USER, /* enable= */ false); + verify(mUserManager).setUserRestriction( + UserManager.DISALLOW_REMOVE_USER, false, UserHandle.of(userId)); + } + + @Test + public void testDefaultNonAdminRestrictions() { + String testUserName = "Test User"; + int userId = 20; + UserInfo newNonAdmin = createUserInfoForId(userId); + + doReturn(newNonAdmin).when(mUserManager).createUser(testUserName, /* flags= */ 0); + + mCarUserManagerHelper.createNewNonAdminUser(testUserName); + + verify(mUserManager).setUserRestriction( + UserManager.DISALLOW_FACTORY_RESET, /* enable= */ true, UserHandle.of(userId)); + verify(mUserManager).setUserRestriction( + UserManager.DISALLOW_SMS, /* enable= */ false, UserHandle.of(userId)); + verify(mUserManager).setUserRestriction( + UserManager.DISALLOW_OUTGOING_CALLS, /* enable= */ false, UserHandle.of(userId)); + } + + @Test + public void testDefaultGuestRestrictions() { + int guestRestrictionsExpectedCount = 7; + + ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class); + mCarUserManagerHelper.initDefaultGuestRestrictions(); + + verify(mUserManager).setDefaultGuestRestrictions(bundleCaptor.capture()); + Bundle guestRestrictions = bundleCaptor.getValue(); + + assertThat(guestRestrictions.keySet()).hasSize(guestRestrictionsExpectedCount); + assertThat(guestRestrictions.getBoolean(UserManager.DISALLOW_FACTORY_RESET)).isTrue(); + assertThat(guestRestrictions.getBoolean(UserManager.DISALLOW_REMOVE_USER)).isTrue(); + assertThat(guestRestrictions.getBoolean(UserManager.DISALLOW_MODIFY_ACCOUNTS)).isTrue(); + assertThat(guestRestrictions.getBoolean(UserManager.DISALLOW_OUTGOING_CALLS)).isTrue(); + assertThat(guestRestrictions.getBoolean(UserManager.DISALLOW_SMS)).isTrue(); + assertThat(guestRestrictions.getBoolean(UserManager.DISALLOW_INSTALL_APPS)).isTrue(); + assertThat(guestRestrictions.getBoolean(UserManager.DISALLOW_UNINSTALL_APPS)).isTrue(); + } + + @Test + public void testAssigningAdminPrivilegesRemovesNonAdminRestrictions() { + int testUserId = 30; + boolean restrictionEnabled = false; + UserInfo testInfo = createUserInfoForId(testUserId); + + // Only admins can assign privileges. + doReturn(true).when(mUserManager).isAdminUser(); + + mCarUserManagerHelper.assignAdminPrivileges(testInfo); + + verify(mUserManager).setUserRestriction( + UserManager.DISALLOW_FACTORY_RESET, restrictionEnabled, UserHandle.of(testUserId)); + } + + @Test public void testRegisterUserChangeReceiver() { - mHelper.registerOnUsersUpdateListener(mTestListener); + mCarUserManagerHelper.registerOnUsersUpdateListener(mTestListener); ArgumentCaptor<BroadcastReceiver> receiverCaptor = ArgumentCaptor.forClass(BroadcastReceiver.class); @@ -375,13 +641,148 @@ public class CarUserManagerHelperTest { assertThat(handlerCaptor.getValue()).isNull(); // Unregister the receiver. - mHelper.unregisterOnUsersUpdateListener(); + mCarUserManagerHelper.unregisterOnUsersUpdateListener(mTestListener); verify(mContext).unregisterReceiver(receiverCaptor.getValue()); } + @Test + public void testMultipleRegistrationsOfSameListener() { + CarUserManagerHelper.OnUsersUpdateListener listener = + Mockito.mock(CarUserManagerHelper.OnUsersUpdateListener.class); + + ArgumentCaptor<BroadcastReceiver> receiverCaptor = + ArgumentCaptor.forClass(BroadcastReceiver.class); + + mCarUserManagerHelper.registerOnUsersUpdateListener(listener); + mCarUserManagerHelper.registerOnUsersUpdateListener(listener); + // Even for multiple registrations of the same listener, broadcast receiver registered once. + verify(mContext, times(1)) + .registerReceiverAsUser(receiverCaptor.capture(), any(), any(), any(), any()); + + // Verify that calling the receiver calls the listener. + receiverCaptor.getValue().onReceive(mContext, new Intent()); + verify(listener).onUsersUpdate(); + + // Verify that a single removal unregisters the listener. + mCarUserManagerHelper.unregisterOnUsersUpdateListener(listener); + verify(mContext).unregisterReceiver(any()); + } + + @Test + public void testMultipleUnregistrationsOfTheSameListener() { + CarUserManagerHelper.OnUsersUpdateListener listener = + Mockito.mock(CarUserManagerHelper.OnUsersUpdateListener.class); + mCarUserManagerHelper.registerOnUsersUpdateListener(listener); + + // Verify that a multiple unregistrations cause only one unregister for broadcast receiver. + mCarUserManagerHelper.unregisterOnUsersUpdateListener(listener); + mCarUserManagerHelper.unregisterOnUsersUpdateListener(listener); + mCarUserManagerHelper.unregisterOnUsersUpdateListener(listener); + verify(mContext, times(1)).unregisterReceiver(any()); + } + + @Test + public void testUnregisterReceiverCalledAfterAllListenersUnregister() { + CarUserManagerHelper.OnUsersUpdateListener listener1 = + Mockito.mock(CarUserManagerHelper.OnUsersUpdateListener.class); + CarUserManagerHelper.OnUsersUpdateListener listener2 = + Mockito.mock(CarUserManagerHelper.OnUsersUpdateListener.class); + + mCarUserManagerHelper.registerOnUsersUpdateListener(listener1); + mCarUserManagerHelper.registerOnUsersUpdateListener(listener2); + + mCarUserManagerHelper.unregisterOnUsersUpdateListener(listener1); + verify(mContext, never()).unregisterReceiver(any()); + + mCarUserManagerHelper.unregisterOnUsersUpdateListener(listener2); + verify(mContext, times(1)).unregisterReceiver(any()); + } + + @Test + public void testRegisteringMultipleListeners() { + CarUserManagerHelper.OnUsersUpdateListener listener1 = + Mockito.mock(CarUserManagerHelper.OnUsersUpdateListener.class); + CarUserManagerHelper.OnUsersUpdateListener listener2 = + Mockito.mock(CarUserManagerHelper.OnUsersUpdateListener.class); + ArgumentCaptor<BroadcastReceiver> receiverCaptor = + ArgumentCaptor.forClass(BroadcastReceiver.class); + + mCarUserManagerHelper.registerOnUsersUpdateListener(listener1); + mCarUserManagerHelper.registerOnUsersUpdateListener(listener2); + verify(mContext, times(1)) + .registerReceiverAsUser(receiverCaptor.capture(), any(), any(), any(), any()); + + // Verify that calling the receiver calls both listeners. + receiverCaptor.getValue().onReceive(mContext, new Intent()); + verify(listener1).onUsersUpdate(); + verify(listener2).onUsersUpdate(); + } + + @Test + public void testUnregisteringListenerStopsUpdatesForListener() { + CarUserManagerHelper.OnUsersUpdateListener listener1 = + Mockito.mock(CarUserManagerHelper.OnUsersUpdateListener.class); + CarUserManagerHelper.OnUsersUpdateListener listener2 = + Mockito.mock(CarUserManagerHelper.OnUsersUpdateListener.class); + ArgumentCaptor<BroadcastReceiver> receiverCaptor = + ArgumentCaptor.forClass(BroadcastReceiver.class); + + mCarUserManagerHelper.registerOnUsersUpdateListener(listener1); + mCarUserManagerHelper.registerOnUsersUpdateListener(listener2); + verify(mContext, times(1)) + .registerReceiverAsUser(receiverCaptor.capture(), any(), any(), any(), any()); + + // Unregister listener2 + mCarUserManagerHelper.unregisterOnUsersUpdateListener(listener2); + + // Verify that calling the receiver calls only one listener. + receiverCaptor.getValue().onReceive(mContext, new Intent()); + verify(listener1).onUsersUpdate(); + verify(listener2, never()).onUsersUpdate(); + } + + @Test + public void testGetInitialUserWithValidLastActiveUser() { + SystemProperties.set("android.car.systemuser.headless", "true"); + int lastActiveUserId = 12; + + UserInfo otherUser1 = createUserInfoForId(lastActiveUserId - 2); + UserInfo otherUser2 = createUserInfoForId(lastActiveUserId - 1); + UserInfo otherUser3 = createUserInfoForId(lastActiveUserId); + + mCarUserManagerHelper.setLastActiveUser( + lastActiveUserId, /* skipGlobalSettings= */ true); + mockGetUsers(mSystemUser, otherUser1, otherUser2, otherUser3); + + assertThat(mCarUserManagerHelper.getInitialUser()).isEqualTo(lastActiveUserId); + } + + @Test + public void testGetInitialUserWithNonExistLastActiveUser() { + SystemProperties.set("android.car.systemuser.headless", "true"); + int lastActiveUserId = 12; + + UserInfo otherUser1 = createUserInfoForId(lastActiveUserId - 2); + UserInfo otherUser2 = createUserInfoForId(lastActiveUserId - 1); + + mCarUserManagerHelper.setLastActiveUser( + lastActiveUserId, /* skipGlobalSettings= */ true); + mockGetUsers(mSystemUser, otherUser1, otherUser2); + + assertThat(mCarUserManagerHelper.getInitialUser()).isEqualTo(lastActiveUserId - 2); + } + private UserInfo createUserInfoForId(int id) { UserInfo userInfo = new UserInfo(); userInfo.id = id; return userInfo; } + + private void mockGetUsers(UserInfo... users) { + List<UserInfo> testUsers = new ArrayList<>(); + for (UserInfo user: users) { + testUsers.add(user); + } + doReturn(testUsers).when(mUserManager).getUsers(true); + } } diff --git a/tests/carservice_unit_test/src/com/android/car/user/CarUserServiceTest.java b/tests/carservice_unit_test/src/com/android/car/user/CarUserServiceTest.java index faa7bd0965..48d447bc0f 100644 --- a/tests/carservice_unit_test/src/com/android/car/user/CarUserServiceTest.java +++ b/tests/carservice_unit_test/src/com/android/car/user/CarUserServiceTest.java @@ -21,18 +21,17 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; import android.car.user.CarUserManagerHelper; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.UserInfo; +import android.location.LocationManager; +import android.os.UserHandle; +import android.os.UserManager; import android.support.test.runner.AndroidJUnit4; -import java.util.ArrayList; -import java.util.List; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -40,6 +39,9 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.ArrayList; +import java.util.List; + /** * This class contains unit tests for the {@link CarUserService}. * @@ -60,17 +62,23 @@ public class CarUserServiceTest { private Context mApplicationContext; @Mock + private LocationManager mLocationManager; + + @Mock private CarUserManagerHelper mCarUserManagerHelper; /** * Initialize all of the objects with the @Mock annotation. */ @Before - public void setUp() throws Exception { + public void setUpMocks() throws Exception { MockitoAnnotations.initMocks(this); - when(mMockContext.getApplicationContext()).thenReturn(mApplicationContext); + doReturn(mApplicationContext).when(mMockContext).getApplicationContext(); + doReturn(mLocationManager).when(mMockContext).getSystemService(Context.LOCATION_SERVICE); mCarUserService = new CarUserService(mMockContext, mCarUserManagerHelper); + + doReturn(new ArrayList<>()).when(mCarUserManagerHelper).getAllUsers(); } /** @@ -83,9 +91,10 @@ public class CarUserServiceTest { mCarUserService.init(); verify(mMockContext).registerReceiver(eq(mCarUserService), argument.capture()); IntentFilter intentFilter = argument.getValue(); - assertThat(intentFilter.countActions()).isEqualTo(1); + assertThat(intentFilter.countActions()).isEqualTo(2); assertThat(intentFilter.getAction(0)).isEqualTo(Intent.ACTION_LOCKED_BOOT_COMPLETED); + assertThat(intentFilter.getAction(1)).isEqualTo(Intent.ACTION_USER_SWITCHED); } /** @@ -102,20 +111,123 @@ public class CarUserServiceTest { */ @Test public void testStartsSecondaryAdminUserOnFirstRun() { + UserInfo admin = mockAdmin(/* adminId= */ 10); + + mCarUserService.onReceive(mMockContext, + new Intent(Intent.ACTION_LOCKED_BOOT_COMPLETED)); + + verify(mCarUserManagerHelper).createNewAdminUser(CarUserService.OWNER_NAME); + verify(mCarUserManagerHelper).switchToUser(admin); + } + + /** + * Test that the {@link CarUserService} disable modify account for user 0 upon first run. + */ + @Test + public void testDisableModifyAccountsForSystemUserOnFirstRun() { + // Mock system user. + UserInfo systemUser = new UserInfo(); + systemUser.id = UserHandle.USER_SYSTEM; + doReturn(systemUser).when(mCarUserManagerHelper).getSystemUserInfo(); + + mockAdmin(10); + + mCarUserService.onReceive(mMockContext, + new Intent(Intent.ACTION_LOCKED_BOOT_COMPLETED)); + + verify(mCarUserManagerHelper) + .setUserRestriction(systemUser, UserManager.DISALLOW_MODIFY_ACCOUNTS, true); + } + + /** + * Test that the {@link CarUserService} disable location service for user 0 upon first run. + */ + @Test + public void testDisableLocationForSystemUserOnFirstRun() { + mockAdmin(/* adminId= */ 10); + + mCarUserService.onReceive(mMockContext, + new Intent(Intent.ACTION_LOCKED_BOOT_COMPLETED)); + + verify(mLocationManager).setLocationEnabledForUser( + /* enabled= */ false, UserHandle.of(UserHandle.USER_SYSTEM)); + } + + /** + * Test that the {@link CarUserService} updates last active user to the first admin user + * on first run. + */ + @Test + public void testUpdateLastActiveUserOnFirstRun() { + UserInfo admin = mockAdmin(/* adminId= */ 10); + + mCarUserService.onReceive(mMockContext, + new Intent(Intent.ACTION_LOCKED_BOOT_COMPLETED)); + + verify(mCarUserManagerHelper) + .setLastActiveUser(admin.id, /* skipGlobalSetting= */ false); + } + + /** + * Test that the {@link CarUserService} starts up the last active user on reboot. + */ + @Test + public void testStartsLastActiveUserOnReboot() { List<UserInfo> users = new ArrayList<>(); int adminUserId = 10; UserInfo admin = new UserInfo(adminUserId, CarUserService.OWNER_NAME, UserInfo.FLAG_ADMIN); + int secUserId = 11; + UserInfo secUser = + new UserInfo(secUserId, CarUserService.OWNER_NAME, UserInfo.FLAG_ADMIN); + + users.add(admin); + users.add(secUser); + doReturn(users).when(mCarUserManagerHelper).getAllUsers(); - // doReturn(users).when(mCarUserManagerHelper.getAllUsers()); - doReturn(admin).when(mCarUserManagerHelper).createNewAdminUser(CarUserService.OWNER_NAME); - doReturn(true).when(mCarUserManagerHelper).switchToUser(admin); + doReturn(secUserId).when(mCarUserManagerHelper).getInitialUser(); mCarUserService.onReceive(mMockContext, new Intent(Intent.ACTION_LOCKED_BOOT_COMPLETED)); - verify(mCarUserManagerHelper).createNewAdminUser(CarUserService.OWNER_NAME); - verify(mCarUserManagerHelper).switchToUser(admin); + verify(mCarUserManagerHelper).switchToUserId(secUserId); + } + + /** + * Test that the {@link CarUserService} updates last active user on user switch intent. + */ + @Test + public void testLastActiveUserUpdatedOnUserSwitch() { + int lastActiveUserId = 11; + + Intent intent = new Intent(Intent.ACTION_USER_SWITCHED); + intent.putExtra(Intent.EXTRA_USER_HANDLE, lastActiveUserId); + + doReturn(true).when(mCarUserManagerHelper).isPersistentUser(lastActiveUserId); + + mCarUserService.onReceive(mMockContext, intent); + + verify(mCarUserManagerHelper).setLastActiveUser( + lastActiveUserId, /* skipGlobalSetting= */ false); + } + + /** + * Test that the {@link CarUserService} sets default guest restrictions on first boot. + */ + @Test + public void testInitializeGuestRestrictionsOnFirstRun() { + mockAdmin(/* adminId= */ 10); + + mCarUserService.onReceive(mMockContext, + new Intent(Intent.ACTION_LOCKED_BOOT_COMPLETED)); + + verify(mCarUserManagerHelper).initDefaultGuestRestrictions(); + } + + private UserInfo mockAdmin(int adminId) { + UserInfo admin = new UserInfo(adminId, CarUserService.OWNER_NAME, UserInfo.FLAG_ADMIN); + doReturn(admin).when(mCarUserManagerHelper).createNewAdminUser(CarUserService.OWNER_NAME); + return admin; } } diff --git a/tests/robotests/src/com/android/car/users/CarUserManagerHelperRoboTest.java b/tests/robotests/src/com/android/car/users/CarUserManagerHelperRoboTest.java index 9c394459ea..fcfd6dc056 100644 --- a/tests/robotests/src/com/android/car/users/CarUserManagerHelperRoboTest.java +++ b/tests/robotests/src/com/android/car/users/CarUserManagerHelperRoboTest.java @@ -122,7 +122,7 @@ public class CarUserManagerHelperRoboTest { } @Test - public void testGetAllUsersExcludesForegroundUser() { + public void testGetAllUsersExceptForegroundUser() { ShadowActivityManager.setCurrentUser(11); ShadowUserManager userManager = ShadowUserManager.getShadow(); |