Merge "Revert "Switch boot animation zip file""
diff --git a/TrustAgent/Android.mk b/TrustAgent/Android.mk
deleted file mode 100644
index bef1402..0000000
--- a/TrustAgent/Android.mk
+++ /dev/null
@@ -1,48 +0,0 @@
-# Copyright (C) 2016 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.
-#
-
-ifneq ($(TARGET_BUILD_PDK), true)
-
-LOCAL_PATH:= $(call my-dir)
-include $(CLEAR_VARS)
-
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
-
-LOCAL_USE_AAPT2 := true
-
-LOCAL_JAVA_LIBRARIES += android.car
-
-LOCAL_STATIC_ANDROID_LIBRARIES += \
- androidx.car_car
-
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-
-LOCAL_PACKAGE_NAME := CarTrustAgentService
-LOCAL_PRIVATE_PLATFORM_APIS := true
-
-LOCAL_PRIVILEGED_MODULE := true
-
-# Remove this to verify permission checks are working correctly
-LOCAL_CERTIFICATE := platform
-
-LOCAL_MODULE_TAGS := optional
-
-LOCAL_PROGUARD_ENABLED := disabled
-
-LOCAL_DEX_PREOPT := false
-
-include $(BUILD_PACKAGE)
-
-endif
diff --git a/TrustAgent/AndroidManifest.xml b/TrustAgent/AndroidManifest.xml
deleted file mode 100644
index 2b7944d..0000000
--- a/TrustAgent/AndroidManifest.xml
+++ /dev/null
@@ -1,85 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2016 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
- -->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.car.trust">
- <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" />
- <!-- TODO(b/77717079) INTERACT_ACROSS_USERS_FULL should have granted INTERACT_ACROSS_USERS -->
- <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
- <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"
- android:theme="@style/Theme.Car.NoActionBar"
- android:supportsRtl="true">
-
- <service
- android:name=".CarBleTrustAgent"
- android:permission="android.permission.BIND_TRUST_AGENT"
- android:directBootAware="true"
- android:exported="true"
- android:singleUser="true">
- <intent-filter>
- <action android:name="android.service.trust.TrustAgentService" />
- <category android:name="android.intent.category.DEFAULT" />
- </intent-filter>
- <!-- Warning: the meta data must be included if the service is direct boot aware.
- If not included, the device will crash before boot completes. Rendering the
- device unusable. -->
- <meta-data android:name="android.service.trust.trustagent"
- android:resource="@xml/car_sample_trust_agent"/>
- </service>
-
- <!-- CarTrustAgentBleService needs to be direct boot aware, since the trust agent
- binds to it during direct boot.-->
- <service
- android:name=".CarTrustAgentBleService"
- android:directBootAware="true"
- android:singleUser="true">
- <!-- Warning: the meta data must be included if the service is direct boot aware.
- If not included, the device will crash before boot completes. Rendering the device
- unusable. -->
- <meta-data android:name="android.service.trust.trustagent"
- android:resource="@xml/car_sample_trust_agent"/>
- </service>
-
- <activity
- android:name=".CarEnrolmentActivity"
- 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/TrustAgent/res/layout/car_enrolment_activity.xml b/TrustAgent/res/layout/car_enrolment_activity.xml
deleted file mode 100644
index 70632f7..0000000
--- a/TrustAgent/res/layout/car_enrolment_activity.xml
+++ /dev/null
@@ -1,69 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 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"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:background="@android:color/black"
- android:orientation="vertical"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <RelativeLayout
- android:layout_width="match_parent"
- android:layout_height="@dimen/car_app_bar_height" >
-
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignParentStart="true"
- android:layout_centerVertical="true"
- android:layout_marginStart="@dimen/car_margin"
- android:text="@string/app_name"
- android:textAppearance="@style/TextAppearance.Car.Body1.Light" />
-
- <Button
- android:id="@+id/revoke_trust_button"
- android:layout_width="wrap_content"
- android:layout_centerVertical="true"
- android:layout_toStartOf="@+id/start_button"
- android:layout_marginEnd="@dimen/car_padding_4"
- android:text="@string/revoke_trust"
- style="?android:attr/borderlessButtonStyle" />
-
- <Button
- android:id="@+id/start_button"
- android:layout_width="wrap_content"
- android:layout_alignParentEnd="true"
- android:layout_centerVertical="true"
- android:layout_marginEnd="@dimen/car_keyline_1"
- android:text="@string/start_advertising"
- style="?android:attr/buttonStyle" />
-
- <View
- android:background="@color/car_list_divider_light"
- android:layout_width="match_parent"
- android:layout_height="@dimen/car_list_divider_height"
- android:layout_alignParentBottom="true" />
- </RelativeLayout>
-
- <androidx.car.widget.PagedListView
- android:id="@+id/list"
- android:layout_marginTop="@dimen/car_padding_4"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- app:showPagedListViewDivider="false"
- app:gutter="both"/>
-</LinearLayout>
diff --git a/TrustAgent/res/layout/output_line.xml b/TrustAgent/res/layout/output_line.xml
deleted file mode 100644
index 53eba57..0000000
--- a/TrustAgent/res/layout/output_line.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?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="@+id/output"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:textAppearance="@style/TextAppearance.Car.Body3.Light" />
diff --git a/TrustAgent/res/values/strings.xml b/TrustAgent/res/values/strings.xml
deleted file mode 100644
index c8dcc18..0000000
--- a/TrustAgent/res/values/strings.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 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.
--->
-<resources>
- <string name="app_name">CarTrustAgentService</string>
- <string name="trust_granted_explanation">Unlock via escrow token, now granting trust</string>
-
- <!-- service/characteristics uuid for unlocking a device -->
- <string name="unlock_service_uuid" translatable="false">5e2a68a1-27be-43f9-8d1e-4546976fabd7</string>
- <string name="unlock_escrow_token_uiid" translatable="false">5e2a68a2-27be-43f9-8d1e-4546976fabd7</string>
- <string name="unlock_handle_uiid" translatable="false">5e2a68a3-27be-43f9-8d1e-4546976fabd7</string>
-
- <!-- service/characteristics uuid for adding new escrow token -->
- <string name="enrollment_service_uuid" translatable="false">5e2a68a4-27be-43f9-8d1e-4546976fabd7</string>
- <string name="enrollment_handle_uuid" translatable="false">5e2a68a5-27be-43f9-8d1e-4546976fabd7</string>
- <string name="enrollment_token_uuid" translatable="false">5e2a68a6-27be-43f9-8d1e-4546976fabd7</string>
-
- <string name="pref_key_token_handle" translatable="false">token-handle-key</string>
- <string name="pref_key_escrow_token" translatable="false">escrow-token-key</string>
-
- <string name="unlock_button">Unlock Car</string>
- <string name="enroll_button">Enroll New Token</string>
-
- <string name="start_advertising">Start Advertising</string>
- <string name="revoke_trust">Revoke Trust</string>
-
- <string name="token_handle_shared_preferences" translatable="false">com.android.car.trust.TOKEN_HANDLE</string>
-
-</resources>
diff --git a/TrustAgent/res/xml/car_sample_trust_agent.xml b/TrustAgent/res/xml/car_sample_trust_agent.xml
deleted file mode 100644
index 0c7d91d..0000000
--- a/TrustAgent/res/xml/car_sample_trust_agent.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2017 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
- -->
-<trust-agent xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:priv-android="http://schemas.android.com/apk/prv/res/android"
- android:settingsActivity=".MainActivity"
- priv-android:unlockProfile="true" />
diff --git a/TrustAgent/src/com/android/car/trust/CarBleTrustAgent.java b/TrustAgent/src/com/android/car/trust/CarBleTrustAgent.java
deleted file mode 100644
index 1dfbd50..0000000
--- a/TrustAgent/src/com/android/car/trust/CarBleTrustAgent.java
+++ /dev/null
@@ -1,303 +0,0 @@
-/*
- * Copyright (C) 2016 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;
-
-import android.annotation.Nullable;
-import android.app.ActivityManager;
-import android.app.UserSwitchObserver;
-import android.bluetooth.BluetoothAdapter;
-import android.car.trust.ICarTrustAgentBleService;
-import android.car.trust.ICarTrustAgentTokenRequestDelegate;
-import android.car.trust.ICarTrustAgentUnlockCallback;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.ServiceConnection;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.os.UserHandle;
-import android.service.trust.TrustAgentService;
-import android.util.Log;
-
-/**
- * A BluetoothLE (BLE) based {@link TrustAgentService} that uses the escrow token unlock APIs. </p>
- *
- * This trust agent runs during direct boot and binds to a BLE service that listens for remote
- * devices to trigger an unlock. <p/>
- *
- * The permissions for this agent must be enabled as priv-app permissions for it to start.
- */
-public class CarBleTrustAgent extends TrustAgentService {
-
- private static final String TAG = CarBleTrustAgent.class.getSimpleName();
-
- /**
- * {@link CarTrustAgentBleService} will callback this function when it receives both
- * handle and token.
- */
- private final ICarTrustAgentUnlockCallback mUnlockCallback =
- new ICarTrustAgentUnlockCallback.Stub() {
- @Override
- public void onUnlockDataReceived(byte[] token, long handle) throws RemoteException {
- UserHandle userHandle = getUserHandleByTokenHandle(handle);
- if (userHandle == null) {
- Log.e(TAG, "Unable to find user by token handle " + handle);
- return;
- }
-
- int uid = userHandle.getIdentifier();
- if (ActivityManager.getCurrentUser() != uid) {
- Log.d(TAG, "Switch to user: " + uid);
- // Try to unlock when user switch completes
- ActivityManager.getService().registerUserSwitchObserver(
- getUserSwitchObserver(uid, token, handle), TAG);
- ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
- am.switchUser(uid);
- } else {
- unlockUserInternally(uid, token, handle);
- }
- }
- };
-
- /**
- * Delegates the escrow token API calls from {@link CarTrustAgentBleService} to
- * {@link TrustAgentService}. Due to the asynchronous nature, the results will be posted to
- * {@link CarTrustAgentBleService} by the following calls
- * <ul>
- * <li>{@link #onEscrowTokenAdded(byte[], long, UserHandle)}</li>
- * <li>{@link #onEscrowTokenRemoved(long, boolean)}</li>
- * <li>{@link #onEscrowTokenStateReceived(long, int)}</li>
- * </ul>
- */
- private final ICarTrustAgentTokenRequestDelegate mTokenRequestDelegate =
- new ICarTrustAgentTokenRequestDelegate.Stub() {
- @Override
- public void revokeTrust() {
- CarBleTrustAgent.this.revokeTrust();
- }
-
- @Override
- public void addEscrowToken(byte[] token, int uid) {
- CarBleTrustAgent.this.addEscrowToken(token, UserHandle.of(uid));
- }
-
- @Override
- public void removeEscrowToken(long handle, int uid) {
- CarBleTrustAgent.this.removeEscrowToken(handle, UserHandle.of(uid));
- }
-
- @Override
- public void isEscrowTokenActive(long handle, int uid) {
- CarBleTrustAgent.this.isEscrowTokenActive(handle, UserHandle.of(uid));
- }
- };
-
- /**
- * Service connection to {@link CarTrustAgentBleService}
- */
- private final ServiceConnection mServiceConnection = new ServiceConnection() {
- @Override
- public void onServiceConnected(ComponentName name, IBinder service) {
- Log.d(TAG, "CarTrustAgentBleService connected");
- mCarTrustAgentBleServiceBound = true;
- mCarTrustAgentBleService = ICarTrustAgentBleService.Stub.asInterface(service);
- try {
- mCarTrustAgentBleService.registerUnlockCallback(mUnlockCallback);
- mCarTrustAgentBleService.setTokenRequestDelegate(mTokenRequestDelegate);
- } catch (RemoteException e) {
- Log.e(TAG, "Error registerUnlockCallback", e);
- }
- }
-
- @Override
- public void onServiceDisconnected(ComponentName name) {
- Log.d(TAG, "CarTrustAgentBleService disconnected");
- if (mCarTrustAgentBleService != null) {
- try {
- mCarTrustAgentBleService.unregisterUnlockCallback(mUnlockCallback);
- mCarTrustAgentBleService.setTokenRequestDelegate(null);
- mCarTrustAgentBleService.stopUnlockAdvertising();
- } catch (RemoteException e) {
- Log.e(TAG, "Error unregisterUnlockCallback", e);
- }
- mCarTrustAgentBleService = null;
- mCarTrustAgentBleServiceBound = false;
- }
- }
- };
-
- /**
- * Receives the bluetooth state change broadcasts. Bluetooth is restarted when switching user,
- * we need to ensure calling {@link ICarTrustAgentBleService#startUnlockAdvertising} after
- * bluetooth is started.
- */
- private final BroadcastReceiver mBluetoothBroadcastReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- switch (intent.getAction()) {
- case BluetoothAdapter.ACTION_STATE_CHANGED:
- onBluetoothStateChanged(intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1));
- break;
- }
- }
- };
-
- private ICarTrustAgentBleService mCarTrustAgentBleService;
- private boolean mCarTrustAgentBleServiceBound;
-
- /**
- * TODO: Currently it relies on {@link #onDeviceLocked()} and {@link #onDeviceUnlocked()}
- * callback, and these callbacks won't happen if the user has unlocked once.
- */
- private boolean mIsOnLockScreen;
-
- @Override
- public void onCreate() {
- super.onCreate();
- setManagingTrust(true);
- bindService(new Intent(this, CarTrustAgentBleService.class),
- mServiceConnection, Context.BIND_AUTO_CREATE);
- IntentFilter intentFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
- registerReceiver(mBluetoothBroadcastReceiver, intentFilter);
- }
-
- @Override
- public void onDestroy() {
- Log.d(TAG, "Car Trust agent shutting down");
- if (mCarTrustAgentBleServiceBound) {
- unbindService(mServiceConnection);
- }
- unregisterReceiver(mBluetoothBroadcastReceiver);
- super.onDestroy();
- }
-
- @Override
- public void onDeviceLocked() {
- super.onDeviceLocked();
- mIsOnLockScreen = true;
- if (mCarTrustAgentBleServiceBound) {
- try {
- mCarTrustAgentBleService.startUnlockAdvertising();
- } catch (RemoteException e) {
- Log.e(TAG, "Error startUnlockAdvertising", e);
- }
- }
- }
-
- @Override
- public void onDeviceUnlocked() {
- super.onDeviceUnlocked();
- mIsOnLockScreen = false;
- if (mCarTrustAgentBleServiceBound) {
- try {
- mCarTrustAgentBleService.stopUnlockAdvertising();
- } catch (RemoteException e) {
- Log.e(TAG, "Error stopUnlockAdvertising", e);
- }
- }
- // Revoke trust right after to enable keyguard when switching user
- revokeTrust();
- }
-
- private UserSwitchObserver getUserSwitchObserver(int uid,
- byte[] token, long handle) {
- return new UserSwitchObserver() {
- @Override
- public void onUserSwitchComplete(int newUserId) throws RemoteException {
- if (uid != newUserId) return;
- unlockUserInternally(uid, token, handle);
- ActivityManager.getService().unregisterUserSwitchObserver(this);
- }
-
- @Override
- public void onLockedBootComplete(int newUserId) {
- // ignored.
- }
- };
- }
-
- private void unlockUserInternally(int uid, byte[] token, long handle) {
- Log.d(TAG, "About to unlock user: " + uid);
- unlockUserWithToken(handle, token, UserHandle.of(uid));
- grantTrust("Granting trust from escrow token",
- 0, FLAG_GRANT_TRUST_DISMISS_KEYGUARD);
- }
-
- private void onBluetoothStateChanged(int state) {
- Log.d(TAG, "onBluetoothStateChanged: " + state);
- if ((state == BluetoothAdapter.STATE_ON || state == BluetoothAdapter.STATE_BLE_ON)
- && mCarTrustAgentBleServiceBound
- && mIsOnLockScreen) {
- try {
- mCarTrustAgentBleService.startUnlockAdvertising();
- } catch (RemoteException e) {
- Log.e(TAG, "Error startUnlockAdvertising", e);
- }
- }
- }
-
- @Override
- public void onEscrowTokenRemoved(long handle, boolean successful) {
- if (mCarTrustAgentBleServiceBound) {
- try {
- mCarTrustAgentBleService.onEscrowTokenRemoved(handle, successful);
- Log.v(TAG, "Callback onEscrowTokenRemoved");
- } catch (RemoteException e) {
- Log.e(TAG, "Error callback onEscrowTokenRemoved", e);
- }
- }
- }
-
- @Override
- public void onEscrowTokenStateReceived(long handle, int tokenState) {
- boolean isActive = tokenState == TOKEN_STATE_ACTIVE;
- if (mCarTrustAgentBleServiceBound) {
- try {
- mCarTrustAgentBleService.onEscrowTokenActiveStateChanged(handle, isActive);
- Log.v(TAG, "Callback onEscrowTokenActiveStateChanged");
- } catch (RemoteException e) {
- Log.e(TAG, "Error callback onEscrowTokenActiveStateChanged", e);
- }
- }
- }
-
- @Override
- public void onEscrowTokenAdded(byte[] token, long handle, UserHandle user) {
- if (mCarTrustAgentBleServiceBound) {
- try {
- mCarTrustAgentBleService.onEscrowTokenAdded(token, handle, user.getIdentifier());
- Log.v(TAG, "Callback onEscrowTokenAdded");
- } catch (RemoteException e) {
- Log.e(TAG, "Error callback onEscrowTokenAdded", e);
- }
- }
- }
-
- private @Nullable UserHandle getUserHandleByTokenHandle(long tokenHandle) {
- if (mCarTrustAgentBleServiceBound) {
- try {
- int userId = mCarTrustAgentBleService.getUserIdByEscrowTokenHandle(tokenHandle);
- return userId < 0 ? null : UserHandle.of(userId);
- } catch (RemoteException e) {
- Log.e(TAG, "Error getUserHandleByTokenHandle");
- }
- }
- return null;
- }
-}
diff --git a/TrustAgent/src/com/android/car/trust/CarEnrolmentActivity.java b/TrustAgent/src/com/android/car/trust/CarEnrolmentActivity.java
deleted file mode 100644
index f93c1b4..0000000
--- a/TrustAgent/src/com/android/car/trust/CarEnrolmentActivity.java
+++ /dev/null
@@ -1,318 +0,0 @@
-/*
- * Copyright (C) 2017 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;
-
-import android.Manifest;
-import android.app.Activity;
-import android.bluetooth.BluetoothDevice;
-import android.car.trust.ICarTrustAgentBleCallback;
-import android.car.trust.ICarTrustAgentBleService;
-import android.car.trust.ICarTrustAgentEnrolmentCallback;
-import android.car.trust.ICarTrustAgentTokenResponseCallback;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
-import android.content.SharedPreferences;
-import android.content.pm.PackageManager;
-import android.os.Bundle;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.os.UserHandle;
-import android.preference.PreferenceManager;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-import androidx.car.widget.PagedListView;
-import androidx.recyclerview.widget.RecyclerView;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Setup activity that binds {@link CarTrustAgentBleService} and starts the enrolment process.
- */
-public class CarEnrolmentActivity extends Activity {
-
- private static final String TAG = CarEnrolmentActivity.class.getSimpleName();
-
- private static final String SP_HANDLE_KEY = "sp-test";
- private static final int FINE_LOCATION_REQUEST_CODE = 42;
-
- private PagedListView mList;
- private OutputAdapter mOutputAdapter;
- private BluetoothDevice mBluetoothDevice;
- private ICarTrustAgentBleService mCarTrustAgentBleService;
- private boolean mCarTrustAgentBleServiceBound;
- private SharedPreferences mPrefs;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.car_enrolment_activity);
-
- mPrefs = PreferenceManager.getDefaultSharedPreferences(/* context= */ this);
-
- findViewById(R.id.start_button).setOnClickListener(v -> {
- if (!mCarTrustAgentBleServiceBound) {
- Intent bindIntent = new Intent(this, CarTrustAgentBleService.class);
- bindService(bindIntent, mServiceConnection, Context.BIND_AUTO_CREATE);
- }
- });
-
- findViewById(R.id.revoke_trust_button).setOnClickListener(v -> {
- if (mCarTrustAgentBleServiceBound) {
- try {
- mCarTrustAgentBleService.revokeTrust();
- } catch (RemoteException e) {
- Log.e(TAG, "Error revokeTrust", e);
- }
- }
- });
-
- mOutputAdapter = new OutputAdapter();
-
- mList = findViewById(R.id.list);
- mList.setAdapter(mOutputAdapter);
- }
-
- @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);
- }
- }
-
- @Override
- protected void onStop() {
- super.onStop();
-
- if (mCarTrustAgentBleServiceBound) {
- unbindService(mServiceConnection);
- mCarTrustAgentBleServiceBound = false;
- }
- }
-
- private void appendOutputText(String text) {
- runOnUiThread(() -> {
- mOutputAdapter.addOutput(text);
- mList.scrollToPosition(mOutputAdapter.getItemCount() - 1);
- });
- }
-
- private void addEscrowToken(byte[] token) throws RemoteException {
- if (!mCarTrustAgentBleServiceBound) {
- Log.e(TAG, "No CarTrustAgentBleService bounded");
- return;
- }
- mCarTrustAgentBleService.addEscrowToken(token, UserHandle.myUserId());
- }
-
- private void checkTokenHandle() {
- long tokenHandle = mPrefs.getLong(SP_HANDLE_KEY, -1);
- if (tokenHandle != -1) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Checking handle active: " + tokenHandle);
- }
-
- if (mCarTrustAgentBleServiceBound) {
- try {
- // Due to the asynchronous nature of isEscrowTokenActive in
- // TrustAgentService, query result will be delivered via
- // {@link #mCarTrustAgentTokenResponseCallback}
- mCarTrustAgentBleService.isEscrowTokenActive(tokenHandle,
- UserHandle.myUserId());
- } catch (RemoteException e) {
- Log.e(TAG, "Error isEscrowTokenActive", e);
- }
- }
- } else {
- appendOutputText("No handles found");
- }
- }
-
- /**
- * Receives escrow token callbacks, registered on {@link CarTrustAgentBleService}
- */
- private final ICarTrustAgentTokenResponseCallback mCarTrustAgentTokenResponseCallback =
- new ICarTrustAgentTokenResponseCallback.Stub() {
- @Override
- public void onEscrowTokenAdded(byte[] token, long handle, int uid) {
- runOnUiThread(() -> {
- mPrefs.edit().putLong(SP_HANDLE_KEY, handle).apply();
-
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "stored new handle for user: " + uid);
- }
- });
-
- if (mBluetoothDevice == null) {
- Log.e(TAG, "No active bluetooth found to add escrow token");
- return;
- }
-
- try {
- // Notify the enrolment client that escrow token has been added
- mCarTrustAgentBleService.sendEnrolmentHandle(mBluetoothDevice, handle);
- appendOutputText("Escrow Token Added. Handle: " + handle);
- appendOutputText("Lock and unlock the device to activate token");
- } catch (RemoteException e) {
- Log.e(TAG, "Error sendEnrolmentHandle", e);
- }
- }
-
- @Override
- public void onEscrowTokenRemoved(long handle, boolean successful) {
- appendOutputText("Escrow token Removed. Handle: " + handle);
- }
-
- @Override
- public void onEscrowTokenActiveStateChanged(long handle, boolean active) {
- appendOutputText("Is token active? " + active + " handle: " + handle);
- }
- };
-
- /**
- * Receives BLE state change callbacks, registered on {@link CarTrustAgentBleService}
- */
- private final ICarTrustAgentBleCallback mBleConnectionCallback =
- new ICarTrustAgentBleCallback.Stub() {
- @Override
- public void onBleServerStartSuccess() {
- appendOutputText("Server started");
- }
-
- @Override
- public void onBleServerStartFailure(int errorCode) {
- appendOutputText("Server failed to start, error code: " + errorCode);
- }
-
- @Override
- public void onBleDeviceConnected(BluetoothDevice device) {
- mBluetoothDevice = device;
- appendOutputText("Device connected: " + device.getName()
- + " address: " + device.getAddress());
- }
-
- @Override
- public void onBleDeviceDisconnected(BluetoothDevice device) {
- mBluetoothDevice = null;
- appendOutputText("Device disconnected: " + device.getName()
- + " address: " + device.getAddress());
- }
- };
-
- /**
- * {@link CarTrustAgentBleService} will callback this when receives enrolment data.
- *
- * <p>Here is the place we can prompt to the user on HU whether or not to add this
- * {@link #mBluetoothDevice} as a trust device.
- */
- private final ICarTrustAgentEnrolmentCallback mEnrolmentCallback =
- new ICarTrustAgentEnrolmentCallback.Stub() {
- @Override
- public void onEnrolmentDataReceived(byte[] token) {
- appendOutputText("Enrolment data received ");
- try {
- addEscrowToken(token);
- } catch (RemoteException e) {
- Log.e(TAG, "Error addEscrowToken", e);
- }
- }
- };
-
- /**
- * Service connection to {@link CarTrustAgentBleService}
- */
- private final ServiceConnection mServiceConnection = new ServiceConnection() {
- @Override
- public void onServiceConnected(ComponentName name, IBinder service) {
- mCarTrustAgentBleServiceBound = true;
- mCarTrustAgentBleService = ICarTrustAgentBleService.Stub.asInterface(service);
- try {
- mCarTrustAgentBleService.registerBleCallback(mBleConnectionCallback);
- mCarTrustAgentBleService.registerEnrolmentCallback(mEnrolmentCallback);
- mCarTrustAgentBleService.setTokenResponseCallback(
- mCarTrustAgentTokenResponseCallback);
- mCarTrustAgentBleService.startEnrolmentAdvertising();
- } catch (RemoteException e) {
- Log.e(TAG, "Error startEnrolmentAdvertising", e);
- }
- checkTokenHandle();
- }
-
- @Override
- public void onServiceDisconnected(ComponentName name) {
- if (mCarTrustAgentBleService != null) {
- try {
- mCarTrustAgentBleService.unregisterBleCallback(mBleConnectionCallback);
- mCarTrustAgentBleService.unregisterEnrolmentCallback(mEnrolmentCallback);
- mCarTrustAgentBleService.setTokenResponseCallback(null);
- mCarTrustAgentBleService.stopEnrolmentAdvertising();
- } catch (RemoteException e) {
- Log.e(TAG, "Error unregister callbacks", e);
- }
- mCarTrustAgentBleService = null;
- }
- mCarTrustAgentBleServiceBound = false;
- }
- };
-
- /** Adapter that displays the output of TrustAgent connections. */
- private static class OutputAdapter extends RecyclerView.Adapter<OutputAdapter.ViewHolder> {
- private final List<String> mOutput = new ArrayList<>();
-
- /** Adds the given string as output to be displayed. */
- public void addOutput(String output) {
- mOutput.add(output);
- notifyItemInserted(mOutput.size() - 1);
- }
-
- @Override
- public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- return new ViewHolder(LayoutInflater.from(parent.getContext())
- .inflate(R.layout.output_line, parent, false));
- }
-
- @Override
- public void onBindViewHolder(ViewHolder holder, int position) {
- holder.mTextView.setText(mOutput.get(position));
- }
-
- @Override
- public int getItemCount() {
- return mOutput.size();
- }
-
- /** ViewHolder for {@link OutputAdapter}. */
- public static class ViewHolder extends RecyclerView.ViewHolder {
- private final TextView mTextView;
-
- ViewHolder(View itemView) {
- super(itemView);
- mTextView = itemView.findViewById(R.id.output);
- }
- }
- }
-}
diff --git a/TrustAgent/src/com/android/car/trust/CarTrustAgentBleService.java b/TrustAgent/src/com/android/car/trust/CarTrustAgentBleService.java
deleted file mode 100644
index d70fd2f..0000000
--- a/TrustAgent/src/com/android/car/trust/CarTrustAgentBleService.java
+++ /dev/null
@@ -1,475 +0,0 @@
-/*
- * 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;
-
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattService;
-import android.bluetooth.le.AdvertiseCallback;
-import android.bluetooth.le.AdvertiseSettings;
-import android.car.trust.ICarTrustAgentBleCallback;
-import android.car.trust.ICarTrustAgentBleService;
-import android.car.trust.ICarTrustAgentEnrolmentCallback;
-import android.car.trust.ICarTrustAgentTokenRequestDelegate;
-import android.car.trust.ICarTrustAgentTokenResponseCallback;
-import android.car.trust.ICarTrustAgentUnlockCallback;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.os.IBinder;
-import android.os.RemoteCallbackList;
-import android.os.RemoteException;
-import android.util.Log;
-
-import java.nio.ByteBuffer;
-import java.text.DateFormat;
-import java.util.UUID;
-
-/**
- * Abstracts enrolment and unlock token exchange via BluetoothLE (BLE).
- * {@link CarBleTrustAgent} and any enrolment client should bind to
- * {@link ICarTrustAgentBleService}.
- */
-public class CarTrustAgentBleService extends SimpleBleServer {
- private static final String TAG = CarTrustAgentBleService.class.getSimpleName();
-
- private RemoteCallbackList<ICarTrustAgentBleCallback> mBleCallbacks;
- private RemoteCallbackList<ICarTrustAgentEnrolmentCallback> mEnrolmentCallbacks;
- private RemoteCallbackList<ICarTrustAgentUnlockCallback> mUnlockCallbacks;
- private ICarTrustAgentTokenRequestDelegate mTokenRequestDelegate;
- private ICarTrustAgentTokenResponseCallback mTokenResponseCallback;
- private CarTrustAgentBleWrapper mCarTrustBleService;
-
- private UUID mEnrolmentEscrowTokenUuid;
- private UUID mEnrolmentTokenHandleUuid;
- private BluetoothGattService mEnrolmentGattService;
-
- private UUID mUnlockEscrowTokenUuid;
- private UUID mUnlockTokenHandleUuid;
- private BluetoothGattService mUnlockGattService;
-
- private byte[] mCurrentUnlockToken;
- private Long mCurrentUnlockHandle;
-
- private SharedPreferences mTokenHandleSharedPreferences;
-
- @Override
- public void onCreate() {
- super.onCreate();
- mBleCallbacks = new RemoteCallbackList<>();
- mEnrolmentCallbacks = new RemoteCallbackList<>();
- mUnlockCallbacks = new RemoteCallbackList<>();
- mCarTrustBleService = new CarTrustAgentBleWrapper();
-
- mEnrolmentEscrowTokenUuid = UUID.fromString(getString(R.string.enrollment_token_uuid));
- mEnrolmentTokenHandleUuid = UUID.fromString(getString(R.string.enrollment_handle_uuid));
- mUnlockEscrowTokenUuid = UUID.fromString(getString(R.string.unlock_escrow_token_uiid));
- mUnlockTokenHandleUuid = UUID.fromString(getString(R.string.unlock_handle_uiid));
-
- setupEnrolmentBleServer();
- setupUnlockBleServer();
-
- mTokenHandleSharedPreferences = getSharedPreferences(
- getString(R.string.token_handle_shared_preferences),
- MODE_PRIVATE);
- }
-
- @Override
- public IBinder onBind(Intent intent) {
- return mCarTrustBleService;
- }
-
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- // keep it alive.
- return START_STICKY;
- }
-
- @Override
- public void onCharacteristicWrite(final BluetoothDevice device, int requestId,
- BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean
- responseNeeded, int offset, byte[] value) {
- UUID uuid = characteristic.getUuid();
-
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "onCharacteristicWrite received uuid: " + uuid);
- }
-
- if (uuid.equals(mEnrolmentEscrowTokenUuid)) {
- final int callbackCount = mEnrolmentCallbacks.beginBroadcast();
- for (int i = 0; i < callbackCount; i++) {
- try {
- mEnrolmentCallbacks.getBroadcastItem(i).onEnrolmentDataReceived(value);
- } catch (RemoteException e) {
- Log.e(TAG, "Error callback onEnrolmentDataReceived", e);
- }
- }
- mEnrolmentCallbacks.finishBroadcast();
- } else if (uuid.equals(mUnlockEscrowTokenUuid)) {
- mCurrentUnlockToken = value;
- maybeSendUnlockToken();
- } else if (uuid.equals(mUnlockTokenHandleUuid)) {
- mCurrentUnlockHandle = getLong(value);
- maybeSendUnlockToken();
- }
- }
-
- @Override
- public void onCharacteristicRead(BluetoothDevice device,
- int requestId, int offset, final BluetoothGattCharacteristic characteristic) {
- // Ignored read requests.
- }
-
- @Override
- protected void onAdvertiseStartSuccess() {
- final int callbackCount = mBleCallbacks.beginBroadcast();
- for (int i = 0; i < callbackCount; i++) {
- try {
- mBleCallbacks.getBroadcastItem(i).onBleServerStartSuccess();
- } catch (RemoteException e) {
- Log.e(TAG, "Error callback onBleServerStartSuccess", e);
- }
- }
- mBleCallbacks.finishBroadcast();
- }
-
- @Override
- protected void onAdvertiseStartFailure(int errorCode) {
- final int callbackCount = mBleCallbacks.beginBroadcast();
- for (int i = 0; i < callbackCount; i++) {
- try {
- mBleCallbacks.getBroadcastItem(i).onBleServerStartFailure(errorCode);
- } catch (RemoteException e) {
- Log.e(TAG, "Error callback onBleServerStartFailure", e);
- }
- }
- mBleCallbacks.finishBroadcast();
- }
-
- @Override
- protected void onAdvertiseDeviceConnected(BluetoothDevice device) {
- final int callbackCount = mBleCallbacks.beginBroadcast();
- for (int i = 0; i < callbackCount; i++) {
- try {
- mBleCallbacks.getBroadcastItem(i).onBleDeviceConnected(device);
- } catch (RemoteException e) {
- Log.e(TAG, "Error callback onBleDeviceConnected", e);
- }
- }
- mBleCallbacks.finishBroadcast();
- }
-
- @Override
- protected void onAdvertiseDeviceDisconnected(BluetoothDevice device) {
- final int callbackCount = mBleCallbacks.beginBroadcast();
- for (int i = 0; i < callbackCount; i++) {
- try {
- mBleCallbacks.getBroadcastItem(i).onBleDeviceDisconnected(device);
- } catch (RemoteException e) {
- Log.e(TAG, "Error callback onBleDeviceDisconnected", e);
- }
- }
- mBleCallbacks.finishBroadcast();
- }
-
- @Override
- public void onDestroy() {
- stopAdvertising(mEnrolmentAdvertisingCallback);
- stopAdvertising(mUnlockAdvertisingCallback);
- super.onDestroy();
- }
-
- private void setupEnrolmentBleServer() {
- mEnrolmentGattService = new BluetoothGattService(
- UUID.fromString(getString(R.string.enrollment_service_uuid)),
- BluetoothGattService.SERVICE_TYPE_PRIMARY);
-
- // Characteristic to describe the escrow token being used for unlock
- BluetoothGattCharacteristic enrolmentEscrowToken = new BluetoothGattCharacteristic(
- mEnrolmentEscrowTokenUuid,
- BluetoothGattCharacteristic.PROPERTY_WRITE,
- BluetoothGattCharacteristic.PERMISSION_WRITE);
-
- // Characteristic to describe the handle being used for this escrow token
- BluetoothGattCharacteristic enrolmentTokenHandle = new BluetoothGattCharacteristic(
- mEnrolmentTokenHandleUuid,
- BluetoothGattCharacteristic.PROPERTY_NOTIFY,
- BluetoothGattCharacteristic.PERMISSION_READ);
-
- mEnrolmentGattService.addCharacteristic(enrolmentEscrowToken);
- mEnrolmentGattService.addCharacteristic(enrolmentTokenHandle);
- }
-
- private void setupUnlockBleServer() {
- mUnlockGattService = new BluetoothGattService(
- UUID.fromString(getString(R.string.unlock_service_uuid)),
- BluetoothGattService.SERVICE_TYPE_PRIMARY);
-
- // Characteristic to describe the escrow token being used for unlock
- BluetoothGattCharacteristic unlockEscrowToken = new BluetoothGattCharacteristic(
- mUnlockEscrowTokenUuid,
- BluetoothGattCharacteristic.PROPERTY_WRITE,
- BluetoothGattCharacteristic.PERMISSION_WRITE);
-
- // Characteristic to describe the handle being used for this escrow token
- BluetoothGattCharacteristic unlockTokenHandle = new BluetoothGattCharacteristic(
- mUnlockTokenHandleUuid,
- BluetoothGattCharacteristic.PROPERTY_WRITE,
- BluetoothGattCharacteristic.PERMISSION_WRITE);
-
- mUnlockGattService.addCharacteristic(unlockEscrowToken);
- mUnlockGattService.addCharacteristic(unlockTokenHandle);
- }
-
- private synchronized void maybeSendUnlockToken() {
- if (mCurrentUnlockToken == null || mCurrentUnlockHandle == null) {
- return;
- }
-
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Handle and token both received, requesting unlock. Time: "
- + DateFormat.getDateInstance(DateFormat.SHORT).format(
- System.currentTimeMillis()));
- }
-
- int callbackCount = mUnlockCallbacks.beginBroadcast();
- for (int i = 0; i < callbackCount; i++) {
- try {
- mUnlockCallbacks.getBroadcastItem(i).onUnlockDataReceived(
- mCurrentUnlockToken, mCurrentUnlockHandle);
- } catch (RemoteException e) {
- Log.e(TAG, "Error callback onUnlockDataReceived", e);
- }
- }
- mUnlockCallbacks.finishBroadcast();
- mCurrentUnlockHandle = null;
- mCurrentUnlockToken = null;
- }
-
- private static byte[] getBytes(long primitive) {
- ByteBuffer buffer = ByteBuffer.allocate(Long.SIZE / Byte.SIZE);
- buffer.putLong(0, primitive);
- return buffer.array();
- }
-
- private static long getLong(byte[] bytes) {
- ByteBuffer buffer = ByteBuffer.allocate(Long.SIZE / Byte.SIZE);
- buffer.put(bytes);
- buffer.flip();
- return buffer.getLong();
- }
-
- private final AdvertiseCallback mEnrolmentAdvertisingCallback = new AdvertiseCallback() {
- @Override
- public void onStartSuccess(AdvertiseSettings settingsInEffect) {
- super.onStartSuccess(settingsInEffect);
- onAdvertiseStartSuccess();
-
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Successfully started advertising service");
- }
- }
-
- @Override
- public void onStartFailure(int errorCode) {
- Log.e(TAG, "Failed to advertise, errorCode: " + errorCode);
-
- super.onStartFailure(errorCode);
- onAdvertiseStartFailure(errorCode);
- }
- };
-
- private final AdvertiseCallback mUnlockAdvertisingCallback = new AdvertiseCallback() {
- @Override
- public void onStartSuccess(AdvertiseSettings settingsInEffect) {
- super.onStartSuccess(settingsInEffect);
- onAdvertiseStartSuccess();
-
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Successfully started advertising service");
- }
- }
-
- @Override
- public void onStartFailure(int errorCode) {
- Log.e(TAG, "Failed to advertise, errorCode: " + errorCode);
-
- super.onStartFailure(errorCode);
- onAdvertiseStartFailure(errorCode);
- }
- };
-
- private final class CarTrustAgentBleWrapper extends ICarTrustAgentBleService.Stub {
- @Override
- public void registerBleCallback(ICarTrustAgentBleCallback callback) {
- mBleCallbacks.register(callback);
- }
-
- @Override
- public void unregisterBleCallback(ICarTrustAgentBleCallback callback) {
- mBleCallbacks.unregister(callback);
- }
-
- @Override
- public void startEnrolmentAdvertising() {
- stopEnrolmentAdvertising();
- stopUnlockAdvertising();
-
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "startEnrolmentAdvertising");
- }
- startAdvertising(mEnrolmentGattService, mEnrolmentAdvertisingCallback);
- }
-
- @Override
- public void stopEnrolmentAdvertising() {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "stopEnrolmentAdvertising");
- }
-
- stopAdvertising(mEnrolmentAdvertisingCallback);
- }
-
- @Override
- public void sendEnrolmentHandle(BluetoothDevice device, long handle) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "sendEnrolmentHandle: " + handle);
- }
- BluetoothGattCharacteristic enrolmentTokenHandle =
- mEnrolmentGattService.getCharacteristic(mEnrolmentTokenHandleUuid);
- enrolmentTokenHandle.setValue(getBytes(handle));
- notifyCharacteristicChanged(device, enrolmentTokenHandle, false);
- }
-
- @Override
- public void registerEnrolmentCallback(ICarTrustAgentEnrolmentCallback callback) {
- mEnrolmentCallbacks.register(callback);
- }
-
- @Override
- public void unregisterEnrolmentCallback(ICarTrustAgentEnrolmentCallback callback) {
- mEnrolmentCallbacks.unregister(callback);
- }
-
- @Override
- public void startUnlockAdvertising() {
- stopUnlockAdvertising();
- stopEnrolmentAdvertising();
-
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "startUnlockAdvertising");
- }
- startAdvertising(mUnlockGattService, mUnlockAdvertisingCallback);
- }
-
- @Override
- public void stopUnlockAdvertising() {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "stopUnlockAdvertising");
- }
- stopAdvertising(mUnlockAdvertisingCallback);
- }
-
- @Override
- public void registerUnlockCallback(ICarTrustAgentUnlockCallback callback) {
- mUnlockCallbacks.register(callback);
- }
-
- @Override
- public void unregisterUnlockCallback(ICarTrustAgentUnlockCallback callback) {
- mUnlockCallbacks.unregister(callback);
- }
-
- @Override
- public void setTokenRequestDelegate(ICarTrustAgentTokenRequestDelegate delegate) {
- mTokenRequestDelegate = delegate;
- }
-
- @Override
- public void revokeTrust() throws RemoteException {
- if (mTokenRequestDelegate != null) {
- mTokenRequestDelegate.revokeTrust();
- }
- }
-
- @Override
- public void addEscrowToken(byte[] token, int uid) throws RemoteException {
- if (mTokenRequestDelegate != null) {
- mTokenRequestDelegate.addEscrowToken(token, uid);
- }
- }
-
- @Override
- public void removeEscrowToken(long handle, int uid) throws RemoteException {
- if (mTokenRequestDelegate != null) {
- mTokenRequestDelegate.removeEscrowToken(handle, uid);
- }
- }
-
- @Override
- public void isEscrowTokenActive(long handle, int uid) throws RemoteException {
- if (mTokenRequestDelegate != null) {
- mTokenRequestDelegate.isEscrowTokenActive(handle, uid);
- }
- }
-
- @Override
- public void setTokenResponseCallback(ICarTrustAgentTokenResponseCallback callback) {
- mTokenResponseCallback = callback;
- }
-
- @Override
- public void onEscrowTokenAdded(byte[] token, long handle, int uid)
- throws RemoteException {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "onEscrowTokenAdded handle:" + handle + " uid:" + uid);
- }
-
- mTokenHandleSharedPreferences.edit()
- .putInt(String.valueOf(handle), uid)
- .apply();
- if (mTokenResponseCallback != null) {
- mTokenResponseCallback.onEscrowTokenAdded(token, handle, uid);
- }
- }
-
- @Override
- public void onEscrowTokenRemoved(long handle, boolean successful) throws RemoteException {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "onEscrowTokenRemoved handle:" + handle);
- }
-
- mTokenHandleSharedPreferences.edit()
- .remove(String.valueOf(handle))
- .apply();
- if (mTokenResponseCallback != null) {
- mTokenResponseCallback.onEscrowTokenRemoved(handle, successful);
- }
- }
-
- @Override
- public void onEscrowTokenActiveStateChanged(long handle, boolean active)
- throws RemoteException {
- if (mTokenResponseCallback != null) {
- mTokenResponseCallback.onEscrowTokenActiveStateChanged(handle, active);
- }
- }
-
- @Override
- public int getUserIdByEscrowTokenHandle(long tokenHandle) {
- return mTokenHandleSharedPreferences.getInt(String.valueOf(tokenHandle), -1);
- }
- }
-}
diff --git a/TrustAgent/src/com/android/car/trust/SimpleBleServer.java b/TrustAgent/src/com/android/car/trust/SimpleBleServer.java
deleted file mode 100644
index 260f5ac..0000000
--- a/TrustAgent/src/com/android/car/trust/SimpleBleServer.java
+++ /dev/null
@@ -1,225 +0,0 @@
-/*
- * Copyright (C) 2017 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;
-
-import static android.bluetooth.BluetoothProfile.GATT_SERVER;
-
-import android.app.Service;
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothGatt;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattServer;
-import android.bluetooth.BluetoothGattServerCallback;
-import android.bluetooth.BluetoothGattService;
-import android.bluetooth.BluetoothManager;
-import android.bluetooth.BluetoothProfile;
-import android.bluetooth.le.AdvertiseCallback;
-import android.bluetooth.le.AdvertiseData;
-import android.bluetooth.le.AdvertiseSettings;
-import android.bluetooth.le.BluetoothLeAdvertiser;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.os.Handler;
-import android.os.ParcelUuid;
-import android.util.Log;
-
-/**
- * A generic service to start a BLE
- */
-public abstract class SimpleBleServer extends Service {
- private static final String TAG = SimpleBleServer.class.getSimpleName();
-
- private static final int BLE_RETRY_LIMIT = 5;
- private static final int BLE_RETRY_INTERVAL_MS = 1000;
-
- private final Handler mHandler = new Handler();
-
- private BluetoothManager mBluetoothManager;
- private BluetoothLeAdvertiser mAdvertiser;
- private BluetoothGattServer mGattServer;
- private int mAdvertiserStartCount;
-
- /**
- * Starts the GATT server with the given {@link BluetoothGattService} and begins
- * advertising.
- *
- * <p>It is possible that BLE service is still in TURNING_ON state when this method is invoked.
- * Therefore, several retries will be made to ensure advertising is started.
- *
- * @param service {@link BluetoothGattService} that will be discovered by clients
- */
- protected void startAdvertising(BluetoothGattService service,
- AdvertiseCallback advertiseCallback) {
- if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
- Log.e(TAG, "System does not support BLE");
- return;
- }
-
- // Only open one Gatt server.
- if (mGattServer == null) {
- mBluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
- mGattServer = mBluetoothManager.openGattServer(this, mGattServerCallback);
-
- if (mGattServer == null) {
- Log.e(TAG, "Gatt Server not created");
- return;
- }
- }
-
- mGattServer.clearServices();
- mGattServer.addService(service);
-
- AdvertiseSettings settings = new AdvertiseSettings.Builder()
- .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
- .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
- .setConnectable(true)
- .build();
-
- AdvertiseData data = new AdvertiseData.Builder()
- .setIncludeDeviceName(true)
- .addServiceUuid(new ParcelUuid(service.getUuid()))
- .build();
-
- mAdvertiserStartCount = 0;
- startAdvertisingInternally(settings, data, advertiseCallback);
- }
-
- private void startAdvertisingInternally(AdvertiseSettings settings, AdvertiseData data,
- AdvertiseCallback advertiseCallback) {
- mAdvertiserStartCount += 1;
- mAdvertiser = BluetoothAdapter.getDefaultAdapter().getBluetoothLeAdvertiser();
- if (mAdvertiser == null && mAdvertiserStartCount < BLE_RETRY_LIMIT) {
- mHandler.postDelayed(
- () -> startAdvertisingInternally(settings, data, advertiseCallback),
- BLE_RETRY_INTERVAL_MS);
- } else {
- mHandler.removeCallbacks(null);
- mAdvertiser.startAdvertising(settings, data, advertiseCallback);
- mAdvertiserStartCount = 0;
- }
- }
-
- protected void stopAdvertising(AdvertiseCallback advertiseCallback) {
- if (mAdvertiser != null) {
- mAdvertiser.stopAdvertising(advertiseCallback);
- }
- }
-
- /**
- * Notifies the characteristic change via {@link BluetoothGattServer}
- */
- protected void notifyCharacteristicChanged(BluetoothDevice device,
- BluetoothGattCharacteristic characteristic, boolean confirm) {
- if (mGattServer != null) {
- mGattServer.notifyCharacteristicChanged(device, characteristic, confirm);
- }
- }
-
- @Override
- public void onDestroy() {
- // Stops the advertiser and GATT server. This needs to be done to avoid leaks
- if (mAdvertiser != null) {
- mAdvertiser.cleanup();
- }
-
- if (mGattServer != null) {
- mGattServer.clearServices();
- try {
- for (BluetoothDevice d : mBluetoothManager.getConnectedDevices(GATT_SERVER)) {
- mGattServer.cancelConnection(d);
- }
- } catch (UnsupportedOperationException e) {
- Log.e(TAG, "Error getting connected devices", e);
- } finally {
- mGattServer.close();
- }
- }
- super.onDestroy();
- }
-
- // Delegate to subclass
- protected void onAdvertiseStartSuccess() { }
- protected void onAdvertiseStartFailure(int errorCode) { }
- protected void onAdvertiseDeviceConnected(BluetoothDevice device) { }
- protected void onAdvertiseDeviceDisconnected(BluetoothDevice device) { }
-
- /**
- * Triggered when this BleService receives a write request from a remote
- * device. Sub-classes should implement how to handle requests.
- */
- protected abstract void onCharacteristicWrite(BluetoothDevice device, int requestId,
- BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean
- responseNeeded, int offset, byte[] value);
-
- /**
- * Triggered when this BleService receives a read request from a remote device.
- */
- protected abstract void onCharacteristicRead(BluetoothDevice device,
- int requestId, int offset, final BluetoothGattCharacteristic characteristic);
-
- private final BluetoothGattServerCallback mGattServerCallback =
- new BluetoothGattServerCallback() {
- @Override
- public void onConnectionStateChange(BluetoothDevice device,
- final int status, final int newState) {
- switch (newState) {
- case BluetoothProfile.STATE_CONNECTED:
- onAdvertiseDeviceConnected(device);
- break;
- case BluetoothProfile.STATE_DISCONNECTED:
- onAdvertiseDeviceDisconnected(device);
- break;
- default:
- Log.w(TAG, "Connection state not connecting or disconnecting; ignoring: "
- + newState);
- }
- }
-
- @Override
- public void onServiceAdded(final int status, BluetoothGattService service) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Service added status: " + status + " uuid: " + service.getUuid());
- }
- }
-
- @Override
- public void onCharacteristicReadRequest(BluetoothDevice device,
- int requestId, int offset, final BluetoothGattCharacteristic characteristic) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Read request for characteristic: " + characteristic.getUuid());
- }
-
- mGattServer.sendResponse(device, requestId,
- BluetoothGatt.GATT_SUCCESS, offset, characteristic.getValue());
- onCharacteristicRead(device, requestId, offset, characteristic);
- }
-
- @Override
- public void onCharacteristicWriteRequest(final BluetoothDevice device, int requestId,
- BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean
- responseNeeded, int offset, byte[] value) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Write request for characteristic: " + characteristic.getUuid());
- }
-
- mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS,
- offset, value);
- onCharacteristicWrite(device, requestId, characteristic,
- preparedWrite, responseNeeded, offset, value);
- }
- };
-}
diff --git a/car-lib/api/current.txt b/car-lib/api/current.txt
index 4d000ee..3eda0a5 100644
--- a/car-lib/api/current.txt
+++ b/car-lib/api/current.txt
@@ -1,9 +1,11 @@
package android.car {
public final class Car {
- method public void connect() throws java.lang.IllegalStateException;
- method public static android.car.Car createCar(android.content.Context, android.content.ServiceConnection, android.os.Handler);
- method public static android.car.Car createCar(android.content.Context, android.content.ServiceConnection);
+ method public deprecated void connect() throws java.lang.IllegalStateException;
+ method public static deprecated android.car.Car createCar(android.content.Context, android.content.ServiceConnection, android.os.Handler);
+ method public static deprecated android.car.Car createCar(android.content.Context, android.content.ServiceConnection);
+ method public static android.car.Car createCar(android.content.Context);
+ method public static android.car.Car createCar(android.content.Context, android.os.Handler);
method public void disconnect();
method public int getCarConnectionType();
method public java.lang.Object getCarManager(java.lang.String) throws android.car.CarNotConnectedException;
diff --git a/car-lib/src/android/car/Car.java b/car-lib/src/android/car/Car.java
index e262e91..b4c4509 100644
--- a/car-lib/src/android/car/Car.java
+++ b/car-lib/src/android/car/Car.java
@@ -45,6 +45,7 @@
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
+import android.os.ServiceManager;
import android.os.UserHandle;
import android.util.Log;
@@ -537,7 +538,10 @@
* service's main thread. Note: the service connection listener will be always on the main
* thread regardless of the handler given.
* @return Car instance if system is in car environment and returns {@code null} otherwise.
+ *
+ * @deprecated use {@link #createCar(Context, Handler)} instead.
*/
+ @Deprecated
public static Car createCar(Context context, ServiceConnection serviceConnectionListener,
@Nullable Handler handler) {
if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
@@ -557,11 +561,44 @@
* Looper}.
*
* @see #createCar(Context, ServiceConnection, Handler)
+ *
+ * @deprecated use {@link #createCar(Context, Handler)} instead.
*/
+ @Deprecated
public static Car createCar(Context context, ServiceConnection serviceConnectionListener) {
return createCar(context, serviceConnectionListener, null);
}
+ /**
+ * Creates new {@link Car} object which connected synchronously to Car Service and ready to use.
+ *
+ * @param context application's context
+ *
+ * @return Car object if operation succeeded, otherwise null.
+ */
+ @Nullable
+ public static Car createCar(Context context) {
+ return createCar(context, (Handler) null);
+ }
+
+ /**
+ * Creates new {@link Car} object which connected synchronously to Car Service and ready to use.
+ *
+ * @param context application's context
+ * @param handler the handler on which the manager's callbacks will be executed, or null to
+ * execute on the application's main thread.
+ *
+ * @return Car object if operation succeeded, otherwise null.
+ */
+ @Nullable
+ public static Car createCar(Context context, @Nullable Handler handler) {
+ IBinder service = ServiceManager.getService("car_service");
+ if (service == null) {
+ return null;
+ }
+ return new Car(context, ICar.Stub.asInterface(service), handler);
+ }
+
private Car(Context context, ServiceConnection serviceConnectionListener,
@Nullable Handler handler) {
mContext = context;
@@ -606,7 +643,11 @@
* Connect to car service. This can be called while it is disconnected.
* @throws IllegalStateException If connection is still on-going from previous
* connect call or it is already connected
+ *
+ * @deprecated this method is not need if this object is created via
+ * {@link #createCar(Context, Handler)}.
*/
+ @Deprecated
public void connect() throws IllegalStateException {
synchronized (this) {
if (mConnectionState != STATE_DISCONNECTED) {
diff --git a/car_product/build/car.mk b/car_product/build/car.mk
index 1e67b47..f4a67c1 100644
--- a/car_product/build/car.mk
+++ b/car_product/build/car.mk
@@ -86,7 +86,6 @@
# Automotive specific packages
PRODUCT_PACKAGES += \
CarService \
- CarTrustAgentService \
CarDialerApp \
CarRadioApp \
OverviewApp \
diff --git a/car_product/overlay/frameworks/base/core/res/res/values/styles_device_default.xml b/car_product/overlay/frameworks/base/core/res/res/values/styles_device_default.xml
index c0c979c..32783b9 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values/styles_device_default.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values/styles_device_default.xml
@@ -94,4 +94,8 @@
<item name="thumb">@drawable/car_seekbar_thumb</item>
</style>
+ <style name="Widget.DeviceDefault.ActionBar.Solid" parent="Widget.Material.ActionBar.Solid">
+ <item name="textSize">@dimen/car_body3_size</item>
+ </style>
+
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values/themes_device_defaults.xml b/car_product/overlay/frameworks/base/core/res/res/values/themes_device_defaults.xml
index 228079e..23a56b5 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values/themes_device_defaults.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values/themes_device_defaults.xml
@@ -41,6 +41,7 @@
<item name="textAppearanceListItem">@style/TextAppearance.DeviceDefault.Large</item>
<item name="textAppearanceListItemSmall">@style/TextAppearance.DeviceDefault.Large</item>
<item name="textAppearanceListItemSecondary">@style/TextAppearance.DeviceDefault.Small</item>
+ <item name="actionBarSize">@dimen/car_app_bar_height</item>
</style>
<style name="Theme.DeviceDefault.Dialog" parent="Theme.Material.Dialog">
<item name="textAppearanceLarge">@style/TextAppearance.DeviceDefault.Large</item>
@@ -94,6 +95,10 @@
<item name="textAppearanceListItemSmall">@style/TextAppearance.DeviceDefault.Large</item>
<item name="textAppearanceListItemSecondary">@style/TextAppearance.DeviceDefault.Small</item>
</style>
+
+ <style name="Theme.DeviceDefault.Settings.Dialog" parent="Theme.DeviceDefault.Dialog.Alert">
+ </style>
+
<!-- The light theme is defined to be the same as the default since currently there is only one
defined theme palette -->
<style name="Theme.DeviceDefault.Light"/>
diff --git a/tests/DirectRenderingClusterSample/res/drawable/speedometer.xml b/tests/DirectRenderingClusterSample/res/drawable/speedometer.xml
new file mode 100644
index 0000000..9879024
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/drawable/speedometer.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt"
+ android:width="200dp"
+ android:height="200dp"
+ android:viewportHeight="64"
+ android:viewportWidth="64">
+
+ <path
+ android:pathData="M0,32
+ A32,32 0 1,1 64,32
+ A32,32 0 1,1 0,32 Z">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:centerX="32"
+ android:centerY="32"
+ android:gradientRadius="32"
+ android:type="radial">
+ <item
+ android:color="#FF000000"
+ android:offset="0.0" />
+ <item
+ android:color="#FF000000"
+ android:offset="0.92" />
+ <item
+ android:color="#00000000"
+ android:offset="1.0" />
+ </gradient>
+ </aapt:attr>
+ </path>
+
+ <path
+ android:fillColor="#000000"
+ android:strokeColor="#FFFFFF"
+ android:strokeWidth="0.25"
+ android:pathData="M2,32
+ A30,30 0 1,1 62,32
+ A30,30 0 1,1 2,32 Z" />
+
+</vector>
\ No newline at end of file
diff --git a/tests/DirectRenderingClusterSample/res/layout/activity_main.xml b/tests/DirectRenderingClusterSample/res/layout/activity_main.xml
index 2cb9662..23f01b0 100644
--- a/tests/DirectRenderingClusterSample/res/layout/activity_main.xml
+++ b/tests/DirectRenderingClusterSample/res/layout/activity_main.xml
@@ -1,12 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/background_dark"
- tools:context=".MainClusterActivity"
- android:windowIsFloating="true">
+ android:windowIsFloating="true"
+ tools:context=".MainClusterActivity">
+
+ <androidx.constraintlayout.widget.Guideline
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/left_unobscured"
+ app:layout_constraintGuide_begin="@dimen/speedometer_overlap_width"
+ android:orientation="vertical"/>
+
+ <androidx.constraintlayout.widget.Guideline
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/right_unobscured"
+ app:layout_constraintGuide_end="@dimen/speedometer_overlap_width"
+ android:orientation="vertical"/>
<LinearLayout
android:layout_width="match_parent"
@@ -32,27 +48,27 @@
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@drawable/btn_nav"
- android:layout_margin="10dp"
+ android:layout_margin="5dp"
android:focusableInTouchMode="true" />
<Button
android:id="@+id/btn_phone"
android:layout_width="48dp"
android:layout_height="48dp"
- android:layout_margin="10dp"
+ android:layout_margin="5dp"
android:background="@drawable/btn_phone"
android:focusableInTouchMode="true" />
<Button
android:id="@+id/btn_music"
android:layout_width="48dp"
android:layout_height="48dp"
- android:layout_margin="10dp"
+ android:layout_margin="5dp"
android:background="@drawable/btn_music"
android:focusableInTouchMode="true" />
<Button
android:id="@+id/btn_car_info"
android:layout_width="48dp"
android:layout_height="48dp"
- android:layout_margin="10dp"
+ android:layout_margin="5dp"
android:background="@drawable/btn_car_info"
android:focusableInTouchMode="true" />
@@ -69,12 +85,24 @@
</LinearLayout>
</LinearLayout>
- <TextView
- android:id="@+id/text_overlay"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_centerInParent="true"
- android:background="@android:color/background_light"
- android:textSize="30sp"
- />
-</RelativeLayout>
+ <ImageView
+ android:id="@+id/maneuver"
+ android:layout_width="@dimen/speedometer_width"
+ android:layout_height="@dimen/speedometer_height"
+ android:src="@drawable/speedometer"
+ android:elevation="2dp"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintRight_toLeftOf="@+id/left_unobscured"/>
+
+ <ImageView
+ android:id="@+id/maneuver"
+ android:layout_width="@dimen/speedometer_width"
+ android:layout_height="@dimen/speedometer_height"
+ android:src="@drawable/speedometer"
+ android:elevation="2dp"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintLeft_toRightOf="@+id/right_unobscured"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/tests/DirectRenderingClusterSample/res/layout/fragment_navigation.xml b/tests/DirectRenderingClusterSample/res/layout/fragment_navigation.xml
index 865a3b5..54894c2 100644
--- a/tests/DirectRenderingClusterSample/res/layout/fragment_navigation.xml
+++ b/tests/DirectRenderingClusterSample/res/layout/fragment_navigation.xml
@@ -34,13 +34,13 @@
<ImageView
android:layout_width="match_parent"
- android:layout_height="30dp"
+ android:layout_height="@dimen/navigation_gradient_height"
android:src="@drawable/gradient_top"
app:layout_constraintTop_toTopOf="parent"/>
<ImageView
android:layout_width="match_parent"
- android:layout_height="30dp"
+ android:layout_height="@dimen/navigation_gradient_height"
android:src="@drawable/gradient_bottom"
app:layout_constraintBottom_toBottomOf="parent"/>
diff --git a/tests/DirectRenderingClusterSample/res/values/dimens.xml b/tests/DirectRenderingClusterSample/res/values/dimens.xml
index 47c8224..80f5206 100644
--- a/tests/DirectRenderingClusterSample/res/values/dimens.xml
+++ b/tests/DirectRenderingClusterSample/res/values/dimens.xml
@@ -2,4 +2,10 @@
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
+ <!-- Size and position of speedometers -->
+ <dimen name="speedometer_height">500dp</dimen>
+ <dimen name="speedometer_width">500dp</dimen>
+ <dimen name="speedometer_overlap_width">80dp</dimen>
+ <!-- Navigation fragment gradients -->
+ <dimen name="navigation_gradient_height">30dp</dimen>
</resources>
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ActivityMonitor.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ActivityMonitor.java
index 8b69e65..d102a49 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ActivityMonitor.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ActivityMonitor.java
@@ -22,6 +22,7 @@
import android.app.IProcessObserver;
import android.app.TaskStackListener;
import android.content.ComponentName;
+import android.os.Handler;
import android.os.RemoteException;
import android.util.Log;
@@ -51,6 +52,7 @@
private IActivityManager mActivityManager;
// Listeners of top activity changes, indexed by the displayId they are interested on.
private final Map<Integer, Set<ActivityListener>> mListeners = new HashMap<>();
+ private final Handler mHandler = new Handler();
private final IProcessObserver.Stub mProcessObserver = new IProcessObserver.Stub() {
@Override
public void onForegroundActivitiesChanged(int pid, int uid, boolean foregroundActivities) {
@@ -120,19 +122,25 @@
mActivityManager = null;
}
+ /**
+ * Notifies listeners on changes of top activities. {@link ActivityManager} might trigger
+ * updates on threads different than UI.
+ */
private void notifyTopActivities() {
- try {
- List<StackInfo> infos = mActivityManager.getAllStackInfos();
- for (StackInfo info : infos) {
- Set<ActivityListener> listeners = mListeners.get(info.displayId);
- if (listeners != null && !listeners.isEmpty()) {
- for (ActivityListener listener : listeners) {
- listener.onTopActivityChanged(info.displayId, info.topActivity);
+ mHandler.post(() -> {
+ try {
+ List<StackInfo> infos = mActivityManager.getAllStackInfos();
+ for (StackInfo info : infos) {
+ Set<ActivityListener> listeners = mListeners.get(info.displayId);
+ if (listeners != null && !listeners.isEmpty()) {
+ for (ActivityListener listener : listeners) {
+ listener.onTopActivityChanged(info.displayId, info.topActivity);
+ }
}
}
+ } catch (RemoteException e) {
+ Log.e(TAG, "Cannot getTasks", e);
}
- } catch (RemoteException e) {
- Log.e(TAG, "Cannot getTasks", e);
- }
+ });
}
}
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterRenderingServiceImpl.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterRenderingServiceImpl.java
index 74125c3..8a383fd 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterRenderingServiceImpl.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterRenderingServiceImpl.java
@@ -15,29 +15,17 @@
*/
package android.car.cluster.sample;
-import static android.car.cluster.CarInstrumentClusterManager.CATEGORY_NAVIGATION;
-import static android.content.Intent.ACTION_USER_SWITCHED;
-import static android.content.Intent.ACTION_USER_UNLOCKED;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static java.lang.Integer.parseInt;
-import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.car.CarNotConnectedException;
-import android.car.cluster.CarInstrumentClusterManager;
import android.car.cluster.ClusterActivityState;
import android.car.cluster.renderer.InstrumentClusterRenderingService;
import android.car.cluster.renderer.NavigationRenderer;
import android.car.navigation.CarNavigationInstrumentCluster;
-import android.content.ActivityNotFoundException;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
import android.graphics.Rect;
import android.hardware.display.DisplayManager.DisplayListener;
import android.os.Bundle;
@@ -64,7 +52,6 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
-import java.util.Objects;
/**
* Implementation of {@link InstrumentClusterRenderingService} which renders an activity on a
@@ -83,29 +70,15 @@
static final int MSG_ON_KEY_EVENT = 3;
static final int MSG_REGISTER_CLIENT = 4;
static final int MSG_UNREGISTER_CLIENT = 5;
- static final int MSG_ON_FREE_NAVIGATION_ACTIVITY_STATE_CHANGED = 6;
static final String MSG_KEY_CATEGORY = "category";
static final String MSG_KEY_ACTIVITY_DISPLAY_ID = "activity_display_id";
static final String MSG_KEY_ACTIVITY_STATE = "activity_state";
static final String MSG_KEY_KEY_EVENT = "key_event";
- static final String MSG_KEY_FREE_NAVIGATION_ACTIVITY_NAME = "free_navigation_activity_name";
- static final String MSG_KEY_FREE_NAVIGATION_ACTIVITY_VISIBLE =
- "free_navigation_activity_visible";
-
- private static final int NAVIGATION_ACTIVITY_MAX_RETRIES = 10;
- private static final int NAVIGATION_ACTIVITY_RETRY_INTERVAL_MS = 1000;
private List<Messenger> mClients = new ArrayList<>();
private ClusterDisplayProvider mDisplayProvider;
private int mDisplayId = NO_DISPLAY;
- private UserReceiver mUserReceiver;
- private final Handler mHandler = new Handler();
private final IBinder mLocalBinder = new Messenger(new MessageHandler(this)).getBinder();
- private final Runnable mRetryLaunchNavigationActivity = this::tryLaunchNavigationActivity;
- private int mNavigationDisplayId = NO_DISPLAY;
- private ComponentName mFreeNavigationActivity;
- private ActivityMonitor mActivityMonitor = new ActivityMonitor();
- private boolean mFreeNavigationActivityVisible;
private final DisplayListener mDisplayListener = new DisplayListener() {
@Override
@@ -126,31 +99,6 @@
}
};
- private static class UserReceiver extends BroadcastReceiver {
- private WeakReference<ClusterRenderingServiceImpl> mService;
-
- UserReceiver(ClusterRenderingServiceImpl service) {
- mService = new WeakReference<>(service);
- }
-
- public void register(Context context) {
- IntentFilter intentFilter = new IntentFilter(ACTION_USER_UNLOCKED);
- intentFilter.addAction(ACTION_USER_SWITCHED);
- context.registerReceiver(this, intentFilter);
- }
-
- public void unregister(Context context) {
- context.unregisterReceiver(this);
- }
-
- @Override
- public void onReceive(Context context, Intent intent) {
- ClusterRenderingServiceImpl service = mService.get();
- Log.d(TAG, "Broadcast received: " + intent);
- service.tryLaunchNavigationActivity();
- }
- }
-
private static class MessageHandler extends Handler {
private final WeakReference<ClusterRenderingServiceImpl> mService;
@@ -175,15 +123,10 @@
category, options, options.getLaunchDisplayId()));
mService.get().setClusterActivityState(category, state);
Log.d(TAG, String.format("activity state set: %s = %s", category, state));
-
- // Starting a default navigation activity. This would take place until any
- // navigation app takes focus.
- mService.get().startNavigationActivity(displayId);
break;
}
case MSG_REGISTER_CLIENT:
mService.get().mClients.add(msg.replyTo);
- mService.get().notifyFreeNavigationActivityChange();
break;
case MSG_UNREGISTER_CLIENT:
mService.get().mClients.remove(msg.replyTo);
@@ -197,17 +140,6 @@
}
}
- private ActivityMonitor.ActivityListener mNavigationActivityMonitor = (displayId, activity) -> {
- if (displayId != mNavigationDisplayId) {
- return;
- }
- boolean activityVisible = activity != null && activity.equals(mFreeNavigationActivity);
- if (activityVisible != mFreeNavigationActivityVisible) {
- mFreeNavigationActivityVisible = activityVisible;
- notifyFreeNavigationActivityChange();
- }
- };
-
@Override
public IBinder onBind(Intent intent) {
Log.d(TAG, "onBind, intent: " + intent);
@@ -221,9 +153,6 @@
super.onCreate();
Log.d(TAG, "onCreate");
mDisplayProvider = new ClusterDisplayProvider(this, mDisplayListener);
- mUserReceiver = new UserReceiver(this);
- mUserReceiver.register(this);
- mActivityMonitor.start();
}
private void launchMainActivity() {
@@ -265,14 +194,6 @@
}
@Override
- public void onDestroy() {
- super.onDestroy();
- Log.w(TAG, "onDestroy");
- mUserReceiver.unregister(this);
- mActivityMonitor.stop();
- }
-
- @Override
protected NavigationRenderer getNavigationRenderer() {
NavigationRenderer navigationRenderer = new NavigationRenderer() {
@Override
@@ -406,119 +327,4 @@
}
}
}
-
- private void startNavigationActivity(int displayId) {
- mActivityMonitor.removeListener(mNavigationDisplayId, mNavigationActivityMonitor);
- mActivityMonitor.addListener(displayId, mNavigationActivityMonitor);
- mNavigationDisplayId = displayId;
- tryLaunchNavigationActivity();
- }
-
- /**
- * Tries to start a default navigation activity in the cluster. During system initialization
- * launching user activities might fail due the system not being ready or {@link PackageManager}
- * not being able to resolve the implicit intent. It is also possible that the system doesn't
- * have a default navigation activity selected yet.
- */
- private void tryLaunchNavigationActivity() {
- int userHandle = ActivityManager.getCurrentUser();
- if (userHandle == UserHandle.USER_SYSTEM || mNavigationDisplayId == NO_DISPLAY) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, String.format("Launch activity ignored (user: %d, display: %d)",
- userHandle, mNavigationDisplayId));
- }
- // Not ready to launch yet.
- return;
- }
- mHandler.removeCallbacks(mRetryLaunchNavigationActivity);
-
- ComponentName navigationActivity = getNavigationActivity();
- if (!Objects.equals(navigationActivity, mFreeNavigationActivity)) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Navigation activity change detected: " + navigationActivity);
- }
- mFreeNavigationActivity = navigationActivity;
- notifyFreeNavigationActivityChange();
- }
-
- try {
- if (navigationActivity == null) {
- throw new ActivityNotFoundException();
- }
- Intent intent = new Intent(Intent.ACTION_MAIN).addCategory(CATEGORY_NAVIGATION)
- .setPackage(navigationActivity.getPackageName())
- .setComponent(navigationActivity)
- .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- Log.d(TAG, "Launching: " + intent + " on display: " + mNavigationDisplayId);
- Bundle activityOptions = ActivityOptions.makeBasic()
- .setLaunchDisplayId(mNavigationDisplayId)
- .toBundle();
-
- startActivityAsUser(intent, activityOptions, UserHandle.CURRENT);
- } catch (ActivityNotFoundException ex) {
- // Some activities might not be available right on startup. We will retry.
- mHandler.postDelayed(mRetryLaunchNavigationActivity,
- NAVIGATION_ACTIVITY_RETRY_INTERVAL_MS);
- } catch (Exception ex) {
- Log.e(TAG, "Unable to start navigation activity: " + navigationActivity, ex);
- }
- }
-
- private void notifyFreeNavigationActivityChange() {
- Bundle bundle = new Bundle();
- bundle.putParcelable(MSG_KEY_FREE_NAVIGATION_ACTIVITY_NAME, mFreeNavigationActivity);
- bundle.putBoolean(MSG_KEY_FREE_NAVIGATION_ACTIVITY_VISIBLE, mFreeNavigationActivityVisible);
- broadcastClientMessage(MSG_ON_FREE_NAVIGATION_ACTIVITY_STATE_CHANGED, bundle);
- }
-
- /**
- * Returns a default navigation activity to show in the cluster.
- * In the current implementation we search for an activity with the
- * {@link CarInstrumentClusterManager#CATEGORY_NAVIGATION} category from the same navigation app
- * selected from CarLauncher (see CarLauncher#getMapsIntent()).
- * Alternatively, other implementations could:
- * <ul>
- * <li>Read this package from a resource (having a OEM default activity to show)
- * <li>Let the user select one from settings.
- * </ul>
- */
- private ComponentName getNavigationActivity() {
- PackageManager pm = getPackageManager();
- int userId = ActivityManager.getCurrentUser();
-
- // Get currently selected navigation app.
- Intent intent = Intent.makeMainSelectorActivity(Intent.ACTION_MAIN,
- Intent.CATEGORY_APP_MAPS);
- ResolveInfo navigationApp = pm.resolveActivityAsUser(intent,
- PackageManager.MATCH_DEFAULT_ONLY, userId);
-
- // Get all possible cluster activities
- intent = new Intent(Intent.ACTION_MAIN).addCategory(CATEGORY_NAVIGATION);
- List<ResolveInfo> candidates = pm.queryIntentActivitiesAsUser(intent, 0, userId);
-
- // If there is a select navigation app, try finding a matching auxiliary navigation activity
- if (navigationApp != null) {
- Log.d(TAG, "Current navigation app: " + navigationApp);
- for (ResolveInfo candidate : candidates) {
- Log.d(TAG, "Candidate: " + candidate);
- if (candidate.activityInfo.packageName.equals(navigationApp.activityInfo
- .packageName)) {
- Log.d(TAG, "Found activity: " + candidate);
- return new ComponentName(candidate.activityInfo.packageName,
- candidate.activityInfo.name);
- }
- }
- } else {
- Log.d(TAG, "NO CURRENT ACTIVITY");
- for (ResolveInfo candidate : candidates) {
- Log.d(TAG, "Candidate: " + candidate);
- }
- }
-
- // During initialization implicit intents might not provided a result. We will just
- // retry until we find one, or we exhaust the retries.
- Log.d(TAG, "No default activity found (it might not be available yet).");
- return null;
- }
-
}
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterViewModel.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterViewModel.java
index 55c9e54..61c8dd0 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterViewModel.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterViewModel.java
@@ -17,6 +17,7 @@
import android.app.Application;
import android.content.ComponentName;
+import android.util.Log;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
@@ -29,44 +30,21 @@
* {@link AndroidViewModel} for cluster information.
*/
public class ClusterViewModel extends AndroidViewModel {
- /**
- * Reference to a component (e.g.: an activity) and whether such component is visible or not.
- */
- public static class ComponentVisibility {
- /**
- * Application component name
- */
- public final ComponentName mComponent;
- /**
- * Whether the component is currently visible to the user or not.
- */
- public final boolean mIsVisible;
+ private static final String TAG = "Cluster.ViewModel";
- /**
- * Creates a new component visibility reference
- */
- private ComponentVisibility(ComponentName component, boolean isVisible) {
- mComponent = component;
- mIsVisible = isVisible;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- ComponentVisibility that = (ComponentVisibility) o;
- return mIsVisible == that.mIsVisible
- && Objects.equals(mComponent, that.mComponent);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(mComponent, mIsVisible);
- }
+ public enum NavigationActivityState {
+ /** No activity has been selected to be displayed on the navigation fragment yet */
+ NOT_SELECTED,
+ /** An activity has been selected, but it is not yet visible to the user */
+ LOADING,
+ /** Navigation activity is visible to the user */
+ VISIBLE,
}
- private final MutableLiveData<ComponentVisibility> mFreeNavigationActivity =
- new MutableLiveData<>(new ComponentVisibility(null, false));
+ private ComponentName mFreeNavigationActivity;
+ private ComponentName mCurrentNavigationActivity;
+ private final MutableLiveData<NavigationActivityState> mNavigationActivityStateLiveData =
+ new MutableLiveData<>();
private final MutableLiveData<Boolean> mNavigationFocus = new MutableLiveData<>(false);
/**
@@ -77,20 +55,17 @@
}
/**
- * Returns a {@link LiveData} providing the activity selected to be displayed on the cluster
- * when navigation focus is not granted (a.k.a.: free navigation). It also indicates whether
- * such activity is currently visible to the user or not.
+ * Returns a {@link LiveData} providing the current state of the activity displayed on the
+ * navigation fragment.
*/
- public LiveData<ComponentVisibility> getFreeNavigationActivity() {
- return mFreeNavigationActivity;
+ public LiveData<NavigationActivityState> getNavigationActivityState() {
+ return mNavigationActivityStateLiveData;
}
/**
* Returns a {@link LiveData} indicating whether navigation focus is currently being granted
* or not. This indicates whether a navigation application is currently providing driving
- * directions. Instrument cluster can use this signal to show/hide turn-by-turn
- * directions UI, and hide/show the free navigation activity
- * (see {@link #getFreeNavigationActivity()}).
+ * directions.
*/
public LiveData<Boolean> getNavigationFocus() {
return mNavigationFocus;
@@ -98,12 +73,22 @@
/**
* Sets the activity selected to be displayed on the cluster when no driving directions are
- * being provided, and whether such activity is currently visible to the user or not
+ * being provided.
*/
- public void setFreeNavigationActivity(ComponentName application, boolean isVisible) {
- ComponentVisibility newValue = new ComponentVisibility(application, isVisible);
- if (!Objects.equals(mFreeNavigationActivity.getValue(), newValue)) {
- mFreeNavigationActivity.setValue(new ComponentVisibility(application, isVisible));
+ public void setFreeNavigationActivity(ComponentName activity) {
+ if (!Objects.equals(activity, mFreeNavigationActivity)) {
+ mFreeNavigationActivity = activity;
+ updateNavigationActivityLiveData();
+ }
+ }
+
+ /**
+ * Sets the activity currently being displayed on the cluster.
+ */
+ public void setCurrentNavigationActivity(ComponentName activity) {
+ if (!Objects.equals(activity, mCurrentNavigationActivity)) {
+ mCurrentNavigationActivity = activity;
+ updateNavigationActivityLiveData();
}
}
@@ -113,6 +98,34 @@
public void setNavigationFocus(boolean navigationFocus) {
if (mNavigationFocus.getValue() == null || mNavigationFocus.getValue() != navigationFocus) {
mNavigationFocus.setValue(navigationFocus);
+ updateNavigationActivityLiveData();
+ }
+ }
+
+ private void updateNavigationActivityLiveData() {
+ NavigationActivityState newState = calculateNavigationActivityState();
+ if (newState != mNavigationActivityStateLiveData.getValue()) {
+ mNavigationActivityStateLiveData.setValue(newState);
+ }
+ }
+
+ private NavigationActivityState calculateNavigationActivityState() {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, String.format("Current state: current activity = '%s', free nav activity = "
+ + "'%s', focus = %s", mCurrentNavigationActivity,
+ mFreeNavigationActivity,
+ mNavigationFocus.getValue()));
+ }
+ if (mNavigationFocus.getValue() != null && mNavigationFocus.getValue()) {
+ // Car service controls which activity is displayed while driving, so we assume this
+ // has already been taken care of.
+ return NavigationActivityState.VISIBLE;
+ } else if (mFreeNavigationActivity == null) {
+ return NavigationActivityState.NOT_SELECTED;
+ } else if (Objects.equals(mFreeNavigationActivity, mCurrentNavigationActivity)) {
+ return NavigationActivityState.VISIBLE;
+ } else {
+ return NavigationActivityState.LOADING;
}
}
}
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MainClusterActivity.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MainClusterActivity.java
index 2a2ac73..7f32a3b 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MainClusterActivity.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MainClusterActivity.java
@@ -20,12 +20,7 @@
import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_KEY_ACTIVITY_DISPLAY_ID;
import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_KEY_ACTIVITY_STATE;
import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_KEY_CATEGORY;
-import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_KEY_FREE_NAVIGATION_ACTIVITY_NAME;
-import static android.car.cluster.sample.ClusterRenderingServiceImpl
- .MSG_KEY_FREE_NAVIGATION_ACTIVITY_VISIBLE;
import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_KEY_KEY_EVENT;
-import static android.car.cluster.sample.ClusterRenderingServiceImpl
- .MSG_ON_FREE_NAVIGATION_ACTIVITY_STATE_CHANGED;
import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_ON_KEY_EVENT;
import static android.car.cluster.sample.ClusterRenderingServiceImpl
.MSG_ON_NAVIGATION_STATE_CHANGED;
@@ -33,14 +28,25 @@
import static android.car.cluster.sample.ClusterRenderingServiceImpl
.MSG_SET_ACTIVITY_LAUNCH_OPTIONS;
import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_UNREGISTER_CLIENT;
+import static android.content.Intent.ACTION_USER_SWITCHED;
+import static android.content.Intent.ACTION_USER_UNLOCKED;
+import android.app.ActivityManager;
+import android.app.ActivityOptions;
import android.car.Car;
import android.car.CarAppFocusManager;
import android.car.CarNotConnectedException;
+import android.car.cluster.CarInstrumentClusterManager;
import android.car.cluster.ClusterActivityState;
+import android.content.ActivityNotFoundException;
+import android.content.BroadcastReceiver;
import android.content.ComponentName;
+import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter;
import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Handler;
@@ -48,6 +54,7 @@
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
+import android.os.UserHandle;
import android.util.Log;
import android.util.SparseArray;
import android.view.Display;
@@ -69,11 +76,30 @@
import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
+import java.util.List;
+/**
+ * Main activity displayed on the instrument cluster. This activity contains fragments for each of
+ * the cluster "facets" (e.g.: navigation, communication, media and car state). Users can navigate
+ * to each facet by using the steering wheel buttons.
+ * <p>
+ * This activity runs on "system user" (see {@link UserHandle#USER_SYSTEM}) but it is visible on
+ * all users (the same activity remains active even during user switch).
+ * <p>
+ * This activity also launches a default navigation app inside a virtual display (which is located
+ * inside {@link NavigationFragment}). This navigation app is launched when:
+ * <ul>
+ * <li>Virtual display for navigation apps is ready.
+ * <li>After every user switch.
+ * </ul>
+ * This is necessary because the navigation app runs under a normal user, and different users will
+ * see different instances of the same application, with their own personalized data.
+ */
public class MainClusterActivity extends FragmentActivity {
private static final String TAG = "Cluster.MainActivity";
private static final NavigationState NULL_NAV_STATE = new NavigationState.Builder().build();
+ private static final int NO_DISPLAY = -1;
private ViewPager mPager;
private NavStateController mNavStateController;
@@ -89,13 +115,26 @@
private Car mCar;
private CarAppFocusManager mCarAppFocusManager;
- public static class VirtualDisplay {
- public final int mDisplayId;
- public final Rect mBounds;
+ private static final int NAVIGATION_ACTIVITY_RETRY_INTERVAL_MS = 1000;
- public VirtualDisplay(int displayId, Rect bounds) {
+ private UserReceiver mUserReceiver;
+ private ActivityMonitor mActivityMonitor = new ActivityMonitor();
+ private final Handler mHandler = new Handler();
+ private final Runnable mRetryLaunchNavigationActivity = this::tryLaunchNavigationActivity;
+ private int mNavigationDisplayId = NO_DISPLAY;
+
+ /**
+ * Description of a virtual display
+ */
+ public static class VirtualDisplay {
+ /** Identifier of the display */
+ public final int mDisplayId;
+ /** Rectangular area inside this display that can be viewed without obstructions */
+ public final Rect mUnobscuredBounds;
+
+ public VirtualDisplay(int displayId, Rect unobscuredBounds) {
mDisplayId = displayId;
- mBounds = bounds;
+ mUnobscuredBounds = unobscuredBounds;
}
}
@@ -184,19 +223,46 @@
mActivity.get().onNavigationStateChange(navState);
}
break;
- case MSG_ON_FREE_NAVIGATION_ACTIVITY_STATE_CHANGED:
- ComponentName activity = data.getParcelable(
- MSG_KEY_FREE_NAVIGATION_ACTIVITY_NAME);
- boolean isVisible = data.getBoolean(MSG_KEY_FREE_NAVIGATION_ACTIVITY_VISIBLE);
- mActivity.get().mClusterViewModel.setFreeNavigationActivity(activity,
- isVisible);
- break;
default:
super.handleMessage(msg);
}
}
}
+ private ActivityMonitor.ActivityListener mNavigationActivityMonitor = (displayId, activity) -> {
+ if (displayId != mNavigationDisplayId) {
+ return;
+ }
+ mClusterViewModel.setCurrentNavigationActivity(activity);
+ };
+
+ private static class UserReceiver extends BroadcastReceiver {
+ private WeakReference<MainClusterActivity> mActivity;
+
+ UserReceiver(MainClusterActivity activity) {
+ mActivity = new WeakReference<>(activity);
+ }
+
+ public void register(Context context) {
+ IntentFilter intentFilter = new IntentFilter(ACTION_USER_UNLOCKED);
+ intentFilter.addAction(ACTION_USER_SWITCHED);
+ context.registerReceiver(this, intentFilter);
+ }
+
+ public void unregister(Context context) {
+ context.unregisterReceiver(this);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ MainClusterActivity activity = mActivity.get();
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Broadcast received: " + intent);
+ }
+ activity.tryLaunchNavigationActivity();
+ }
+ }
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -221,17 +287,29 @@
mNavStateController = new NavStateController(findViewById(R.id.navigation_state));
mClusterViewModel = ViewModelProviders.of(this).get(ClusterViewModel.class);
- mClusterViewModel.getNavigationFocus().observe(this, active ->
- mNavStateController.setActive(active));
+ mClusterViewModel.getNavigationFocus().observe(this, focus -> {
+ mNavStateController.setActive(focus);
+ // If focus is lost, we launch the default navigation activity again.
+ if (!focus) {
+ tryLaunchNavigationActivity();
+ }
+ });
mCar = Car.createCar(this, mCarServiceConnection);
mCar.connect();
+
+ mActivityMonitor.start();
+
+ mUserReceiver = new UserReceiver(this);
+ mUserReceiver.register(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroy");
+ mUserReceiver.unregister(this);
+ mActivityMonitor.stop();
mCar.disconnect();
mCarAppFocusManager = null;
if (mService != null) {
@@ -259,6 +337,10 @@
}
public void updateNavDisplay(VirtualDisplay virtualDisplay) {
+ // Starting the default navigation activity. This activity will be shown when navigation
+ // focus is not taken.
+ startNavigationActivity(virtualDisplay.mDisplayId);
+ // Notify the service (so it updates display properties on car service)
if (mService == null) {
// Service is not bound yet. Hold the information and notify when the service is bound.
mPendingVirtualDisplay = virtualDisplay;
@@ -274,7 +356,7 @@
data.putInt(MSG_KEY_ACTIVITY_DISPLAY_ID, virtualDisplay.mDisplayId);
data.putBundle(MSG_KEY_ACTIVITY_STATE, ClusterActivityState
.create(virtualDisplay.mDisplayId != Display.INVALID_DISPLAY,
- virtualDisplay.mBounds)
+ virtualDisplay.mUnobscuredBounds)
.toBundle());
sendServiceMessage(MSG_SET_ACTIVITY_LAUNCH_OPTIONS, data, null);
}
@@ -350,4 +432,98 @@
return mFragment;
}
}
+
+ private void startNavigationActivity(int displayId) {
+ mActivityMonitor.removeListener(mNavigationDisplayId, mNavigationActivityMonitor);
+ mActivityMonitor.addListener(displayId, mNavigationActivityMonitor);
+ mNavigationDisplayId = displayId;
+ tryLaunchNavigationActivity();
+ }
+
+ /**
+ * Tries to start a default navigation activity in the cluster. During system initialization
+ * launching user activities might fail due the system not being ready or {@link PackageManager}
+ * not being able to resolve the implicit intent. It is also possible that the system doesn't
+ * have a default navigation activity selected yet.
+ */
+ private void tryLaunchNavigationActivity() {
+ int userHandle = ActivityManager.getCurrentUser();
+ if (userHandle == UserHandle.USER_SYSTEM || mNavigationDisplayId == NO_DISPLAY) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, String.format("Launch activity ignored (user: %d, display: %d)",
+ userHandle, mNavigationDisplayId));
+ }
+ // Not ready to launch yet.
+ return;
+ }
+ mHandler.removeCallbacks(mRetryLaunchNavigationActivity);
+
+ ComponentName navigationActivity = getNavigationActivity();
+ mClusterViewModel.setFreeNavigationActivity(navigationActivity);
+
+ try {
+ if (navigationActivity == null) {
+ throw new ActivityNotFoundException();
+ }
+ Intent intent = new Intent(Intent.ACTION_MAIN).addCategory(CATEGORY_NAVIGATION)
+ .setPackage(navigationActivity.getPackageName())
+ .setComponent(navigationActivity)
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ Log.d(TAG, "Launching: " + intent + " on display: " + mNavigationDisplayId);
+ Bundle activityOptions = ActivityOptions.makeBasic()
+ .setLaunchDisplayId(mNavigationDisplayId)
+ .toBundle();
+
+ startActivityAsUser(intent, activityOptions, UserHandle.CURRENT);
+ } catch (ActivityNotFoundException ex) {
+ // Some activities might not be available right on startup. We will retry.
+ mHandler.postDelayed(mRetryLaunchNavigationActivity,
+ NAVIGATION_ACTIVITY_RETRY_INTERVAL_MS);
+ } catch (Exception ex) {
+ Log.e(TAG, "Unable to start navigation activity: " + navigationActivity, ex);
+ }
+ }
+
+ /**
+ * Returns a default navigation activity to show in the cluster.
+ * In the current implementation we search for an activity with the
+ * {@link CarInstrumentClusterManager#CATEGORY_NAVIGATION} category from the same navigation app
+ * selected from CarLauncher (see CarLauncher#getMapsIntent()).
+ * Alternatively, other implementations could:
+ * <ul>
+ * <li>Read this package from a resource (having a OEM default activity to show)
+ * <li>Let the user select one from settings.
+ * </ul>
+ */
+ private ComponentName getNavigationActivity() {
+ PackageManager pm = getPackageManager();
+ int userId = ActivityManager.getCurrentUser();
+
+ // Get currently selected navigation app.
+ Intent intent = Intent.makeMainSelectorActivity(Intent.ACTION_MAIN,
+ Intent.CATEGORY_APP_MAPS);
+ ResolveInfo navigationApp = pm.resolveActivityAsUser(intent,
+ PackageManager.MATCH_DEFAULT_ONLY, userId);
+
+ // Get all possible cluster activities
+ intent = new Intent(Intent.ACTION_MAIN).addCategory(CATEGORY_NAVIGATION);
+ List<ResolveInfo> candidates = pm.queryIntentActivitiesAsUser(intent, 0, userId);
+
+ // If there is a select navigation app, try finding a matching auxiliary navigation activity
+ if (navigationApp != null) {
+ for (ResolveInfo candidate : candidates) {
+ if (candidate.activityInfo.packageName.equals(navigationApp.activityInfo
+ .packageName)) {
+ Log.d(TAG, "Found activity: " + candidate);
+ return new ComponentName(candidate.activityInfo.packageName,
+ candidate.activityInfo.name);
+ }
+ }
+ }
+
+ // During initialization implicit intents might not provided a result. We will just
+ // retry until we find one, or we exhaust the retries.
+ Log.d(TAG, "No default activity found (it might not be available yet).");
+ return null;
+ }
}
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavigationFragment.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavigationFragment.java
index 407e13a..d785c69 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavigationFragment.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavigationFragment.java
@@ -138,8 +138,13 @@
Log.i(TAG, "surfaceChanged, holder: " + holder + ", size:" + width + "x" + height
+ ", format:" + format);
- //Create dummy unobscured area to report to navigation activity.
- mUnobscuredBounds = new Rect(40, 0, width - 80, height - 40);
+ // Create dummy unobscured area to report to navigation activity.
+ int obscuredWidth = (int) getResources()
+ .getDimension(R.dimen.speedometer_overlap_width);
+ int obscuredHeight = (int) getResources()
+ .getDimension(R.dimen.navigation_gradient_height);
+ mUnobscuredBounds = new Rect(obscuredWidth, obscuredHeight, width - obscuredWidth,
+ height - obscuredHeight);
if (mVirtualDisplay == null) {
mVirtualDisplay = createVirtualDisplay(holder.getSurface(), width, height);
@@ -159,10 +164,14 @@
mProgressBar = root.findViewById(R.id.progress_bar);
mMessage = root.findViewById(R.id.message);
- mViewModel.getFreeNavigationActivity().observe(this, app -> {
- mProgressBar.setVisibility(app.mComponent != null && !app.mIsVisible ? View.VISIBLE
- : View.GONE);
- mMessage.setVisibility(app.mComponent == null ? View.VISIBLE : View.GONE);
+ mViewModel.getNavigationActivityState().observe(this, state -> {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "State: " + state);
+ }
+ mProgressBar.setVisibility(state == ClusterViewModel.NavigationActivityState.LOADING
+ ? View.VISIBLE : View.INVISIBLE);
+ mMessage.setVisibility(state == ClusterViewModel.NavigationActivityState.NOT_SELECTED
+ ? View.VISIBLE : View.INVISIBLE);
});
return root;
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NetworkedVirtualDisplay.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NetworkedVirtualDisplay.java
index f5f535e..871d93f 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NetworkedVirtualDisplay.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NetworkedVirtualDisplay.java
@@ -38,6 +38,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.io.RandomAccessFile;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.ByteBuffer;
@@ -46,10 +47,10 @@
/**
* This class encapsulates all work related to managing networked virtual display.
* <p>
- * It opens server socket and listens on port {@code PORT} for incoming connections. Once connection
- * is established it creates virtual display and media encoder and starts streaming video to that
- * socket. If the receiving part is disconnected, it will keep port open and virtual display won't
- * be destroyed.
+ * It opens a socket and listens on port {@code PORT} for connections, or the emulator pipe. Once
+ * connection is established it creates virtual display and media encoder and starts streaming video
+ * to that socket. If the receiving part is disconnected, it will keep port open and virtual
+ * display won't be destroyed.
*/
public class NetworkedVirtualDisplay {
private static final String TAG = "Cluster." + NetworkedVirtualDisplay.class.getSimpleName();
@@ -61,21 +62,29 @@
private final int mHeight;
private final int mDpi;
- private static final int PORT = 5151;
private static final int FPS = 25;
private static final int BITRATE = 6144000;
private static final String MEDIA_FORMAT_MIMETYPE = MediaFormat.MIMETYPE_VIDEO_AVC;
- private static final int MSG_START = 0;
- private static final int MSG_STOP = 1;
- private static final int MSG_SEND_FRAME = 2;
+ public static final int MSG_START = 0;
+ public static final int MSG_STOP = 1;
+ public static final int MSG_SEND_FRAME = 2;
+
+ private static final String PIPE_NAME = "pipe:qemud:carCluster";
+ private static final String PIPE_DEVICE = "/dev/qemu_pipe";
+
+ // Constants shared with emulator in car-cluster-widget.cpp
+ public static final int PIPE_START = 1;
+ public static final int PIPE_STOP = 2;
+
+ private static final int PORT = 5151;
+
+ private SenderThread mActiveThread;
+ private HandlerThread mBroadcastThread = new HandlerThread("BroadcastThread");
private VirtualDisplay mVirtualDisplay;
private MediaCodec mVideoEncoder;
- private HandlerThread mThread = new HandlerThread("NetworkThread");
private Handler mHandler;
- private ServerSocket mServerSocket;
- private OutputStream mOutputStream;
private byte[] mBuffer = null;
private int mLastFrameLength = 0;
@@ -120,31 +129,31 @@
* @throws IllegalStateException thrown if networked display already started
*/
public String start() {
- if (mThread.isAlive()) {
+ if (mBroadcastThread.isAlive()) {
throw new IllegalStateException("Already started");
}
- mThread.start();
- mHandler = new NetworkThreadHandler(mThread.getLooper());
- mHandler.sendMessage(Message.obtain(mHandler, MSG_START));
+ mBroadcastThread.start();
+ mHandler = new BroadcastThreadHandler(mBroadcastThread.getLooper());
+ mHandler.sendMessage(Message.obtain(mHandler, MSG_START));
return getDisplayName();
}
public void release() {
- stopCasting();
+ mHandler.sendMessage(Message.obtain(mHandler, MSG_STOP));
+ mBroadcastThread.quitSafely();
if (mVirtualDisplay != null) {
+ mVirtualDisplay.setSurface(null);
mVirtualDisplay.release();
mVirtualDisplay = null;
}
- mThread.quit();
}
private String getDisplayName() {
return "Cluster-" + mUniqueId;
}
-
private VirtualDisplay createVirtualDisplay() {
Log.i(TAG, "createVirtualDisplay " + mWidth + "x" + mHeight +"@" + mDpi);
return mDisplayManager.createVirtualDisplay(getDisplayName(), mWidth, mHeight, mDpi,
@@ -157,14 +166,18 @@
private void startCasting(Handler handler) {
Log.i(TAG, "Start casting...");
+ if (mVideoEncoder != null) {
+ Log.i(TAG, "Already started casting");
+ return;
+ }
mVideoEncoder = createVideoStream(handler);
if (mVirtualDisplay == null) {
mVirtualDisplay = createVirtualDisplay();
}
+
mVirtualDisplay.setSurface(mVideoEncoder.createInputSurface());
mVideoEncoder.start();
-
Log.i(TAG, "Video encoder started");
}
@@ -194,6 +207,8 @@
public void onError(@NonNull MediaCodec codec, @NonNull CodecException e) {
Log.e(TAG, "onError, codec: " + codec, e);
mCounter.bufferErrors++;
+ stopCasting();
+ startCasting(handler);
}
@Override
@@ -242,43 +257,28 @@
}
private void sendFrame(byte[] buf, int len) {
- try {
- if (mOutputStream != null) {
- mOutputStream.write(buf, 0, len);
- }
- } catch (IOException e) {
- mCounter.clientsDisconnected++;
- mOutputStream = null;
- Log.e(TAG, "Failed to write data to socket, restart casting", e);
- restart();
+ if (mActiveThread != null) {
+ mActiveThread.send(buf, len);
}
}
private void stopCasting() {
Log.i(TAG, "Stopping casting...");
- if (mServerSocket != null) {
- try {
- mServerSocket.close();
- } catch (IOException e) {
- Log.w(TAG, "Failed to close server socket, ignoring", e);
- }
- mServerSocket = null;
- }
if (mVirtualDisplay != null) {
- // We do not want to destroy virtual display (as it will also destroy all the
- // activities on that display, instead we will turn off the display by setting
- // a null surface.
Surface surface = mVirtualDisplay.getSurface();
if (surface != null) surface.release();
- mVirtualDisplay.setSurface(null);
}
if (mVideoEncoder != null) {
// Releasing encoder as stop/start didn't work well (couldn't create or reuse input
// surface).
- mVideoEncoder.stop();
- mVideoEncoder.release();
+ try {
+ mVideoEncoder.stop();
+ mVideoEncoder.release();
+ } catch (IllegalStateException e) {
+ // do nothing, already released
+ }
mVideoEncoder = null;
}
Log.i(TAG, "Casting stopped");
@@ -287,14 +287,17 @@
private synchronized void restart() {
// This method could be called from different threads when receiver has disconnected.
if (mHandler.hasMessages(MSG_START)) return;
-
mHandler.sendMessage(Message.obtain(mHandler, MSG_STOP));
mHandler.sendMessage(Message.obtain(mHandler, MSG_START));
}
- private class NetworkThreadHandler extends Handler {
+ private class BroadcastThreadHandler extends Handler {
+ private static final int MAX_FAIL_COUNT = 10;
+ private int mFailConnectCounter;
+ private SocketThread mSocketThread;
+ private PipeThread mPipeThread;
- NetworkThreadHandler(Looper looper) {
+ BroadcastThreadHandler(Looper looper) {
super(looper);
}
@@ -302,34 +305,102 @@
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_START:
- if (mServerSocket == null) {
- mServerSocket = openServerSocket();
+ Log.i(TAG, "Received start message");
+ // Failure to connect to either pipe or network returns null
+ if (mActiveThread == null) {
+ mActiveThread = tryPipeConnect();
}
- Log.i(TAG, "Server socket opened");
-
- mOutputStream = waitForReceiver(mServerSocket);
- if (mOutputStream == null) {
- sendMessage(Message.obtain(this, MSG_START));
+ if (mActiveThread == null) {
+ mActiveThread = tryNetworkConnect();
+ }
+ if (mActiveThread == null) {
+ // When failed attempt limit is reached, clean up and quit this thread.
+ mFailConnectCounter++;
+ if (mFailConnectCounter >= MAX_FAIL_COUNT) {
+ Log.e(TAG, "Too many failed connection attempts; aborting");
+ release();
+ throw new RuntimeException("Abort after failed connection attempts");
+ }
+ mHandler.sendMessage(Message.obtain(mHandler, MSG_START));
break;
}
+ mFailConnectCounter = 0;
mCounter.clientsConnected++;
-
+ mActiveThread.start();
startCasting(this);
break;
case MSG_STOP:
+ Log.i(TAG, "Received stop message");
stopCasting();
+ mCounter.clientsDisconnected++;
+ if (mActiveThread != null) {
+ mActiveThread.close();
+ try {
+ mActiveThread.join();
+ } catch (InterruptedException e) {
+ Log.e(TAG, "Waiting for active thread to close failed", e);
+ }
+ mActiveThread = null;
+ }
break;
case MSG_SEND_FRAME:
- if (mServerSocket != null && mOutputStream != null) {
- sendFrame(mBuffer, mLastFrameLength);
+ if (mActiveThread == null) {
+ // Stop the chaining signal if there's no client to send to
+ break;
}
+ sendFrame(mBuffer, mLastFrameLength);
// We will keep sending last frame every second as a heartbeat.
sendFrameAsync(1000L);
break;
}
}
+
+ // Returns null if can't establish pipe connection
+ // Otherwise returns the corresponding client thread
+ private PipeThread tryPipeConnect() {
+ try {
+ RandomAccessFile pipe = new RandomAccessFile(PIPE_DEVICE, "rw");
+ byte[] temp = new byte[PIPE_NAME.length() + 1];
+ temp[PIPE_NAME.length()] = 0;
+ System.arraycopy(PIPE_NAME.getBytes(), 0, temp, 0, PIPE_NAME.length());
+ pipe.write(temp);
+
+ // At this point, the pipe exists, so we will just wait for a start signal
+ // This is in case pipe still sends leftover stops from last instantiation
+ int signal = pipe.read();
+ while (signal != PIPE_START) {
+ Log.i(TAG, "Received non-start signal: " + signal);
+ signal = pipe.read();
+ }
+ return new PipeThread(mHandler, pipe);
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to establish pipe connection", e);
+ return null;
+ }
+ }
+
+ // Returns null if can't establish network connection
+ // Otherwise returns the corresponding client thread
+ private SocketThread tryNetworkConnect() {
+ try {
+ ServerSocket serverSocket = new ServerSocket(PORT);
+ Log.i(TAG, "Server socket opened");
+ Socket socket = serverSocket.accept();
+ socket.setTcpNoDelay(true);
+ socket.setKeepAlive(true);
+ socket.setSoLinger(true, 0);
+
+ InputStream inputStream = socket.getInputStream();
+ OutputStream outputStream = socket.getOutputStream();
+
+ return new SocketThread(mHandler, serverSocket, inputStream, outputStream);
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to establish network connection", e);
+ return null;
+ }
+ }
}
private static void configureVideoEncoder(MediaCodec codec, int width, int height) {
@@ -349,51 +420,12 @@
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
}
- private OutputStream waitForReceiver(ServerSocket serverSocket) {
- try {
- Log.i(TAG, "Listening for incoming connections on port: " + PORT);
- Socket socket = serverSocket.accept();
- socket.setTcpNoDelay(true);
- socket.setKeepAlive(true);
- socket.setSoLinger(true, 0);
-
- Log.i(TAG, "Receiver connected: " + socket);
- listenReceiverDisconnected(socket.getInputStream());
-
- return socket.getOutputStream();
- } catch (IOException e) {
- Log.e(TAG, "Failed to accept connection");
- return null;
- }
- }
-
- private void listenReceiverDisconnected(InputStream inputStream) {
- new Thread(() -> {
- try {
- if (inputStream.read() == -1) throw new IOException();
- } catch (IOException e) {
- Log.w(TAG, "Receiver has disconnected", e);
- }
- restart();
- }).start();
- }
-
- private static ServerSocket openServerSocket() {
- try {
- return new ServerSocket(PORT);
- } catch (IOException e) {
- Log.e(TAG, "Failed to create server socket", e);
- throw new RuntimeException(e);
- }
- }
-
@Override
public String toString() {
return getClass() + "{"
- + mServerSocket
- +", receiver connected: " + (mOutputStream != null)
- +", encoder: " + mVideoEncoder
- +", virtualDisplay" + mVirtualDisplay
+ + ", receiver connected: " + (mActiveThread != null)
+ + ", encoder: " + mVideoEncoder
+ + ", virtualDisplay" + mVirtualDisplay
+ "}";
}
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/PipeThread.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/PipeThread.java
new file mode 100644
index 0000000..c96f482
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/PipeThread.java
@@ -0,0 +1,81 @@
+/*
+ * 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 android.car.cluster.sample;
+import android.os.Handler;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * Thread that can send data to the emulator using a qemud service.
+ */
+public class PipeThread extends SenderThread {
+ private static final String TAG = "Cluster." + PipeThread.class.getSimpleName();
+
+ private RandomAccessFile mPipe;
+
+ /**
+ * Creates instance of pipe thread that can write to given pipe file.
+ *
+ * @param handler {@link Handler} used to message broadcaster.
+ * @param pipe {@link RandomAccessFile} file already connected to pipe.
+ */
+ PipeThread(Handler handler, RandomAccessFile pipe) {
+ super(handler);
+ mPipe = pipe;
+ }
+
+ public void run() {
+ try {
+ int signal = mPipe.read();
+ while (signal != NetworkedVirtualDisplay.PIPE_STOP) {
+ Log.i(TAG, "Received non-stop signal: " + signal);
+ signal = mPipe.read();
+ }
+ restart();
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to read from pipe");
+ restart();
+ }
+ }
+
+ @Override
+ public void send(byte[] buf, int len) {
+ try {
+ // First sends the size prior to sending the data, since receiving side only sees
+ // the size of the buffer, which could be significant larger than the actual data.
+ mPipe.write(ByteBuffer.allocate(4)
+ .order(ByteOrder.LITTLE_ENDIAN).putInt(len).array());
+ mPipe.write(buf);
+ } catch (IOException e) {
+ Log.e(TAG, "Write to pipe failed");
+ restart();
+ }
+ }
+
+ @Override
+ public void close() {
+ try {
+ mPipe.close();
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to close pipe", e);
+ }
+ }
+}
+
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/SenderThread.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/SenderThread.java
new file mode 100644
index 0000000..2c7f0d6
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/SenderThread.java
@@ -0,0 +1,42 @@
+/*
+ * 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 android.car.cluster.sample;
+import android.os.Handler;
+import android.os.Message;
+
+/**
+ * This class serves as a template for sending to specific clients of the broadcaster.
+ */
+public abstract class SenderThread extends Thread {
+ private Handler mHandler;
+
+ SenderThread(Handler handler) {
+ mHandler = handler;
+ }
+
+ abstract void send(byte[] buf, int len);
+ abstract void close();
+
+ /**
+ * Tells the broadcasting thread to stop and close everything in progress, and start over again.
+ * It will kill the current instance of this thread, and produce a new one.
+ */
+ void restart() {
+ if (mHandler.hasMessages(NetworkedVirtualDisplay.MSG_START)) return;
+ mHandler.sendMessage(Message.obtain(mHandler, NetworkedVirtualDisplay.MSG_STOP));
+ mHandler.sendMessage(Message.obtain(mHandler, NetworkedVirtualDisplay.MSG_START));
+ }
+}
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/SocketThread.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/SocketThread.java
new file mode 100644
index 0000000..a5215b0
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/SocketThread.java
@@ -0,0 +1,86 @@
+/*
+ * 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 android.car.cluster.sample;
+import android.os.Handler;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.ServerSocket;
+
+/**
+ * The thread that will send data on an opened socket.
+ */
+public class SocketThread extends SenderThread {
+ private static final String TAG = "Cluster." + SocketThread.class.getSimpleName();
+ private ServerSocket mServerSocket;
+ private OutputStream mOutputStream;
+ private InputStream mInputStream;
+
+ /**
+ * Create instance of thread that can write to given open socket.
+ *
+ * @param handler {@link Handler} used to message the broadcaster.
+ * @param serverSocket {@link ServerSocket} should be already opened.
+ * @param inputStream {@link InputStream} corresponding to opened socket.
+ * @param outputStream {@link OutputStream} corresponding to opened socket.
+ */
+ SocketThread(Handler handler, ServerSocket serverSocket, InputStream inputStream,
+ OutputStream outputStream) {
+ super(handler);
+ mServerSocket = serverSocket;
+ mInputStream = inputStream;
+ mOutputStream = outputStream;
+ }
+
+ public void run() {
+ try {
+ // This read should block until something disconnects (or something
+ // similar) which should cause an exception, in which case we should
+ // try to setup again and reconnect
+ mInputStream.read();
+ } catch (IOException e) {
+ Log.e(TAG, "Socket thread disconnected.");
+ }
+ restart();
+ }
+
+ @Override
+ public void send(byte[] buf, int len) {
+ try {
+ mOutputStream.write(buf, 0, len);
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to write data to socket, retrying connection");
+ restart();
+ }
+ }
+
+ @Override
+ public void close() {
+ if (mServerSocket != null) {
+ try {
+ mServerSocket.close();
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to close server socket, ignoring");
+ }
+ mServerSocket = null;
+ }
+ mInputStream = null;
+ mOutputStream = null;
+ }
+}
+
diff --git a/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml b/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
index c400bd8..798e6b4 100644
--- a/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
+++ b/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
@@ -33,6 +33,7 @@
<uses-permission android:name="android.car.permission.CAR_CONTROL_AUDIO_VOLUME"/>
<uses-permission android:name="android.car.permission.VEHICLE_DYNAMICS_STATE"/>
<uses-permission android:name="android.car.permission.CAR_DISPLAY_IN_CLUSTER"/>
+ <uses-permission android:name="android.car.permission.CAR_INSTRUMENT_CLUSTER_CONTROL"/>
<uses-permission android:name="android.car.permission.STORAGE_MONITORING" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.MANAGE_USB" />
@@ -97,10 +98,9 @@
android:launchMode="singleInstance"
android:resizeableActivity="true"
android:allowEmbedded="true"
- android:permission="android.car.permission.CAR_INSTRUMENT_CLUSTER_CONTROL">
+ android:permission="android.car.permission.CAR_DISPLAY_IN_CLUSTER">
<intent-filter android:priority="-1">
<action android:name="android.intent.action.MAIN"/>
- <category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.car.cluster.NAVIGATION"/>
</intent-filter>
</activity>
@@ -108,7 +108,7 @@
<activity android:name=".activityview.ActivityViewTestFragment"/>
<!-- temporary solution until b/68882625 is fixed. -->
- <receiver android:name=".touchsound.DisableTouchSoundOnBoot" android:exported="true">
+ <receiver android:name=".touchsound.DisableTouchSoundOnBoot" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/instrument_cluster.xml b/tests/EmbeddedKitchenSinkApp/res/layout/instrument_cluster.xml
index f58af8f..d244904 100644
--- a/tests/EmbeddedKitchenSinkApp/res/layout/instrument_cluster.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/instrument_cluster.xml
@@ -13,40 +13,47 @@
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"
- android:layout_marginTop="40dp"
- android:layout_marginStart="40dp">
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center"
+ android:layout_marginTop="40dp"
+ android:layout_marginStart="40dp"
+ android:layout_marginEnd="40dp">
<LinearLayout
- android:orientation="vertical"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
+ android:orientation="vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
<Button
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="1"
- android:text="@string/cluster_start"
- android:id="@+id/cluster_start_button"/>
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="10dp"
+ android:padding="20dp"
+ android:text="@string/cluster_start"
+ android:id="@+id/cluster_start_button"/>
<Button
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="1"
- android:text="@string/cluster_turn_left"
- android:id="@+id/cluster_turn_left_button"/>
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="10dp"
+ android:padding="20dp"
+ android:text="@string/cluster_start_guidance"
+ android:id="@+id/cluster_turn_left_button"/>
<Button
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="1"
- android:text="@string/cluster_stop"
- android:id="@+id/cluster_stop_button"/>
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="10dp"
+ android:padding="20dp"
+ android:text="@string/cluster_stop"
+ android:id="@+id/cluster_stop_button"/>
<Button
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="1"
- android:text="@string/cluster_start_activity"
- android:id="@+id/cluster_start_activity"/>
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="10dp"
+ android:padding="20dp"
+ android:text="@string/cluster_start_activity"
+ android:id="@+id/cluster_start_activity"/>
</LinearLayout>
</LinearLayout>
diff --git a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
index 35d3ec2..15f2afc 100644
--- a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
@@ -126,11 +126,14 @@
<string name="open_kb_button">Hide/Show Input</string>
<!-- instrument cluster -->
- <string name="cluster_start">Start metadata</string>
- <string name="cluster_turn_left">Send turn-by-turn</string>
- <string name="cluster_stop">Stop metadata</string>
+ <string name="cluster_start">Request focus</string>
+ <string name="cluster_start_guidance">Start turn-by-turn</string>
+ <string name="cluster_stop">Abandon focus</string>
+ <string name="cluster_stop_guidance">Stop turn-by-turn</string>
<string name="cluster_nav_app_context_loss">Navigation app context lost!</string>
<string name="cluster_start_activity">Start Nav Activity</string>
+ <string name="cluster_start_activity_failed">Failed to start activity in cluster</string>
+ <string name="cluster_not_started">Missing navigation focus</string>
<!-- input test -->
<string name="volume_up">Volume +</string>
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/InstrumentClusterFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/InstrumentClusterFragment.java
index ad4b93a..7e40b5b 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/InstrumentClusterFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/InstrumentClusterFragment.java
@@ -15,7 +15,7 @@
*/
package com.google.android.car.kitchensink.cluster;
-import android.app.AlertDialog;
+import android.annotation.Nullable;
import android.car.Car;
import android.car.CarAppFocusManager;
import android.car.CarNotConnectedException;
@@ -31,14 +31,15 @@
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.Button;
import android.widget.Toast;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.car.cluster.navigation.NavigationState;
import androidx.fragment.app.Fragment;
+import com.google.android.car.kitchensink.KitchenSinkActivity;
import com.google.android.car.kitchensink.R;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
@@ -47,10 +48,8 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
-import java.util.Arrays;
import java.util.Timer;
import java.util.TimerTask;
-import java.util.stream.Collectors;
/**
* Contains functions to test instrument cluster API.
@@ -65,35 +64,61 @@
private Car mCarApi;
private Timer mTimer;
private NavigationState[] mNavStateData;
+ private Button mTurnByTurnButton;
- private final ServiceConnection mServiceConnection = new ServiceConnection() {
- @Override
- public void onServiceConnected(ComponentName name, IBinder service) {
- Log.d(TAG, "Connected to Car Service");
- try {
- mCarNavigationStatusManager =
- (CarNavigationStatusManager) mCarApi.getCarManager(
- Car.CAR_NAVIGATION_SERVICE);
- mCarAppFocusManager =
- (CarAppFocusManager) mCarApi.getCarManager(Car.APP_FOCUS_SERVICE);
- } catch (CarNotConnectedException e) {
- Log.e(TAG, "Car is not connected!", e);
- }
+ private ServiceConnection mCarServiceConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ Log.d(TAG, "Connected to Car Service");
+ try {
+ mCarNavigationStatusManager = (CarNavigationStatusManager) mCarApi
+ .getCarManager(Car.CAR_NAVIGATION_SERVICE);
+ mCarAppFocusManager = (CarAppFocusManager) mCarApi
+ .getCarManager(Car.APP_FOCUS_SERVICE);
+ } catch (CarNotConnectedException e) {
+ Log.e(TAG, "Car is not connected!", e);
}
+ }
- @Override
- public void onServiceDisconnected(ComponentName name) {
- Log.d(TAG, "Disconnect from Car Service");
- }
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ Log.d(TAG, "Disconnect from Car Service");
+ }
};
+ private final CarAppFocusManager.OnAppFocusOwnershipCallback mFocusCallback =
+ new CarAppFocusManager.OnAppFocusOwnershipCallback() {
+ @Override
+ public void onAppFocusOwnershipLost(@CarAppFocusManager.AppFocusType int appType) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onAppFocusOwnershipLost, appType: " + appType);
+ }
+ Toast.makeText(getContext(), getText(R.string.cluster_nav_app_context_loss),
+ Toast.LENGTH_LONG).show();
+ }
+
+ @Override
+ public void onAppFocusOwnershipGranted(@CarAppFocusManager.AppFocusType int appType) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onAppFocusOwnershipGranted, appType: " + appType);
+ }
+ }
+ };
+ private CarAppFocusManager.OnAppFocusChangedListener mOnAppFocusChangedListener =
+ (appType, active) -> {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onAppFocusChanged, appType: " + appType + " active: " + active);
+ }
+ };
+
+
private void initCarApi() {
if (mCarApi != null && mCarApi.isConnected()) {
mCarApi.disconnect();
mCarApi = null;
}
- mCarApi = Car.createCar(getContext(), mServiceConnection);
+ mCarApi = Car.createCar(getContext(), mCarServiceConnection);
mCarApi.connect();
}
@@ -122,7 +147,7 @@
InputStream inputStream = getResources().openRawResource(resId);
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
StringBuilder builder = new StringBuilder();
- for (String line = null; (line = reader.readLine()) != null; ) {
+ for (String line; (line = reader.readLine()) != null; ) {
builder.append(line).append("\n");
}
return builder.toString();
@@ -135,9 +160,12 @@
View view = inflater.inflate(R.layout.instrument_cluster, container, false);
view.findViewById(R.id.cluster_start_button).setOnClickListener(v -> initCluster());
- view.findViewById(R.id.cluster_turn_left_button).setOnClickListener(v -> toogleSendTurn());
+ view.findViewById(R.id.cluster_stop_button).setOnClickListener(v -> stopCluster());
view.findViewById(R.id.cluster_start_activity).setOnClickListener(v -> startNavActivity());
+ mTurnByTurnButton = view.findViewById(R.id.cluster_turn_left_button);
+ mTurnByTurnButton.setOnClickListener(v -> toggleSendTurn());
+
return view;
}
@@ -159,14 +187,15 @@
return;
}
- // Implicit intent
+ // Implicit intent ("startActivity" method doesn't work with explicit intents)
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(CarInstrumentClusterManager.CATEGORY_NAVIGATION);
+ intent.setPackage(KitchenSinkActivity.class.getPackage().getName());
try {
clusterManager.startActivity(intent);
} catch (android.car.CarNotConnectedException e) {
Log.e(TAG, "Failed to startActivity in cluster", e);
- Toast.makeText(getContext(), "Failed to start activity in cluster",
+ Toast.makeText(getContext(), getText(R.string.cluster_start_activity_failed),
Toast.LENGTH_LONG).show();
return;
}
@@ -175,32 +204,48 @@
/**
* Enables/disables sending turn-by-turn data through the {@link CarNavigationStatusManager}
*/
- private void toogleSendTurn() {
+ private void toggleSendTurn() {
// If we haven't yet load the sample navigation state data, do so.
if (mNavStateData == null) {
mNavStateData = getNavStateData();
- Log.i(TAG, "Loaded: " + Arrays.asList(mNavStateData)
- .stream()
- .map(n -> n.toString())
- .collect(Collectors.joining(", ")));
}
// Toggle a timer to send update periodically.
if (mTimer == null) {
- mTimer = new Timer();
- mTimer.schedule(new TimerTask() {
- private int mPos;
-
- @Override
- public void run() {
- sendTurn(mNavStateData[mPos]);
- mPos = (mPos + 1) % mNavStateData.length;
- }
- }, 0, 1000);
+ startSendTurn();
} else {
+ stopSendTurn();
+ }
+ }
+
+ private void startSendTurn() {
+ if (mTimer != null) {
+ stopSendTurn();
+ }
+ if (!hasFocus()) {
+ Toast.makeText(getContext(), getText(R.string.cluster_not_started), Toast.LENGTH_LONG)
+ .show();
+ return;
+ }
+ mTimer = new Timer();
+ mTimer.schedule(new TimerTask() {
+ private int mPos;
+
+ @Override
+ public void run() {
+ sendTurn(mNavStateData[mPos]);
+ mPos = (mPos + 1) % mNavStateData.length;
+ }
+ }, 0, 1000);
+ mTurnByTurnButton.setText(R.string.cluster_stop_guidance);
+ }
+
+ private void stopSendTurn() {
+ if (mTimer != null) {
mTimer.cancel();
mTimer = null;
}
+ mTurnByTurnButton.setText(R.string.cluster_start_guidance);
}
/**
@@ -218,55 +263,44 @@
}
private void initCluster() {
- try {
- mCarAppFocusManager
- .addFocusListener(new CarAppFocusManager.OnAppFocusChangedListener() {
- @Override
- public void onAppFocusChanged(int appType, boolean active) {
- Log.d(TAG, "onAppFocusChanged, appType: " + appType + " active: "
- + active);
- }
- }, CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
- } catch (CarNotConnectedException e) {
- Log.e(TAG, "Failed to register focus listener", e);
+ if (hasFocus()) {
+ return;
}
-
- CarAppFocusManager.OnAppFocusOwnershipCallback
- focusCallback = new CarAppFocusManager.OnAppFocusOwnershipCallback() {
- @Override
- public void onAppFocusOwnershipLost(int focus) {
- Log.w(TAG, "onAppFocusOwnershipLost, focus: " + focus);
- new AlertDialog.Builder(getContext())
- .setTitle(getContext().getApplicationInfo().name)
- .setMessage(R.string.cluster_nav_app_context_loss)
- .show();
- }
-
- @Override
- public void onAppFocusOwnershipGranted(int focus) {
- Log.w(TAG, "onAppFocusOwnershipGranted, focus: " + focus);
- }
-
- };
try {
+ mCarAppFocusManager.addFocusListener(mOnAppFocusChangedListener,
+ CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
mCarAppFocusManager.requestAppFocus(CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION,
- focusCallback);
- } catch (CarNotConnectedException e) {
- Log.e(TAG, "Failed to set active focus", e);
- }
-
- try {
- boolean ownsFocus = mCarAppFocusManager.isOwningFocus(
- focusCallback, CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
- Log.d(TAG, "Owns APP_FOCUS_TYPE_NAVIGATION: " + ownsFocus);
- if (!ownsFocus) {
+ mFocusCallback);
+ if (!hasFocus()) {
throw new RuntimeException("Focus was not acquired.");
}
} catch (CarNotConnectedException e) {
- Log.e(TAG, "Failed to get owned focus", e);
+ Log.e(TAG, "Failed to set active focus", e);
}
}
+ private boolean hasFocus() {
+ try {
+ boolean ownsFocus = mCarAppFocusManager.isOwningFocus(mFocusCallback,
+ CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Owns APP_FOCUS_TYPE_NAVIGATION: " + ownsFocus);
+ }
+ return ownsFocus;
+ } catch (CarNotConnectedException e) {
+ Log.e(TAG, "Failed to get owned focus", e);
+ return false;
+ }
+ }
+
+ private void stopCluster() {
+ stopSendTurn();
+ mCarAppFocusManager.removeFocusListener(mOnAppFocusChangedListener,
+ CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
+ mCarAppFocusManager.abandonAppFocus(mFocusCallback,
+ CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
+ }
+
@Override
public void onResume() {
super.onResume();