Merge "Add message time and read status to MAP MCE test page output" into qt-dev
diff --git a/EncryptionRunner/src/android/car/encryptionrunner/Ukey2EncryptionRunner.java b/EncryptionRunner/src/android/car/encryptionrunner/Ukey2EncryptionRunner.java
index 745d0b3..eed7852 100644
--- a/EncryptionRunner/src/android/car/encryptionrunner/Ukey2EncryptionRunner.java
+++ b/EncryptionRunner/src/android/car/encryptionrunner/Ukey2EncryptionRunner.java
@@ -17,9 +17,10 @@
package android.car.encryptionrunner;
import android.annotation.NonNull;
-import android.util.Base64;
import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
+
import com.google.security.cryptauth.lib.securegcm.D2DConnectionContext;
import com.google.security.cryptauth.lib.securegcm.Ukey2Handshake;
@@ -33,8 +34,7 @@
private static final Ukey2Handshake.HandshakeCipher CIPHER =
Ukey2Handshake.HandshakeCipher.P256_SHA512;
- // This is from Ukey2HandshakeTest
- private static final int MAX_AUTH_STRING_LENGTH = 32;
+ private static final int AUTH_STRING_LENGTH = 6;
private Ukey2Handshake mUkey2client;
private boolean mRunnerIsInvalid;
@@ -104,9 +104,8 @@
String verificationCode = null;
if (mUkey2client.getHandshakeState() == Ukey2Handshake.State.VERIFICATION_NEEDED) {
- verificationCode = Base64.encodeToString(
- mUkey2client.getVerificationString(MAX_AUTH_STRING_LENGTH),
- Base64.DEFAULT);
+ verificationCode = generateReadablePairingCode(
+ mUkey2client.getVerificationString(AUTH_STRING_LENGTH));
}
return HandshakeMessage.newBuilder()
.setHandshakeState(getHandshakeState())
@@ -119,6 +118,24 @@
}
}
+ /**
+ * Returns a human-readable pairing code string generated from the verification bytes. Converts
+ * each byte into a digit with a simple modulo.
+ *
+ * <p>This should match the implementation in the iOS and Android client libraries.
+ */
+ @VisibleForTesting
+ String generateReadablePairingCode(byte[] verificationCode) {
+ StringBuilder outString = new StringBuilder();
+ for (byte b : verificationCode) {
+ int unsignedInt = Byte.toUnsignedInt(b);
+ int digit = unsignedInt % 10;
+ outString.append(digit);
+ }
+
+ return outString.toString();
+ }
+
private static class UKey2Key implements Key {
private final D2DConnectionContext mConnectionContext;
diff --git a/EncryptionRunner/test/android/car/encryptionrunner/Ukey2EncryptionRunnerTest.java b/EncryptionRunner/test/android/car/encryptionrunner/Ukey2EncryptionRunnerTest.java
new file mode 100644
index 0000000..14ca77d
--- /dev/null
+++ b/EncryptionRunner/test/android/car/encryptionrunner/Ukey2EncryptionRunnerTest.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2019 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.encryptionrunner;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class Ukey2EncryptionRunnerTest {
+
+ private Ukey2EncryptionRunner mRunner;
+
+ @Before
+ public void setup() {
+ mRunner = new Ukey2EncryptionRunner();
+ }
+
+ @Test
+ public void generateReadablePairingCode_modsBytesAcrossRange() throws Exception {
+ // 194 is an example of a value that would fail if using signed instead of unsigned ints
+ // 194 -> 11000010
+ // 11000010 -> 194 (unsigned 8-bit int)
+ // 11000010 -> -62 (signed 8-bit int)
+ byte[] bytes = new byte[]{0, 7, (byte) 161, (byte) 194, (byte) 196, (byte) 255};
+ String pairingCode = mRunner.generateReadablePairingCode(bytes);
+
+ assertThat(pairingCode).isEqualTo("071465");
+ }
+}
diff --git a/car-lib/src/android/car/VehiclePropertyIds.java b/car-lib/src/android/car/VehiclePropertyIds.java
index cfc6e34..14642fe 100644
--- a/car-lib/src/android/car/VehiclePropertyIds.java
+++ b/car-lib/src/android/car/VehiclePropertyIds.java
@@ -295,6 +295,11 @@
*/
public static final int EV_BATTERY_DISPLAY_UNITS = 289408515;
/**
+ * Speed Units for display
+ * @hide
+ */
+ public static final int VEHICLE_SPEED_DISPLAY_UNITS = 289408516;
+ /**
* Fuel consumption units for display
*/
public static final int FUEL_CONSUMPTION_UNITS_DISTANCE_OVER_VOLUME = 287311364;
@@ -972,6 +977,9 @@
if (o == READING_LIGHTS_SWITCH) {
return "READING_LIGHTS_SWITCH";
}
+ if (o == VEHICLE_SPEED_DISPLAY_UNITS) {
+ return "VEHICLE_SPEED_DISPLAY_UNITS";
+ }
return "0x" + Integer.toHexString(o);
}
}
diff --git a/car-lib/src/android/car/VehicleUnit.java b/car-lib/src/android/car/VehicleUnit.java
index 4fb099a..0c09e87 100644
--- a/car-lib/src/android/car/VehicleUnit.java
+++ b/car-lib/src/android/car/VehicleUnit.java
@@ -72,6 +72,12 @@
public static final int DEGREES = 0x80;
/** @hide */
+ public static final int MILES_PER_HOUR = 0x90;
+
+ /** @hide */
+ public static final int KILOMETERS_PER_HOUR = 0x91;
+
+ /** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef({
SHOULD_NOT_USE,
@@ -102,7 +108,9 @@
KILOWATT_HOUR,
PSI,
BAR,
- DEGREES
+ DEGREES,
+ MILES_PER_HOUR,
+ KILOMETERS_PER_HOUR
})
public @interface Enum {}
diff --git a/car-lib/src/android/car/hardware/property/CarPropertyEvent.java b/car-lib/src/android/car/hardware/property/CarPropertyEvent.java
index 638fbf2..260d5fa 100644
--- a/car-lib/src/android/car/hardware/property/CarPropertyEvent.java
+++ b/car-lib/src/android/car/hardware/property/CarPropertyEvent.java
@@ -16,6 +16,7 @@
package android.car.hardware.property;
+import android.annotation.NonNull;
import android.car.hardware.CarPropertyValue;
import android.os.Parcel;
import android.os.Parcelable;
@@ -68,7 +69,7 @@
/**
* Constructor for {@link CarPropertyEvent}.
*/
- public CarPropertyEvent(int eventType, CarPropertyValue<?> carPropertyValue) {
+ public CarPropertyEvent(@NonNull int eventType, @NonNull CarPropertyValue<?> carPropertyValue) {
mEventType = eventType;
mCarPropertyValue = carPropertyValue;
}
diff --git a/car-lib/src/android/car/hardware/property/CarPropertyManager.java b/car-lib/src/android/car/hardware/property/CarPropertyManager.java
index 6317c66..85ffd34 100644
--- a/car-lib/src/android/car/hardware/property/CarPropertyManager.java
+++ b/car-lib/src/android/car/hardware/property/CarPropertyManager.java
@@ -211,7 +211,7 @@
return true;
}
- private class CarPropertyEventListenerToService extends ICarPropertyEventListener.Stub{
+ private static class CarPropertyEventListenerToService extends ICarPropertyEventListener.Stub{
private final WeakReference<CarPropertyManager> mMgr;
CarPropertyEventListenerToService(CarPropertyManager mgr) {
diff --git a/car-lib/src/com/android/car/internal/CarRatedFloatListeners.java b/car-lib/src/com/android/car/internal/CarRatedFloatListeners.java
index 60b6a62..93519d6 100644
--- a/car-lib/src/com/android/car/internal/CarRatedFloatListeners.java
+++ b/car-lib/src/com/android/car/internal/CarRatedFloatListeners.java
@@ -29,6 +29,7 @@
* @hide
*/
public class CarRatedFloatListeners<T> {
+ private static final float NANOSECOND_PER_SECOND = 1000 * 1000 * 1000;
private final Map<T, Float> mListenersToRate = new HashMap<>(4);
private final Map<T, Long> mListenersUpdateTime = new HashMap<>(4);
@@ -112,7 +113,7 @@
return true;
}
if (nextUpdateTime <= eventTimeStamp) {
- Float cycle = 1000 / updateRate;
+ Float cycle = NANOSECOND_PER_SECOND / updateRate;
nextUpdateTime = eventTimeStamp + cycle.longValue();
mListenersUpdateTime.put(listener, nextUpdateTime);
return true;
diff --git a/car_product/overlay/frameworks/base/core/res/res/values/dimens.xml b/car_product/overlay/frameworks/base/core/res/res/values/dimens.xml
index aed2a41..c005792 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values/dimens.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values/dimens.xml
@@ -23,7 +23,11 @@
<dimen name="car_qs_header_system_icons_area_height">76dp</dimen>
<dimen name="navigation_bar_height">112dp</dimen>
<dimen name="navigation_bar_height_landscape">112dp</dimen>
- <dimen name="status_bar_icon_size">36dp</dimen>
+
+ <!-- Notification icon size in the status bar. -->
+ <dimen name="status_bar_icon_size">32dp</dimen>
+ <!-- System status icon size in the status bar. -->
+ <dimen name="status_bar_system_icon_size">32dp</dimen>
<!-- The size of the right icon. This size is used to down-sample the bitmaps for "large icon". -->
<dimen name="notification_right_icon_size">@*android:dimen/car_touch_target_size</dimen>
diff --git a/car_product/sepolicy/test/kitchensink_app.te b/car_product/sepolicy/test/kitchensink_app.te
index 8bd5a68..297b3e7 100644
--- a/car_product/sepolicy/test/kitchensink_app.te
+++ b/car_product/sepolicy/test/kitchensink_app.te
@@ -9,6 +9,7 @@
allow kitchensink_app {
accessibility_service
activity_service
+ activity_task_service
autofill_service
connectivity_service
content_service
diff --git a/evs/app/EvsStateControl.cpp b/evs/app/EvsStateControl.cpp
index 50b3aeb..838c8d9 100644
--- a/evs/app/EvsStateControl.cpp
+++ b/evs/app/EvsStateControl.cpp
@@ -58,7 +58,8 @@
mGearValue.prop = static_cast<int32_t>(VehicleProperty::GEAR_SELECTION);
mTurnSignalValue.prop = static_cast<int32_t>(VehicleProperty::TURN_SIGNAL_STATE);
-#if 0 // This way we only ever deal with cameras which exist in the system
+#if 1
+ // This way we only ever deal with cameras which exist in the system
// Build our set of cameras for the states we support
ALOGD("Requesting camera list");
mEvs->getCameraList([this, &config](hidl_vec<CameraDesc> cameraList) {
diff --git a/evs/sampleDriver/Android.mk b/evs/sampleDriver/Android.mk
index c4463a9..434f54a 100644
--- a/evs/sampleDriver/Android.mk
+++ b/evs/sampleDriver/Android.mk
@@ -27,6 +27,7 @@
libhidltransport \
liblog \
libutils \
+ libhardware_legacy
LOCAL_INIT_RC := android.hardware.automotive.evs@1.0-sample.rc
diff --git a/evs/sampleDriver/EvsEnumerator.cpp b/evs/sampleDriver/EvsEnumerator.cpp
index 96fd067..071aeaa 100644
--- a/evs/sampleDriver/EvsEnumerator.cpp
+++ b/evs/sampleDriver/EvsEnumerator.cpp
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2016-2019 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.
@@ -19,8 +19,11 @@
#include "EvsGlDisplay.h"
#include <dirent.h>
+#include <hardware_legacy/uevent.h>
+using namespace std::chrono_literals;
+
namespace android {
namespace hardware {
namespace automotive {
@@ -33,10 +36,82 @@
// That is to say, this is effectively a singleton despite the fact that HIDL
// constructs a new instance for each client.
std::list<EvsEnumerator::CameraRecord> EvsEnumerator::sCameraList;
-wp<EvsGlDisplay> EvsEnumerator::sActiveDisplay;
+wp<EvsGlDisplay> EvsEnumerator::sActiveDisplay;
+std::mutex EvsEnumerator::sLock;
+std::condition_variable EvsEnumerator::sCameraSignal;
-// Number of trials to open the camera.
-static const unsigned int kMaxRetry = 3;
+// Constants
+const auto kEnumerationTimeout = 10s;
+
+void EvsEnumerator::EvsUeventThread(std::atomic<bool>& running) {
+ int status = uevent_init();
+ if (!status) {
+ ALOGE("Failed to initialize uevent handler.");
+ return;
+ }
+
+ char uevent_data[PAGE_SIZE - 2] = {};
+ while (running) {
+ int length = uevent_next_event(uevent_data, static_cast<int32_t>(sizeof(uevent_data)));
+
+ // Ensure double-null termination.
+ uevent_data[length] = uevent_data[length + 1] = '\0';
+
+ const char *action = nullptr;
+ const char *devname = nullptr;
+ const char *subsys = nullptr;
+ char *cp = uevent_data;
+ while (*cp) {
+ // EVS is interested only in ACTION, SUBSYSTEM, and DEVNAME.
+ if (!std::strncmp(cp, "ACTION=", 7)) {
+ action = cp + 7;
+ } else if (!std::strncmp(cp, "SUBSYSTEM=", 10)) {
+ subsys = cp + 10;
+ } else if (!std::strncmp(cp, "DEVNAME=", 8)) {
+ devname = cp + 8;
+ }
+
+ // Advance to after next \0
+ while (*cp++);
+ }
+
+ if (!devname || !subsys || std::strcmp(subsys, "video4linux")) {
+ // EVS expects that the subsystem of enabled video devices is
+ // video4linux.
+ continue;
+ }
+
+ // Update shared list.
+ bool cmd_addition = !std::strcmp(action, "add");
+ bool cmd_removal = !std::strcmp(action, "remove");
+ {
+ std::string devpath = "/dev/";
+ devpath += devname;
+
+ std::lock_guard<std::mutex> lock(sLock);
+ if (cmd_removal) {
+ for (auto it = sCameraList.begin(); it != sCameraList.end(); ++it) {
+ if (!devpath.compare(it->desc.cameraId)) {
+ // There must be no entry with duplicated name.
+ sCameraList.erase(it);
+ break;
+ }
+ }
+ } else if (cmd_addition) {
+ // NOTE: we are here adding new device without a validation
+ // because it always fails to open, b/132164956.
+ sCameraList.emplace_back(devpath.c_str());
+ } else {
+ // Ignore all other actions including "change".
+ }
+
+ // Notify the change.
+ sCameraSignal.notify_all();
+ }
+ }
+
+ return;
+}
EvsEnumerator::EvsEnumerator() {
ALOGD("EvsEnumerator created");
@@ -61,15 +136,19 @@
LOG_FATAL("Failed to open /dev folder\n");
}
struct dirent* entry;
- while ((entry = readdir(dir)) != nullptr) {
- // We're only looking for entries starting with 'video'
- if (strncmp(entry->d_name, "video", 5) == 0) {
- std::string deviceName("/dev/");
- deviceName += entry->d_name;
- videoCount++;
- if (qualifyCaptureDevice(deviceName.c_str())) {
- sCameraList.emplace_back(deviceName.c_str());
- captureCount++;
+ {
+ std::lock_guard<std::mutex> lock(sLock);
+
+ while ((entry = readdir(dir)) != nullptr) {
+ // We're only looking for entries starting with 'video'
+ if (strncmp(entry->d_name, "video", 5) == 0) {
+ std::string deviceName("/dev/");
+ deviceName += entry->d_name;
+ videoCount++;
+ if (qualifyCaptureDevice(deviceName.c_str())) {
+ sCameraList.emplace_back(deviceName.c_str());
+ captureCount++;
+ }
}
}
}
@@ -80,19 +159,16 @@
// Methods from ::android::hardware::automotive::evs::V1_0::IEvsEnumerator follow.
Return<void> EvsEnumerator::getCameraList(getCameraList_cb _hidl_cb) {
ALOGD("getCameraList");
-
- if (sCameraList.size() < 1) {
- // WAR: this assumes that the device has at least one compatible camera and
- // therefore keeps trying until it succeeds to open.
- // TODO: this is required for external USB camera so would be better to
- // subscribe hot-plug event.
- unsigned tries = 0;
- ALOGI("No camera is available; enumerate devices again.");
- while (sCameraList.size() < 1 && tries++ < kMaxRetry) {
- enumerateDevices();
-
- // TODO: remove this.
- usleep(5000);
+ {
+ std::unique_lock<std::mutex> lock(sLock);
+ if (sCameraList.size() < 1) {
+ // No qualified device has been found. Wait until new device is ready,
+ // for 10 seconds.
+ if (!sCameraSignal.wait_for(lock,
+ kEnumerationTimeout,
+ []{ return sCameraList.size() > 0; })) {
+ ALOGD("Timer expired. No new device has been added.");
+ }
}
}
diff --git a/evs/sampleDriver/EvsEnumerator.h b/evs/sampleDriver/EvsEnumerator.h
index e7e94d4..01a57bd 100644
--- a/evs/sampleDriver/EvsEnumerator.h
+++ b/evs/sampleDriver/EvsEnumerator.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2016-2019 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.
@@ -21,6 +21,8 @@
#include <android/hardware/automotive/evs/1.0/IEvsCamera.h>
#include <list>
+#include <thread>
+#include <atomic>
namespace android {
@@ -48,6 +50,9 @@
// Implementation details
EvsEnumerator();
+ // Listen to video device uevents
+ static void EvsUeventThread(std::atomic<bool>& running);
+
private:
struct CameraRecord {
CameraDesc desc;
@@ -70,6 +75,9 @@
static std::list<CameraRecord> sCameraList;
static wp<EvsGlDisplay> sActiveDisplay; // Weak pointer. Object destructs if client dies.
+
+ static std::mutex sLock; // Mutex on shared camera device list.
+ static std::condition_variable sCameraSignal; // Signal on camera device addition.
};
} // namespace implementation
diff --git a/evs/sampleDriver/service.cpp b/evs/sampleDriver/service.cpp
index 41be742..a444686 100644
--- a/evs/sampleDriver/service.cpp
+++ b/evs/sampleDriver/service.cpp
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2016-2019 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.
@@ -15,6 +15,7 @@
*/
#include <unistd.h>
+#include <atomic>
#include <hidl/HidlTransportSupport.h>
#include <utils/Errors.h>
@@ -45,6 +46,9 @@
configureRpcThreadpool(1, true /* callerWillJoin */);
+ std::atomic<bool> running { true };
+ std::thread ueventHandler(EvsEnumerator::EvsUeventThread, std::ref(running));
+
// Register our service -- if somebody is already registered by our name,
// they will be killed (their thread pool will throw an exception).
status_t status = service->registerAsService(kEnumeratorServiceName);
@@ -55,6 +59,12 @@
ALOGE("Could not register service %s (%d).", kEnumeratorServiceName, status);
}
+ // Exit a uevent handler thread.
+ running = false;
+ if (ueventHandler.joinable()) {
+ ueventHandler.join();
+ }
+
// In normal operation, we don't expect the thread pool to exit
ALOGE("EVS Hardware Enumerator is shutting down");
return 1;
diff --git a/evs/sepolicy/evs_app.te b/evs/sepolicy/evs_app.te
index 098499a..b5e3c95 100644
--- a/evs/sepolicy/evs_app.te
+++ b/evs/sepolicy/evs_app.te
@@ -18,3 +18,7 @@
allow evs_app gpu_device:chr_file rw_file_perms;
allow evs_app ion_device:chr_file r_file_perms;
allow evs_app system_file:dir r_dir_perms;
+
+# Allow use of binder and find surfaceflinger
+binder_use(evs_app);
+allow evs_app surfaceflinger_service:service_manager find;
diff --git a/evs/sepolicy/evs_driver.te b/evs/sepolicy/evs_driver.te
index dcf6700..3d5263e 100644
--- a/evs/sepolicy/evs_driver.te
+++ b/evs/sepolicy/evs_driver.te
@@ -18,3 +18,6 @@
binder_call(hal_evs_driver, surfaceflinger);
allow hal_evs_driver surfaceflinger_service:service_manager find;
allow hal_evs_driver ion_device:chr_file r_file_perms;
+
+# Allow the driver to access kobject uevents
+allow hal_evs_driver self:netlink_kobject_uevent_socket create_socket_perms_no_ioctl;
diff --git a/service/src/com/android/car/CarMediaService.java b/service/src/com/android/car/CarMediaService.java
index 309f393..a4e0d6e 100644
--- a/service/src/com/android/car/CarMediaService.java
+++ b/service/src/com/android/car/CarMediaService.java
@@ -15,12 +15,12 @@
*/
package com.android.car;
+import android.app.ActivityManager;
import android.car.media.CarMediaManager;
import android.car.media.CarMediaManager.MediaSourceChangedListener;
import android.car.media.ICarMedia;
import android.car.media.ICarMediaSourceListener;
import android.content.BroadcastReceiver;
-import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
@@ -32,9 +32,8 @@
import android.media.session.MediaSession;
import android.media.session.MediaSession.Token;
import android.media.session.MediaSessionManager;
+import android.media.session.MediaSessionManager.OnActiveSessionsChangedListener;
import android.media.session.PlaybackState;
-import android.os.Handler;
-import android.os.HandlerThread;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.service.media.MediaBrowserService;
@@ -44,7 +43,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import com.android.car.media.MediaBrowserConnector;
import com.android.car.user.CarUserService;
import java.io.PrintWriter;
@@ -70,25 +68,23 @@
public class CarMediaService extends ICarMedia.Stub implements CarServiceBase {
private static final String SOURCE_KEY = "media_source";
+ private static final String PLAYBACK_STATE_KEY = "playback_state";
private static final String SHARED_PREF = "com.android.car.media.car_media_service";
private static final String PACKAGE_NAME_SEPARATOR = ",";
private Context mContext;
private final MediaSessionManager mMediaSessionManager;
- private final MediaSessionUpdater mMediaSessionUpdater;
+ private MediaSessionUpdater mMediaSessionUpdater;
private String mPrimaryMediaPackage;
private SharedPreferences mSharedPrefs;
- // MediaController for the primary media source. Can be null if the primary media source has not
- // played any media yet.
- private MediaController mPrimaryMediaController;
- private final MediaBrowserConnector mMediaBrowserConnector;
+ // MediaController for the current active user's active media session. This controller can be
+ // null if playback has not been started yet.
+ private MediaController mActiveUserMediaController;
+ private SessionChangedListener mSessionsListener;
private RemoteCallbackList<ICarMediaSourceListener> mMediaSourceListeners =
new RemoteCallbackList();
- private HandlerThread mHandlerThread;
- private Handler mHandler;
-
/** The package name of the last media source that was removed while being primary. */
private String mRemovedMediaSourcePackage;
@@ -120,16 +116,10 @@
}
};
- private final MediaBrowserConnector.Callback mConnectedBrowserCallback = browser -> {
- if (browser != null && browser.isConnected()) {
- mPrimaryMediaController =
- new MediaController(mContext, browser.getSessionToken());
- TransportControls controls = mPrimaryMediaController.getTransportControls();
- if (controls != null) {
- controls.prepare();
- }
- } else {
- mPrimaryMediaController = null;
+ private BroadcastReceiver mUserSwitchReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ updateMediaSessionCallbackForCurrentUser();
}
};
@@ -145,14 +135,11 @@
filter.addDataScheme("package");
mContext.registerReceiver(mPackageRemovedReceiver, filter);
- mMediaSessionUpdater.registerCallbacks(mMediaSessionManager.getActiveSessions(null));
- mMediaSessionManager.addOnActiveSessionsChangedListener(
- controllers -> mMediaSessionUpdater.registerCallbacks(controllers), null);
+ IntentFilter userSwitchFilter = new IntentFilter();
+ userSwitchFilter.addAction(Intent.ACTION_USER_SWITCHED);
+ mContext.registerReceiver(mUserSwitchReceiver, userSwitchFilter);
- mMediaBrowserConnector = new MediaBrowserConnector(mContext, mConnectedBrowserCallback);
- mHandlerThread = new HandlerThread(CarLog.TAG_MEDIA);
- mHandlerThread.start();
- mHandler = new Handler(mHandlerThread.getLooper());
+ updateMediaSessionCallbackForCurrentUser();
}
@Override
@@ -173,12 +160,13 @@
public void dump(PrintWriter writer) {
writer.println("*CarMediaService*");
writer.println("\tCurrent media package: " + mPrimaryMediaPackage);
- if (mPrimaryMediaController != null) {
- writer
- .println("\tCurrent media controller: " + mPrimaryMediaController.getPackageName());
+ if (mActiveUserMediaController != null) {
+ writer.println(
+ "\tCurrent media controller: " + mActiveUserMediaController.getPackageName());
}
writer.println("\tNumber of active media sessions: "
- + mMediaSessionManager.getActiveSessions(null).size());
+ + mMediaSessionManager.getActiveSessionsForUser(null,
+ ActivityManager.getCurrentUser()).size());
}
/**
@@ -217,18 +205,46 @@
mMediaSourceListeners.unregister(callback);
}
+ private void updateMediaSessionCallbackForCurrentUser() {
+ if (mSessionsListener != null) {
+ mMediaSessionManager.removeOnActiveSessionsChangedListener(mSessionsListener);
+ }
+ mSessionsListener = new SessionChangedListener(ActivityManager.getCurrentUser());
+ mMediaSessionManager.addOnActiveSessionsChangedListener(mSessionsListener, null,
+ ActivityManager.getCurrentUser(), null);
+ mMediaSessionUpdater.registerCallbacks(mMediaSessionManager.getActiveSessionsForUser(
+ null, ActivityManager.getCurrentUser()));
+ }
+
/**
* Attempts to stop the current source using MediaController.TransportControls.stop()
*/
private void stop() {
- if (mPrimaryMediaController != null) {
- TransportControls controls = mPrimaryMediaController.getTransportControls();
+ if (mActiveUserMediaController != null) {
+ TransportControls controls = mActiveUserMediaController.getTransportControls();
if (controls != null) {
controls.stop();
}
}
}
+ private class SessionChangedListener implements OnActiveSessionsChangedListener {
+ private final int mCurrentUser;
+
+ SessionChangedListener(int currentUser) {
+ mCurrentUser = currentUser;
+ }
+
+ @Override
+ public void onActiveSessionsChanged(List<MediaController> controllers) {
+ if (ActivityManager.getCurrentUser() != mCurrentUser) {
+ Log.e(CarLog.TAG_MEDIA, "Active session callback for old user: " + mCurrentUser);
+ return;
+ }
+ mMediaSessionUpdater.registerCallbacks(controllers);
+ }
+ }
+
private class MediaControllerCallback extends MediaController.Callback {
private final MediaController mMediaController;
@@ -288,6 +304,11 @@
mCallbacks = updatedCallbacks;
updatePrimaryMediaSourceWithCurrentlyPlaying(additions);
+ // If there are no playing media sources, and we don't currently have the controller
+ // for the active source, check the new active sessions for a matching controller.
+ if (mActiveUserMediaController == null) {
+ updateActiveMediaController(additions);
+ }
}
/**
@@ -311,17 +332,8 @@
stop();
mPrimaryMediaPackage = packageName;
- mPrimaryMediaController = null;
- if (packageName != null) {
- String browseServiceName = getBrowseServiceClassName(packageName);
- if (browseServiceName != null) {
- mHandler.post(() -> mMediaBrowserConnector.connectTo(
- new ComponentName(packageName, browseServiceName)));
- } else {
- Log.e(CarLog.TAG_MEDIA, "Can't connect to BrowseService for media source: "
- + packageName);
- }
- }
+ updateActiveMediaController(mMediaSessionManager
+ .getActiveSessionsForUser(null, ActivityManager.getCurrentUser()));
if (mSharedPrefs != null) {
if (!TextUtils.isEmpty(mPrimaryMediaPackage)) {
@@ -348,6 +360,13 @@
mMediaSourceListeners.finishBroadcast();
}
+ private MediaController.Callback mMediaControllerCallback = new MediaController.Callback() {
+ @Override
+ public void onPlaybackStateChanged(PlaybackState state) {
+ savePlaybackState(state);
+ }
+ };
+
/**
* Finds the currently playing media source, then updates the active source if different
*/
@@ -422,4 +441,31 @@
String[] packageNames = serialized.split(PACKAGE_NAME_SEPARATOR);
return new ArrayDeque(Arrays.asList(packageNames));
}
+
+ private void savePlaybackState(PlaybackState state) {
+ mSharedPrefs.edit().putInt(PLAYBACK_STATE_KEY, state != null ? state.getState()
+ : PlaybackState.STATE_NONE).apply();
+ }
+
+ /**
+ * Updates active media controller from the list that has the same package name as the primary
+ * media package. Clears callback and resets media controller to null if not found.
+ */
+ private void updateActiveMediaController(List<MediaController> mediaControllers) {
+ if (mPrimaryMediaPackage == null) {
+ return;
+ }
+ if (mActiveUserMediaController != null) {
+ mActiveUserMediaController.unregisterCallback(mMediaControllerCallback);
+ mActiveUserMediaController = null;
+ }
+ for (MediaController controller : mediaControllers) {
+ if (mPrimaryMediaPackage.equals(controller.getPackageName())) {
+ mActiveUserMediaController = controller;
+ savePlaybackState(mActiveUserMediaController.getPlaybackState());
+ mActiveUserMediaController.registerCallback(mMediaControllerCallback);
+ return;
+ }
+ }
+ }
}
diff --git a/service/src/com/android/car/CarPropertyService.java b/service/src/com/android/car/CarPropertyService.java
index 961ba5e..ed24251 100644
--- a/service/src/com/android/car/CarPropertyService.java
+++ b/service/src/com/android/car/CarPropertyService.java
@@ -206,15 +206,20 @@
List<CarPropertyEvent> events = new LinkedList<CarPropertyEvent>();
if (mConfigs.get(propId).isGlobalProperty()) {
CarPropertyValue value = mHal.getProperty(propId, 0);
- CarPropertyEvent event = new CarPropertyEvent(
+ // CarPropertyEvent without a CarPropertyValue can not be used by any listeners.
+ if (value != null) {
+ CarPropertyEvent event = new CarPropertyEvent(
CarPropertyEvent.PROPERTY_EVENT_PROPERTY_CHANGE, value);
- events.add(event);
+ events.add(event);
+ }
} else {
for (int areaId : mConfigs.get(propId).getAreaIds()) {
CarPropertyValue value = mHal.getProperty(propId, areaId);
- CarPropertyEvent event = new CarPropertyEvent(
+ if (value != null) {
+ CarPropertyEvent event = new CarPropertyEvent(
CarPropertyEvent.PROPERTY_EVENT_PROPERTY_CHANGE, value);
- events.add(event);
+ events.add(event);
+ }
}
}
try {
diff --git a/service/src/com/android/car/hal/PropertyHalService.java b/service/src/com/android/car/hal/PropertyHalService.java
index ad81507..545fc2b 100644
--- a/service/src/com/android/car/hal/PropertyHalService.java
+++ b/service/src/com/android/car/hal/PropertyHalService.java
@@ -303,6 +303,9 @@
}
if (listener != null) {
for (VehiclePropValue v : values) {
+ if (v == null) {
+ continue;
+ }
int mgrPropId = halToManagerPropId(v.prop);
if (mgrPropId == NOT_SUPPORTED_PROPERTY) {
Log.e(TAG, "Property is not supported: 0x" + toHexString(v.prop));
diff --git a/service/src/com/android/car/hal/PropertyHalServiceIds.java b/service/src/com/android/car/hal/PropertyHalServiceIds.java
index ca90f78..5409b4d 100644
--- a/service/src/com/android/car/hal/PropertyHalServiceIds.java
+++ b/service/src/com/android/car/hal/PropertyHalServiceIds.java
@@ -400,6 +400,9 @@
mProps.put(VehicleProperty.FUEL_CONSUMPTION_UNITS_DISTANCE_OVER_VOLUME, new Pair<>(
Car.PERMISSION_READ_DISPLAY_UNITS,
Car.PERMISSION_CONTROL_DISPLAY_UNITS));
+ mProps.put(VehicleProperty.VEHICLE_SPEED_DISPLAY_UNITS, new Pair<>(
+ Car.PERMISSION_READ_DISPLAY_UNITS,
+ Car.PERMISSION_CONTROL_DISPLAY_UNITS));
}
/**
diff --git a/service/src/com/android/car/media/MediaBrowserConnector.java b/service/src/com/android/car/media/MediaBrowserConnector.java
deleted file mode 100644
index 0cc03f6..0000000
--- a/service/src/com/android/car/media/MediaBrowserConnector.java
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * Copyright (C) 2019 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.media;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.content.ComponentName;
-import android.content.Context;
-import android.media.browse.MediaBrowser;
-import android.util.Log;
-
-/**
- * A helper class to connect to a single {@link MediaBrowser}. Connecting to a new one
- * automatically disconnects the previous browser. Changes of the currently connected browser are
- * sent via {@link MediaBrowserConnector.Callback}.
- */
-public class MediaBrowserConnector {
-
- private static final String TAG = "MediaBrowserConnector";
-
- /** The callback to receive the currently connected {@link MediaBrowser}. */
- public interface Callback {
- /** When disconnecting, the given browser will be null. */
- void onConnectedBrowserChanged(@Nullable MediaBrowser browser);
- }
-
- private final Context mContext;
- private final Callback mCallback;
-
- @Nullable private ComponentName mBrowseService;
- @Nullable private MediaBrowser mBrowser;
-
- /**
- * Create a new MediaBrowserConnector.
- *
- * @param context The Context with which to build MediaBrowsers.
- */
- public MediaBrowserConnector(@NonNull Context context, @NonNull Callback callback) {
- mContext = context;
- mCallback = callback;
- }
-
- /** Counter so callbacks from obsolete connections can be ignored. */
- private int mBrowserConnectionCallbackCounter = 0;
-
- private class BrowserConnectionCallback extends MediaBrowser.ConnectionCallback {
-
- private final int mSequenceNumber = ++mBrowserConnectionCallbackCounter;
- private final String mCallbackPackage = mBrowseService.getPackageName();
-
- private boolean isValidCall(String method) {
- if (mSequenceNumber != mBrowserConnectionCallbackCounter) {
- Log.e(TAG, "Ignoring callback " + method + " for " + mCallbackPackage + " seq: "
- + mSequenceNumber + " current: " + mBrowserConnectionCallbackCounter
- + " current: " + mBrowseService.getPackageName());
- return false;
- } else if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, method + " " + mBrowseService.getPackageName());
- }
- return true;
- }
-
- @Override
- public void onConnected() {
- if (isValidCall("onConnected")) {
- mCallback.onConnectedBrowserChanged(mBrowser);
- }
- }
-
- @Override
- public void onConnectionFailed() {
- if (isValidCall("onConnectionFailed")) {
- mCallback.onConnectedBrowserChanged(null);
- }
- }
-
- @Override
- public void onConnectionSuspended() {
- if (isValidCall("onConnectionSuspended")) {
- mCallback.onConnectedBrowserChanged(null);
- }
- }
- }
-
- /**
- * Creates and connects a new {@link MediaBrowser} if the given {@link ComponentName}
- * isn't null. If needed, the previous browser is disconnected.
- * @param browseService the ComponentName of the media browser service.
- * @see MediaBrowser#MediaBrowser(Context, ComponentName,
- * MediaBrowser.ConnectionCallback, android.os.Bundle)
- */
- public void connectTo(@Nullable ComponentName browseService) {
- if (mBrowser != null && mBrowser.isConnected()) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Disconnecting: " + mBrowseService.getPackageName());
- }
- mCallback.onConnectedBrowserChanged(null);
- mBrowser.disconnect();
- }
-
- mBrowseService = browseService;
- if (mBrowseService != null) {
- mBrowser = createMediaBrowser(mBrowseService, new BrowserConnectionCallback());
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Connecting to: " + mBrowseService.getPackageName());
- }
- try {
- mBrowser.connect();
- } catch (IllegalStateException ex) {
- // Is this comment still valid ?
- // Ignore: MediaBrowse could be in an intermediate state (not connected, but not
- // disconnected either.). In this situation, trying to connect again can throw
- // this exception, but there is no way to know without trying.
- Log.e(TAG, "Connection exception: " + ex);
- }
- } else {
- mBrowser = null;
- }
- }
-
- // Override for testing.
- @NonNull
- protected MediaBrowser createMediaBrowser(@NonNull ComponentName browseService,
- @NonNull MediaBrowser.ConnectionCallback callback) {
- return new MediaBrowser(mContext, browseService, callback, null);
- }
-}
diff --git a/service/src/com/android/car/trust/BLEMessagePayloadStream.java b/service/src/com/android/car/trust/BLEMessagePayloadStream.java
new file mode 100644
index 0000000..1f1fdb1
--- /dev/null
+++ b/service/src/com/android/car/trust/BLEMessagePayloadStream.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2019 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.NonNull;
+import android.util.Log;
+
+import com.android.car.trust.BLEStream.BLEMessage;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+/**
+ * Manage a stream in which the {@code payload} field of
+ * {@link com.android.car.trust.BLEStream.BLEMessage}
+ * is written to.
+ */
+class BLEMessagePayloadStream {
+ private static final String TAG = "BLEMessagePayloadStream";
+ private ByteArrayOutputStream mPendingData = new ByteArrayOutputStream();
+ private boolean mIsComplete;
+
+ /**
+ * Clears this data stream.
+ */
+ public void reset() {
+ mPendingData.reset();
+ }
+
+ /**
+ * Extracts the payload from the given bytes and writes it to the stream.
+ *
+ * @param value The bytes that represent a {@link com.android.car.trust.BLEStream.BLEMessage}
+ */
+ public void write(byte[] value) {
+ try {
+ BLEMessage message = BLEMessage.parseFrom(value);
+ mPendingData.write(message.getPayload().toByteArray());
+ if (message.getPacketNumber() == message.getTotalPackets()) {
+ mIsComplete = true;
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Can not parse BLE message", e);
+ }
+ }
+
+ /**
+ * Returns {@code true} if a complete payload has been formed.
+ */
+ public boolean isComplete() {
+ return mIsComplete;
+ }
+
+ /**
+ * Returns the current contents of a stream as a byte array.
+ */
+ @NonNull
+ public byte[] toByteArray() {
+ return mPendingData.toByteArray();
+ }
+}
diff --git a/service/src/com/android/car/trust/BLEMessageV1Factory.java b/service/src/com/android/car/trust/BLEMessageV1Factory.java
new file mode 100644
index 0000000..c0b11d4
--- /dev/null
+++ b/service/src/com/android/car/trust/BLEMessageV1Factory.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2019 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.util.Log;
+
+import com.android.car.trust.BLEStream.BLEMessage;
+import com.android.car.trust.BLEStream.BLEMessage.OperationType;
+import com.android.car.trust.protobuf.ByteString;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Methods for creating {@link BLEStream} protos
+ */
+class BLEMessageV1Factory {
+
+ private static final String TAG = "BLEMessageFactory";
+
+ /**
+ * The size in bytes of a {@code fixed32} field in the proto.
+ * See this <a href="https://developers.google.com/protocol-buffers/docs/encoding">site</a> for
+ * more details.
+ */
+ private static final int FIXED_32_SIZE = 4;
+
+ /**
+ * Additional bytes that are needed during the encoding of the {@code payload} field.
+ *
+ * <p>The {@code payload} field is defined as {@code bytes}, and thus, needs 2 extra bytes to
+ * encode: one to encode the field number and another for length delimiting.
+ */
+ private static final int ADDITIONAL_PAYLOAD_ENCODING_SIZE = 2;
+
+ // The size needed to encode a boolean proto field
+ private static final int BOOLEAN_FIELD_ENCODING_SIZE = 1;
+
+ /**
+ * The bytes needed to encode the field number in the proto.
+ *
+ * <p>Since the v1 proto only has 6 fields, it will only take 1 additional byte to encode.
+ */
+ private static final int FIELD_NUMBER_ENCODING_SIZE = 1;
+ /**
+ * Current version of the proto.
+ */
+ private static final int PROTOCOL_VERSION = 1;
+ /**
+ * The size of the version field in the proto.
+ *
+ * <p>The version field is a {@code variant} and thus, its size can vary between 1-5 bytes.
+ * Since the version is {@code 1}, it will only take 1 byte to encode + 1 byte for the field
+ * number.
+ */
+ private static final int VERSION_SIZE = getEncodedSize(PROTOCOL_VERSION)
+ + FIELD_NUMBER_ENCODING_SIZE;
+ /**
+ * The size of fields in the header that do not change depending on their value.
+ *
+ * <p>The fixed fields are:
+ *
+ * <ol>
+ * <li>Version (1 byte + 1 byte for field)
+ * <li>Packet number (4 bytes + 1 byte for field)
+ * <li>Total packets (4 bytes + 1 byte for field)
+ * </ol>
+ *
+ * <p>Note, the version code is an {@code Int32Value} and thus, can vary, but it is always a set
+ * value at compile time. Thus, it can be calculated statically.
+ */
+ private static final int CONSTANT_HEADER_FIELD_SIZE = VERSION_SIZE
+ + (FIXED_32_SIZE + FIELD_NUMBER_ENCODING_SIZE)
+ + (FIXED_32_SIZE + FIELD_NUMBER_ENCODING_SIZE);
+
+ private BLEMessageV1Factory() {
+ }
+
+ /**
+ * Method used to generate a single message, the packet number and total packets will set to 1
+ * by default
+ *
+ * @param payload The data object to use as the
+ * {@link com.android.car.trust.BLEStream.BLEMessage}
+ * payload
+ * @param operation The operation this message represents
+ * @return The generated {@link com.android.car.trust.BLEStream.BLEMessage}
+ */
+ private static BLEMessage makeBLEMessage(byte[] payload, OperationType operation,
+ boolean isPayloadEncrypted) {
+ return BLEMessage.newBuilder()
+ .setVersion(PROTOCOL_VERSION)
+ .setOperation(operation)
+ .setPacketNumber(1)
+ .setTotalPackets(1)
+ .setIsPayloadEncrypted(isPayloadEncrypted)
+ .setPayload(ByteString.copyFrom(payload))
+ .build();
+ }
+
+ /**
+ * Split given data if necessary to fit within the given {@code maxSize}
+ *
+ * @param payload The payload to potentially split across multiple {@link
+ * com.android.car.trust.BLEStream.BLEMessage}s
+ * @param operation The operation this message represents
+ * @param maxSize The maximum size of each chunk
+ * @return An array of {@link com.android.car.trust.BLEStream.BLEMessage}s
+ */
+ public static List<BLEMessage> makeBLEMessages(byte[] payload, OperationType operation,
+ int maxSize, boolean isPayloadEncrypted) {
+ List<BLEMessage> bleMessages = new ArrayList();
+ int maxPayloadSize = maxSize - getProtoHeaderSize(operation, isPayloadEncrypted);
+ int payloadLength = payload.length;
+ if (payloadLength <= maxPayloadSize) {
+ bleMessages.add(makeBLEMessage(payload, operation, isPayloadEncrypted));
+ return bleMessages;
+ }
+ int totalPackets = (int) Math.ceil((double) payloadLength / maxPayloadSize);
+ int start = 0;
+ int end = maxPayloadSize;
+ for (int i = 0; i < totalPackets; i++) {
+ bleMessages.add(BLEMessage.newBuilder()
+ .setVersion(PROTOCOL_VERSION)
+ .setOperation(operation)
+ .setPacketNumber(i + 1)
+ .setTotalPackets(totalPackets)
+ .setIsPayloadEncrypted(isPayloadEncrypted)
+ .setPayload(ByteString.copyFrom(Arrays.copyOfRange(payload, start, end)))
+ .build());
+ start = end;
+ end = Math.min(start + maxPayloadSize, payloadLength);
+ }
+ return bleMessages;
+ }
+
+ /**
+ * Returns the header size for the proto in bytes. This method assumes that the proto
+ * contain a payload.
+ */
+ public static int getProtoHeaderSize(OperationType operation, boolean isPayloadEncrypted) {
+ int isPayloadEncryptedFieldSize =
+ isPayloadEncrypted ? (BOOLEAN_FIELD_ENCODING_SIZE + FIELD_NUMBER_ENCODING_SIZE) : 0;
+ int operationSize = getEncodedSize(operation.getNumber()) + FIELD_NUMBER_ENCODING_SIZE;
+ return CONSTANT_HEADER_FIELD_SIZE + operationSize + isPayloadEncryptedFieldSize
+ + ADDITIONAL_PAYLOAD_ENCODING_SIZE;
+ }
+
+ /**
+ * The methods in this section are taken from
+ * google3/third_party/swift/swift_protobuf/Sources/SwiftProtobuf/Variant.swift.
+ * It should be kept in sync as long as the proto version remains the same.
+ *
+ * <p>Computes the number of bytes that would be needed to store a 32-bit variant. Negative
+ * value is not considered because all proto values should be positive.
+ *
+ * @param value the data that need to be encoded
+ * @return the size of the encoded data
+ */
+ private static int getEncodedSize(int value) {
+ if (value < 0) {
+ Log.e(TAG, "Get a negative value from proto");
+ return 10;
+ }
+ if ((value & (~0 << 7)) == 0) {
+ return 1;
+ }
+ if ((value & (~0 << 14)) == 0) {
+ return 2;
+ }
+ if ((value & (~0 << 21)) == 0) {
+ return 3;
+ }
+ if ((value & (~0 << 28)) == 0) {
+ return 4;
+ }
+ return 5;
+ }
+}
diff --git a/service/src/com/android/car/trust/BleManager.java b/service/src/com/android/car/trust/BleManager.java
index 4a8bcbc..61e1be6 100644
--- a/service/src/com/android/car/trust/BleManager.java
+++ b/service/src/com/android/car/trust/BleManager.java
@@ -54,10 +54,12 @@
private static final int BLE_RETRY_LIMIT = 5;
private static final int BLE_RETRY_INTERVAL_MS = 1000;
- // https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.service.generic_access.xml
+ // https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth
+ // .service.generic_access.xml
private static final UUID GENERIC_ACCESS_PROFILE_UUID =
UUID.fromString("00001800-0000-1000-8000-00805f9b34fb");
- //https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.gap.device_name.xml
+ //https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth
+ // .characteristic.gap.device_name.xml
private static final UUID DEVICE_NAME_UUID =
UUID.fromString("00002a00-0000-1000-8000-00805f9b34fb");
@@ -81,8 +83,8 @@
* <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
- * @param data {@link AdvertiseData} data to advertise
+ * @param service {@link BluetoothGattService} that will be discovered by clients
+ * @param data {@link AdvertiseData} data to advertise
* @param advertiseCallback {@link AdvertiseCallback} callback for advertiser
*/
protected void startAdvertising(BluetoothGattService service, AdvertiseData data,
@@ -220,6 +222,14 @@
}
/**
+ * Triggered if a remote client has requested to change the MTU for a given connection.
+ *
+ * @param size The new MTU size.
+ */
+ protected void onMtuSizeChanged(int size) {
+ }
+
+ /**
* Triggered when a device (GATT client) connected.
*
* @param device Remote device that connected on BLE.
@@ -313,6 +323,15 @@
onCharacteristicWrite(device, requestId, characteristic,
preparedWrite, responseNeeded, offset, value);
}
+
+ @Override
+ public void onMtuChanged(BluetoothDevice device, int mtu) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onMtuChanged: " + mtu + " for device " + device.getAddress());
+ }
+ onMtuSizeChanged(mtu);
+ }
+
};
private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
@@ -337,7 +356,7 @@
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG,
"Connection state not connecting or disconnecting; ignoring: "
- + newState);
+ + newState);
}
}
}
diff --git a/service/src/com/android/car/trust/CarTrustAgentBleManager.java b/service/src/com/android/car/trust/CarTrustAgentBleManager.java
index 9e0785b..c47f527 100644
--- a/service/src/com/android/car/trust/CarTrustAgentBleManager.java
+++ b/service/src/com/android/car/trust/CarTrustAgentBleManager.java
@@ -33,9 +33,12 @@
import com.android.car.CarLocalServices;
import com.android.car.R;
import com.android.car.Utils;
+import com.android.car.trust.BLEStream.BLEMessage;
+import com.android.car.trust.BLEStream.BLEMessage.OperationType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.List;
import java.util.UUID;
/**
@@ -70,6 +73,7 @@
private String mOriginalBluetoothName;
private byte[] mUniqueId;
private String mRandomName;
+ private int mMtuSize = 20;
// Enrollment Service and Characteristic UUIDs
private UUID mEnrollmentServiceUuid;
@@ -83,6 +87,8 @@
private UUID mUnlockTokenHandleUuid;
private BluetoothGattService mUnlockGattService;
+ private BLEMessagePayloadStream mBleMessagePayloadStream = new BLEMessagePayloadStream();
+
CarTrustAgentBleManager(Context context) {
super(context);
}
@@ -117,6 +123,11 @@
}
@Override
+ protected void onMtuSizeChanged(int size) {
+ mMtuSize = size;
+ }
+
+ @Override
public void onCharacteristicWrite(BluetoothDevice device, int requestId,
BluetoothGattCharacteristic characteristic, boolean preparedWrite,
boolean responseNeeded, int offset, byte[] value) {
@@ -124,19 +135,28 @@
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onCharacteristicWrite received uuid: " + uuid);
}
+ // This write operation is not thread safe individually, but is guarded by the callback
+ // here.
+ mBleMessagePayloadStream.write(value);
+ if (!mBleMessagePayloadStream.isComplete()) {
+ return;
+ }
if (uuid.equals(mEnrollmentClientWriteUuid)) {
if (getEnrollmentService() != null) {
- getEnrollmentService().onEnrollmentDataReceived(value);
+ getEnrollmentService().onEnrollmentDataReceived(
+ mBleMessagePayloadStream.toByteArray());
}
} else if (uuid.equals(mUnlockEscrowTokenUuid)) {
if (getUnlockService() != null) {
- getUnlockService().onUnlockTokenReceived(value);
+ getUnlockService().onUnlockTokenReceived(mBleMessagePayloadStream.toByteArray());
+
}
} else if (uuid.equals(mUnlockTokenHandleUuid)) {
if (getUnlockService() != null) {
- getUnlockService().onUnlockHandleReceived(value);
+ getUnlockService().onUnlockHandleReceived(mBleMessagePayloadStream.toByteArray());
}
}
+ mBleMessagePayloadStream.reset();
}
@Override
@@ -316,24 +336,33 @@
}
void disconnectRemoteDevice() {
+ mBleMessagePayloadStream.reset();
stopGattServer();
}
/**
* Sends the given message to the specified device.
*
- * @param device The device to send the message to.
+ * @param device The device to send the message to.
* @param message A message to send.
*/
- void sendMessage(BluetoothDevice device, byte[] message) {
+ void sendMessage(BluetoothDevice device, byte[] message, OperationType operation,
+ boolean isPayloadEncrypted) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "sendMessage to: " + device.getAddress());
}
-
BluetoothGattCharacteristic serverCharacteristic = mEnrollmentGattService
.getCharacteristic(mEnrollmentServerWriteUuid);
- serverCharacteristic.setValue(message);
- notifyCharacteristicChanged(device, serverCharacteristic, false);
+ List<BLEMessage> bleMessages = BLEMessageV1Factory.makeBLEMessages(message, operation,
+ mMtuSize, isPayloadEncrypted);
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "sending " + bleMessages.size() + " messages to device");
+ }
+ for (BLEMessage bleMessage : bleMessages) {
+ // TODO(b/131719066) get acknowledgement from the phone then continue to send packets
+ serverCharacteristic.setValue(bleMessage.toByteArray());
+ notifyCharacteristicChanged(device, serverCharacteristic, false);
+ }
}
private final AdvertiseCallback mEnrollmentAdvertisingCallback = new AdvertiseCallback() {
diff --git a/service/src/com/android/car/trust/CarTrustAgentEnrollmentService.java b/service/src/com/android/car/trust/CarTrustAgentEnrollmentService.java
index 635d38f..39ec796 100644
--- a/service/src/com/android/car/trust/CarTrustAgentEnrollmentService.java
+++ b/service/src/com/android/car/trust/CarTrustAgentEnrollmentService.java
@@ -42,6 +42,7 @@
import com.android.car.R;
import com.android.car.Utils;
+import com.android.car.trust.BLEStream.BLEMessage.OperationType;
import com.android.internal.annotations.GuardedBy;
import java.io.PrintWriter;
@@ -67,7 +68,6 @@
*/
public class CarTrustAgentEnrollmentService extends ICarTrustAgentEnrollment.Stub {
private static final String TAG = "CarTrustAgentEnroll";
- private static final String FAKE_AUTH_STRING = "000000";
private static final String TRUSTED_DEVICE_ENROLLMENT_ENABLED_KEY =
"trusted_device_enrollment_enabled";
private static final byte[] CONFIRMATION_SIGNAL = "True".getBytes();
@@ -87,8 +87,6 @@
private Object mRemoteDeviceLock = new Object();
@GuardedBy("mRemoteDeviceLock")
private BluetoothDevice mRemoteEnrollmentDevice;
- @GuardedBy("this")
- private boolean mEnrollmentHandshakeAccepted;
private final Map<Long, Boolean> mTokenActiveStateMap = new HashMap<>();
private String mDeviceName;
private String mDeviceId;
@@ -110,8 +108,9 @@
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef({ENROLLMENT_STATE_NONE, ENROLLMENT_STATE_UNIQUE_ID,
- ENROLLMENT_STATE_ENCRYPTION_COMPLETED, ENROLLMENT_STATE_HANDLE})
- public @interface EnrollmentState {}
+ ENROLLMENT_STATE_ENCRYPTION_COMPLETED, ENROLLMENT_STATE_HANDLE})
+ public @interface EnrollmentState {
+ }
@EnrollmentState
private int mEnrollmentState;
@@ -190,7 +189,8 @@
@Override
public void enrollmentHandshakeAccepted(BluetoothDevice device) {
addEnrollmentServiceLog("enrollmentHandshakeAccepted");
- mCarTrustAgentBleManager.sendMessage(device, CONFIRMATION_SIGNAL);
+ mCarTrustAgentBleManager.sendMessage(device, CONFIRMATION_SIGNAL,
+ OperationType.ENCRYPTION_HANDSHAKE, /* isPayloadEncrypted= */ false);
setEnrollmentHandshakeAccepted(true);
}
@@ -219,7 +219,7 @@
}
}
- /**
+ /*
* Returns if there is an active token for the given user and handle.
*
* @param handle handle corresponding to the escrow token
@@ -416,7 +416,8 @@
Log.d(TAG, "Sending handle: " + handle);
}
mCarTrustAgentBleManager.sendMessage(mRemoteEnrollmentDevice,
- mEncryptionKey.encryptData(Utils.longToBytes(handle)));
+ mEncryptionKey.encryptData(Utils.longToBytes(handle)),
+ OperationType.CLIENT_MESSAGE, /* isPayloadEncrypted= */ true);
} else {
removeEscrowToken(handle, uid);
}
@@ -481,7 +482,6 @@
}
return;
}
-
switch (mEnrollmentState) {
case ENROLLMENT_STATE_NONE:
notifyDeviceIdReceived(value);
@@ -523,8 +523,8 @@
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Sending device id: " + uniqueId.toString());
}
- mCarTrustAgentBleManager.sendMessage(
- mRemoteEnrollmentDevice, Utils.uuidToBytes(uniqueId));
+ mCarTrustAgentBleManager.sendMessage(mRemoteEnrollmentDevice, Utils.uuidToBytes(uniqueId),
+ OperationType.CLIENT_MESSAGE, /* isPayloadEncrypted= */ false);
mEnrollmentState++;
}
@@ -541,13 +541,16 @@
/**
* Processes the given message as one that will establish encryption for secure communication.
*
- * <p>This method should be called continually until {@link #mEnrollmentHandshakeAccepted} is
- * {@code true}, meaning an secure channel has been set up.
+ * <p>This method should be called continually until {@link #mEncryptionState} is
+ * {@link HandshakeState#FINISHED}, meaning an secure channel has been set up.
*
* @param message The message received from the connected device.
* @throws HandshakeException If an error was encountered during the handshake flow.
*/
private void processInitEncryptionMessage(byte[] message) throws HandshakeException {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Processing init encryption message.");
+ }
switch (mEncryptionState) {
case HandshakeState.UNKNOWN:
if (Log.isLoggable(TAG, Log.DEBUG)) {
@@ -557,7 +560,8 @@
mHandshakeMessage = mEncryptionRunner.respondToInitRequest(message);
mEncryptionState = mHandshakeMessage.getHandshakeState();
mCarTrustAgentBleManager.sendMessage(
- mRemoteEnrollmentDevice, mHandshakeMessage.getNextMessage());
+ mRemoteEnrollmentDevice, mHandshakeMessage.getNextMessage(),
+ OperationType.ENCRYPTION_HANDSHAKE, /* isPayloadEncrypted= */ false);
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Updated encryption state: " + mEncryptionState);
@@ -582,9 +586,9 @@
showVerificationCode();
return;
}
-
mCarTrustAgentBleManager.sendMessage(mRemoteEnrollmentDevice,
- mHandshakeMessage.getNextMessage());
+ mHandshakeMessage.getNextMessage(), OperationType.ENCRYPTION_HANDSHAKE,
+ /* isPayloadEncrypted= */ false);
break;
case HandshakeState.VERIFICATION_NEEDED:
Log.w(TAG, "Encountered VERIFICATION_NEEDED state when it should have been "
@@ -638,8 +642,6 @@
}
private synchronized void setEnrollmentHandshakeAccepted(boolean accepted) {
- mEnrollmentHandshakeAccepted = accepted;
-
if (!accepted) {
return;
}
@@ -846,7 +848,7 @@
* <p>
* It also registers for death notifications of the host.
*/
- private class EnrollmentStateClient implements IBinder.DeathRecipient {
+ private class EnrollmentStateClient implements DeathRecipient {
private final IBinder mListenerBinder;
private final ICarTrustAgentEnrollmentCallback mListener;
@@ -877,7 +879,7 @@
}
}
- private class BleStateChangeClient implements IBinder.DeathRecipient {
+ private class BleStateChangeClient implements DeathRecipient {
private final IBinder mListenerBinder;
private final ICarTrustAgentBleCallback mListener;
diff --git a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/applications/appinfo/AppDataUsagePreferenceController.java b/tests/CarDeveloperOptions/src/com/android/car/developeroptions/applications/appinfo/AppDataUsagePreferenceController.java
index ea3bc1b..cebd79e 100644
--- a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/applications/appinfo/AppDataUsagePreferenceController.java
+++ b/tests/CarDeveloperOptions/src/com/android/car/developeroptions/applications/appinfo/AppDataUsagePreferenceController.java
@@ -92,7 +92,6 @@
.addUid(mParent.getAppEntry().info.uid)
.setRetrieveDetail(false)
.setNetworkTemplate(template)
- .setSubscriberId(template.getSubscriberId())
.build();
}
diff --git a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/datausage/AppDataUsage.java b/tests/CarDeveloperOptions/src/com/android/car/developeroptions/datausage/AppDataUsage.java
index 27aa4bd..9a2cdbf 100644
--- a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/datausage/AppDataUsage.java
+++ b/tests/CarDeveloperOptions/src/com/android/car/developeroptions/datausage/AppDataUsage.java
@@ -393,8 +393,7 @@
final NetworkCycleDataForUidLoader.Builder builder
= NetworkCycleDataForUidLoader.builder(mContext);
builder.setRetrieveDetail(true)
- .setNetworkTemplate(mTemplate)
- .setSubscriberId(mTemplate.getSubscriberId());
+ .setNetworkTemplate(mTemplate);
if (mAppItem.category == AppItem.CATEGORY_USER) {
for (int i = 0; i < mAppItem.uids.size(); i++) {
builder.addUid(mAppItem.uids.keyAt(i));
diff --git a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/datausage/DataUsageList.java b/tests/CarDeveloperOptions/src/com/android/car/developeroptions/datausage/DataUsageList.java
index c6d3b4f..83e11f2 100644
--- a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/datausage/DataUsageList.java
+++ b/tests/CarDeveloperOptions/src/com/android/car/developeroptions/datausage/DataUsageList.java
@@ -492,7 +492,6 @@
public Loader<List<NetworkCycleChartData>> onCreateLoader(int id, Bundle args) {
return NetworkCycleChartDataLoader.builder(getContext())
.setNetworkTemplate(mTemplate)
- .setSubscriberId(mTelephonyManager.getSubscriberId(mSubId))
.build();
}
@@ -518,8 +517,7 @@
return new NetworkStatsSummaryLoader.Builder(getContext())
.setStartTime(mChart.getInspectStart())
.setEndTime(mChart.getInspectEnd())
- .setNetworkType(mNetworkType)
- .setSubscriberId(mTelephonyManager.getSubscriberId(mSubId))
+ .setNetworkTemplate(mTemplate)
.build();
}
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/bluetooth_headset.xml b/tests/EmbeddedKitchenSinkApp/res/layout/bluetooth_headset.xml
index 0f2a8e7..86d505c 100644
--- a/tests/EmbeddedKitchenSinkApp/res/layout/bluetooth_headset.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/bluetooth_headset.xml
@@ -13,25 +13,19 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:orientation="vertical"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
- <LinearLayout
- android:orientation="horizontal"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_weight="1" >
- <TextView
- android:id="@+id/bluetooth_device"
- android:layout_height="wrap_content"
- android:layout_width="wrap_content"/>
- <Button
- android:id="@+id/bluetooth_pick_device"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/bluetooth_pick_device"/>
- </LinearLayout>
+<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:columnCount = "2">
+ <TextView
+ android:id="@+id/bluetooth_device"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"/>
+ <Button
+ android:id="@+id/bluetooth_pick_device"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/bluetooth_pick_device"/>
<Button
android:id="@+id/bluetooth_headset_connect"
android:layout_width="wrap_content"
@@ -58,6 +52,21 @@
android:layout_height="wrap_content"
android:text="@string/bluetooth_quiet_mode_enable"/>
<Button
+ android:id="@+id/bluetooth_start_outgoing_call"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/bluetooth_start_outgoing_call"/>
+ <EditText
+ android:id="@+id/bluetooth_outgoing_phone_number"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:hint="@string/bluetooth_outgoing_phone_number"/>
+ <Button
+ android:id="@+id/bluetooth_end_outgoing_call"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/bluetooth_end_outgoing_call"/>
+ <Button
android:id="@+id/bluetooth_hold_call"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -72,4 +81,4 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/bluetooth_voice_recognition_disable"/>
-</LinearLayout>
+</GridLayout>
diff --git a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
index d292ee6..77b64c7 100644
--- a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
@@ -265,6 +265,9 @@
<string name="bluetooth_pick_device" translatable="false">Pick Device</string>
<string name="bluetooth_quiet_mode_enable" translatable="false">Quiet Mode</string>
<string name="bluetooth_hold_call" translatable="false">Hold call</string>
+ <string name="bluetooth_start_outgoing_call" translatable="false">Start call</string>
+ <string name="bluetooth_end_outgoing_call" translatable="false">End call</string>
+ <string name="bluetooth_outgoing_phone_number" translatable="false">Enter number</string>
<string name="bluetooth_voice_recognition_enable" translatable="false">Start Voice Recognition</string>
<string name="bluetooth_voice_recognition_disable" translatable="false">Stop Voice Recognition</string>
<string name="uploading_supported_feature" translatable="false">Uploading_Supported bit value</string>
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/bluetooth/BluetoothHeadsetFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/bluetooth/BluetoothHeadsetFragment.java
index 2d57d28..619a2b3 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/bluetooth/BluetoothHeadsetFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/bluetooth/BluetoothHeadsetFragment.java
@@ -20,6 +20,7 @@
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothDevicePicker;
import android.bluetooth.BluetoothHeadsetClient;
+import android.bluetooth.BluetoothHeadsetClientCall;
import android.bluetooth.BluetoothProfile;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -31,7 +32,9 @@
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
+import android.widget.EditText;
import android.widget.TextView;
+import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
@@ -53,8 +56,12 @@
Button mHoldCall;
Button mEnableBVRA;
Button mDisableBVRA;
+ Button mStartOutgoingCall;
+ Button mEndOutgoingCall;
+ EditText mOutgoingPhoneNumber;
BluetoothHeadsetClient mHfpClientProfile;
+ BluetoothHeadsetClientCall mOutgoingCall;
// Intent for picking a Bluetooth device
public static final String DEVICE_PICKER_ACTION =
@@ -75,6 +82,9 @@
mHoldCall = (Button) v.findViewById(R.id.bluetooth_hold_call);
mEnableBVRA = (Button) v.findViewById(R.id.bluetooth_voice_recognition_enable);
mDisableBVRA = (Button) v.findViewById(R.id.bluetooth_voice_recognition_disable);
+ mStartOutgoingCall = (Button) v.findViewById(R.id.bluetooth_start_outgoing_call);
+ mEndOutgoingCall = (Button) v.findViewById(R.id.bluetooth_end_outgoing_call);
+ mOutgoingPhoneNumber = (EditText) v.findViewById(R.id.bluetooth_outgoing_phone_number);
// Pick a bluetooth device
mDevicePicker.setOnClickListener(new View.OnClickListener() {
@@ -147,6 +157,23 @@
stopBVRA();
}
});
+
+ // Start a outgoing call
+ mStartOutgoingCall.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ startCall();
+ }
+ });
+
+ // Stop a outgoing call
+ mEndOutgoingCall.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ stopCall();
+ }
+ });
+
return v;
}
@@ -260,6 +287,57 @@
mHfpClientProfile.stopVoiceRecognition(mPickedDevice);
}
+ void startCall() {
+ if (mPickedDevice == null) {
+ Log.w(TAG, "Device null when trying to start voice call!");
+ return;
+ }
+
+ // Check if we have the proxy and connect the device.
+ if (mHfpClientProfile == null) {
+ Log.w(TAG, "HFP Profile proxy not available, cannot start voice call to "
+ + mPickedDevice);
+ return;
+ }
+
+ if (mOutgoingCall != null) {
+ Log.w(TAG, "Potential on-going call or a stale call " + mOutgoingCall);
+ }
+
+ String number = mOutgoingPhoneNumber.getText().toString();
+ mOutgoingCall = mHfpClientProfile.dial(mPickedDevice, number);
+ if (mOutgoingCall == null) {
+ Log.w(TAG, "Fail to dial number " + number + ". Make sure profile connect first.");
+ } else {
+ Log.d(TAG, "Succeed in creating outgoing call " + mOutgoingCall + " for number "
+ + number);
+ }
+ }
+
+ void stopCall() {
+ if (mPickedDevice == null) {
+ Log.w(TAG, "Device null when trying to stop voice call!");
+ return;
+ }
+
+ // Check if we have the proxy and connect the device.
+ if (mHfpClientProfile == null) {
+ Log.w(TAG, "HFP Profile proxy not available, cannot stop voice call to "
+ + mPickedDevice);
+ return;
+ }
+
+ if (mOutgoingCall != null) {
+ if (mHfpClientProfile.terminateCall(mPickedDevice, mOutgoingCall)) {
+ Log.d(TAG, "Succeed in terminating outgoing call " + mOutgoingCall);
+ mOutgoingCall = null;
+ } else {
+ Log.d(TAG, "Fail to terminate outgoing call " + mOutgoingCall);
+ }
+ } else {
+ Log.w(TAG, "No outgoing call to terminate");
+ }
+ }
private final BroadcastReceiver mPickerReceiver = new BroadcastReceiver() {
@@ -271,6 +349,10 @@
if (BluetoothDevicePicker.ACTION_DEVICE_SELECTED.equals(action)) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+ if (device == null) {
+ Toast.makeText(getContext(), "No device selected", Toast.LENGTH_SHORT).show();
+ return;
+ }
mPickedDevice = device;
String text = device.getName() == null ?
device.getAddress() : device.getName() + " " + device.getAddress();
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/bluetooth/MapMceTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/bluetooth/MapMceTestFragment.java
index ab48a82..68d1c73 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/bluetooth/MapMceTestFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/bluetooth/MapMceTestFragment.java
@@ -364,6 +364,10 @@
if (BluetoothDevicePicker.ACTION_DEVICE_SELECTED.equals(action)) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
Log.v(TAG, "mPickerReceiver got " + device);
+ if (device == null) {
+ Toast.makeText(getContext(), "No device selected", Toast.LENGTH_SHORT).show();
+ return;
+ }
mMapProfile.connect(device);
// The receiver can now be disabled.
diff --git a/tests/carservice_unit_test/src/android/car/userlib/CarUserManagerHelperTest.java b/tests/carservice_unit_test/src/android/car/userlib/CarUserManagerHelperTest.java
index 993979a..02a3f13 100644
--- a/tests/carservice_unit_test/src/android/car/userlib/CarUserManagerHelperTest.java
+++ b/tests/carservice_unit_test/src/android/car/userlib/CarUserManagerHelperTest.java
@@ -24,6 +24,7 @@
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import android.app.ActivityManager;
import android.content.BroadcastReceiver;
@@ -35,11 +36,9 @@
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
-import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
-import android.sysprop.CarProperties;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
@@ -77,6 +76,8 @@
private ActivityManager mActivityManager;
@Mock
private CarUserManagerHelper.OnUsersUpdateListener mTestListener;
+ @Mock
+ private TestableFrameworkWrapper mTestableFrameworkWrapper;
private static final String GUEST_USER_NAME = "testGuest";
private static final String TEST_USER_NAME = "testUser";
@@ -98,7 +99,7 @@
doReturn(InstrumentationRegistry.getTargetContext().getContentResolver())
.when(mContext).getContentResolver();
doReturn(mContext).when(mContext).getApplicationContext();
- mCarUserManagerHelper = new CarUserManagerHelper(mContext);
+ mCarUserManagerHelper = new CarUserManagerHelper(mContext, mTestableFrameworkWrapper);
mCarUserManagerHelper.setDefaultAdminName(DEFAULT_ADMIN_NAME);
mCurrentProcessUser = createUserInfoForId(UserHandle.myUserId());
@@ -111,8 +112,9 @@
mForegroundUserId = ActivityManager.getCurrentUser();
mForegroundUser = createUserInfoForId(mForegroundUserId);
- // Clear boot override for every test
- CarProperties.boot_user_override_id(null);
+ // Clear boot override for every test by returning the default value passed to the method
+ when(mTestableFrameworkWrapper.getBootUserOverrideId(anyInt()))
+ .thenAnswer(stub -> stub.getArguments()[0]);
}
@Test
@@ -164,9 +166,9 @@
UserInfo user2 = createUserInfoForId(mForegroundUserId + 1);
// Create two ephemeral users.
UserInfo user3 = new UserInfo(
- /* id= */mForegroundUserId + 2, /* name = */ "user3", UserInfo.FLAG_EPHEMERAL);
+ /* id= */mForegroundUserId + 2, /* name = */ "user3", UserInfo.FLAG_EPHEMERAL);
UserInfo user4 = new UserInfo(
- /* id= */mForegroundUserId + 3, /* name = */ "user4", UserInfo.FLAG_EPHEMERAL);
+ /* id= */mForegroundUserId + 3, /* name = */ "user4", UserInfo.FLAG_EPHEMERAL);
mockGetUsers(user1, user2, user3, user4);
@@ -217,33 +219,33 @@
@Test
public void testCurrentProcessCanAddUsers() {
doReturn(false).when(mUserManager)
- .hasUserRestriction(UserManager.DISALLOW_ADD_USER);
+ .hasUserRestriction(UserManager.DISALLOW_ADD_USER);
assertThat(mCarUserManagerHelper.canCurrentProcessAddUsers()).isTrue();
doReturn(true).when(mUserManager)
- .hasUserRestriction(UserManager.DISALLOW_ADD_USER);
+ .hasUserRestriction(UserManager.DISALLOW_ADD_USER);
assertThat(mCarUserManagerHelper.canCurrentProcessAddUsers()).isFalse();
}
@Test
public void testCurrentProcessCanRemoveUsers() {
doReturn(false).when(mUserManager)
- .hasUserRestriction(UserManager.DISALLOW_REMOVE_USER);
+ .hasUserRestriction(UserManager.DISALLOW_REMOVE_USER);
assertThat(mCarUserManagerHelper.canCurrentProcessRemoveUsers()).isTrue();
doReturn(true).when(mUserManager)
- .hasUserRestriction(UserManager.DISALLOW_REMOVE_USER);
+ .hasUserRestriction(UserManager.DISALLOW_REMOVE_USER);
assertThat(mCarUserManagerHelper.canCurrentProcessRemoveUsers()).isFalse();
}
@Test
public void testCurrentProcessCanSwitchUsers() {
doReturn(false).when(mUserManager)
- .hasUserRestriction(UserManager.DISALLOW_USER_SWITCH);
+ .hasUserRestriction(UserManager.DISALLOW_USER_SWITCH);
assertThat(mCarUserManagerHelper.canCurrentProcessSwitchUsers()).isTrue();
doReturn(true).when(mUserManager)
- .hasUserRestriction(UserManager.DISALLOW_USER_SWITCH);
+ .hasUserRestriction(UserManager.DISALLOW_USER_SWITCH);
assertThat(mCarUserManagerHelper.canCurrentProcessSwitchUsers()).isFalse();
}
@@ -277,7 +279,7 @@
@Test
public void testGetMaxSupportedUsers() {
- SystemProperties.set("fw.max_users", "11");
+ setMaxSupportedUsers(11);
// Max users - headless system user.
assertThat(mCarUserManagerHelper.getMaxSupportedUsers()).isEqualTo(10);
@@ -285,7 +287,7 @@
@Test
public void testGetMaxSupportedRealUsers() {
- SystemProperties.set("fw.max_users", "7");
+ setMaxSupportedUsers(7);
// Create three managed profiles, and two normal users.
UserInfo user1 = createUserInfoForId(10);
@@ -314,16 +316,16 @@
mockGetUsers(mSystemUser, user1, user2, user3, user4);
- SystemProperties.set("fw.max_users", "6");
+ setMaxSupportedUsers(6);
assertThat(mCarUserManagerHelper.isUserLimitReached()).isFalse();
- SystemProperties.set("fw.max_users", "5");
+ setMaxSupportedUsers(5);
assertThat(mCarUserManagerHelper.isUserLimitReached()).isTrue();
}
@Test
public void testIsUserLimitReachedIgnoresGuests() {
- SystemProperties.set("fw.max_users", "6");
+ setMaxSupportedUsers(6);
UserInfo user1 = createUserInfoForId(10);
UserInfo user2 =
@@ -500,7 +502,7 @@
mockGetUsers(adminInfo);
assertThat(mCarUserManagerHelper.removeUser(adminInfo, GUEST_USER_NAME))
- .isEqualTo(false);
+ .isEqualTo(false);
}
@Test
@@ -679,7 +681,7 @@
assertThat(guestRestrictions.getBoolean(UserManager.DISALLOW_MODIFY_ACCOUNTS)).isTrue();
assertThat(guestRestrictions.getBoolean(UserManager.DISALLOW_INSTALL_APPS)).isTrue();
assertThat(guestRestrictions.getBoolean(UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES))
- .isTrue();
+ .isTrue();
assertThat(guestRestrictions.getBoolean(UserManager.DISALLOW_UNINSTALL_APPS)).isTrue();
}
@@ -954,7 +956,7 @@
private void mockGetUsers(UserInfo... users) {
List<UserInfo> testUsers = new ArrayList<>();
- for (UserInfo user: users) {
+ for (UserInfo user : users) {
testUsers.add(user);
}
doReturn(testUsers).when(mUserManager).getUsers(true);
@@ -966,6 +968,11 @@
}
private void setDefaultBootUserOverride(int userId) {
- CarProperties.boot_user_override_id(userId);
+ doReturn(userId).when(mTestableFrameworkWrapper)
+ .getBootUserOverrideId(anyInt());
}
-}
+
+ private void setMaxSupportedUsers(int maxValue) {
+ doReturn(maxValue).when(mTestableFrameworkWrapper).userManagerGetMaxSupportedUsers();
+ }
+}
\ No newline at end of file
diff --git a/user/car-user-lib/src/android/car/userlib/CarUserManagerHelper.java b/user/car-user-lib/src/android/car/userlib/CarUserManagerHelper.java
index cfd50cd..a46cf37 100644
--- a/user/car-user-lib/src/android/car/userlib/CarUserManagerHelper.java
+++ b/user/car-user-lib/src/android/car/userlib/CarUserManagerHelper.java
@@ -99,6 +99,7 @@
private final Context mContext;
private final UserManager mUserManager;
private final ActivityManager mActivityManager;
+ private final TestableFrameworkWrapper mTestableFrameworkWrapper;
private String mDefaultAdminName;
private Bitmap mDefaultGuestUserIcon;
private ArrayList<OnUsersUpdateListener> mUpdateListeners;
@@ -122,10 +123,16 @@
* @param context Application Context
*/
public CarUserManagerHelper(Context context) {
+ this(context, new TestableFrameworkWrapper());
+ }
+
+ @VisibleForTesting
+ CarUserManagerHelper(Context context, TestableFrameworkWrapper testableFrameworkWrapper) {
mUpdateListeners = new ArrayList<>();
mContext = context.getApplicationContext();
mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
mActivityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
+ mTestableFrameworkWrapper = testableFrameworkWrapper;
}
/**
@@ -212,7 +219,7 @@
*
* This method checks for the initial user via three mechanisms in this order:
* <ol>
- * <li>Check for a boot user override via {@link KEY_BOOT_USER_OVERRIDE_ID}</li>
+ * <li>Check for a boot user override via {@link CarProperties#boot_user_override_id()}</li>
* <li>Check for the last active user in the system</li>
* <li>Fallback to the smallest user id that is not {@link UserHandle.USER_SYSTEM}</li>
* </ol>
@@ -226,7 +233,7 @@
public int getInitialUser() {
List<Integer> allUsers = userInfoListToUserIdList(getAllPersistentUsers());
- int bootUserOverride = CarProperties.boot_user_override_id().orElse(BOOT_USER_NOT_FOUND);
+ int bootUserOverride = mTestableFrameworkWrapper.getBootUserOverrideId(BOOT_USER_NOT_FOUND);
// If an override user is present and a real user, return it
if (bootUserOverride != BOOT_USER_NOT_FOUND
@@ -468,9 +475,9 @@
*/
public int getMaxSupportedUsers() {
if (isHeadlessSystemUser()) {
- return UserManager.getMaxSupportedUsers() - 1;
+ return mTestableFrameworkWrapper.userManagerGetMaxSupportedUsers() - 1;
}
- return UserManager.getMaxSupportedUsers();
+ return mTestableFrameworkWrapper.userManagerGetMaxSupportedUsers();
}
/**
diff --git a/user/car-user-lib/src/android/car/userlib/TestableFrameworkWrapper.java b/user/car-user-lib/src/android/car/userlib/TestableFrameworkWrapper.java
new file mode 100644
index 0000000..472ccac
--- /dev/null
+++ b/user/car-user-lib/src/android/car/userlib/TestableFrameworkWrapper.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2019 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.userlib;
+
+import android.os.UserManager;
+import android.sysprop.CarProperties;
+
+/**
+ * Testable versions of APIs from the framework. This helps test difficult areas of the framework
+ * due to limitations like SELinux or hard to mock static methods.
+ */
+public class TestableFrameworkWrapper {
+ /**
+ * Wrapper around {@link CarProperties#boot_user_override_id()}
+ */
+ public int getBootUserOverrideId(int defaultValue) {
+ return CarProperties.boot_user_override_id().orElse(defaultValue);
+ }
+
+ /**
+ * Wrapper around {@link UserManager#getMaxSupportedUsers()}
+ */
+ public int userManagerGetMaxSupportedUsers() {
+ return UserManager.getMaxSupportedUsers();
+ }
+}