Merge a98f249fab34d1997d74005769308c0b82af4516 on remote branch
Change-Id: I6a77fcc93360e31530d8eea428f41e19b435b6e3
diff --git a/car-apps-common/src/com/android/car/apps/common/ControlBar.java b/car-apps-common/src/com/android/car/apps/common/ControlBar.java
index 4181867..94fb206 100644
--- a/car-apps-common/src/com/android/car/apps/common/ControlBar.java
+++ b/car-apps-common/src/com/android/car/apps/common/ControlBar.java
@@ -32,6 +32,7 @@
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
+import android.view.ViewTreeObserver.OnGlobalFocusChangeListener;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.LinearLayout;
@@ -103,6 +104,17 @@
// Weight for the spacers used between buttons
private static final float SPACERS_WEIGHT = 1f;
+ private final OnGlobalFocusChangeListener mFocusChangeListener =
+ (oldFocus, newFocus) -> {
+ // Collapse the control bar when it is expanded and loses focus.
+ boolean hasFocus = hasFocus();
+ if (mHasFocus && !hasFocus && mIsExpanded) {
+ onExpandCollapse();
+ }
+ mHasFocus = hasFocus;
+ };
+
+
public ControlBar(Context context) {
super(context);
init(context, null, 0, 0);
@@ -167,15 +179,6 @@
mDefaultExpandCollapseView.setContentDescription(context.getString(
R.string.control_bar_expand_collapse_button));
mDefaultExpandCollapseView.setOnClickListener(v -> onExpandCollapse());
-
- // Collapse the control bar when it is expanded and loses focus.
- getViewTreeObserver().addOnGlobalFocusChangeListener((oldFocus, newFocus) -> {
- boolean hasFocus = hasFocus();
- if (mHasFocus && !hasFocus && mIsExpanded) {
- onExpandCollapse();
- }
- mHasFocus = hasFocus;
- });
}
private int getSlotIndex(@SlotPosition int slotPosition) {
@@ -183,6 +186,18 @@
}
@Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ getViewTreeObserver().addOnGlobalFocusChangeListener(mFocusChangeListener);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ getViewTreeObserver().removeOnGlobalFocusChangeListener(mFocusChangeListener);
+ super.onDetachedFromWindow();
+ }
+
+ @Override
public void setView(@Nullable View view, @SlotPosition int slotPosition) {
if (view != null) {
mFixedViews.put(slotPosition, view);
diff --git a/car-apps-common/src/com/android/car/apps/common/util/ViewUtils.java b/car-apps-common/src/com/android/car/apps/common/util/ViewUtils.java
index a6cc870..0e41a6d 100644
--- a/car-apps-common/src/com/android/car/apps/common/util/ViewUtils.java
+++ b/car-apps-common/src/com/android/car/apps/common/util/ViewUtils.java
@@ -39,13 +39,34 @@
* Utility methods to operate over views.
*/
public class ViewUtils {
+
+ /** Listener to take action when animations are done. */
+ public interface ViewAnimEndListener {
+ /**
+ * Called when the animation created by {@link #hideViewAnimated} or
+ * {@link #showHideViewAnimated} has reached its end.
+ */
+ void onAnimationEnd(View view);
+ }
+
+ /** Shows the view if show is set to true otherwise hides it. */
+ public static void showHideViewAnimated(boolean show, @NonNull View view, int duration,
+ @Nullable ViewAnimEndListener listener) {
+ if (show) {
+ showViewAnimated(view, duration, listener);
+ } else {
+ hideViewAnimated(view, duration, listener);
+ }
+ }
+
/**
* Hides a view using a fade-out animation
*
* @param view {@link View} to be hidden
* @param duration animation duration in milliseconds.
*/
- public static void hideViewAnimated(@NonNull View view, int duration) {
+ public static void hideViewAnimated(@NonNull View view, int duration,
+ @Nullable ViewAnimEndListener listener) {
// Cancel existing animation to avoid race condition
// if show and hide are called at the same time
view.animate().cancel();
@@ -56,12 +77,26 @@
return;
}
+ Animator.AnimatorListener hider = hideViewAfterAnimation(view);
view.animate()
.setDuration(duration)
- .setListener(hideViewAfterAnimation(view))
+ .setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ hider.onAnimationEnd(animation);
+ if (listener != null) {
+ listener.onAnimationEnd(view);
+ }
+ }
+ })
.alpha(0f);
}
+ /** Hides a view using a fade-out animation. */
+ public static void hideViewAnimated(@NonNull View view, int duration) {
+ hideViewAnimated(view, duration, null);
+ }
+
/** Returns an AnimatorListener that hides the view at the end. */
public static Animator.AnimatorListener hideViewAfterAnimation(View view) {
return new AnimatorListenerAdapter() {
@@ -79,20 +114,26 @@
* @param duration animation duration in milliseconds.
*/
public static void hideViewsAnimated(@Nullable List<View> views, int duration) {
+ if (views == null) {
+ return;
+ }
for (View view : views) {
if (view != null) {
- hideViewAnimated(view, duration);
+ hideViewAnimated(view, duration, null);
}
}
}
/**
- * Shows a view using a fade-in animation
+ * Shows a view using a fade-in animation. The view's alpha value isn't changed so that
+ * animating an already visible won't have a visible effect. Therefore, <b>views initialized as
+ * hidden must have their alpha set to 0 prior to calling this method</b>.
*
* @param view {@link View} to be shown
* @param duration animation duration in milliseconds.
*/
- public static void showViewAnimated(@NonNull View view, int duration) {
+ public static void showViewAnimated(@NonNull View view, int duration,
+ @Nullable ViewAnimEndListener listener) {
// Cancel existing animation to avoid race condition
// if show and hide are called at the same time
view.animate().cancel();
@@ -107,10 +148,21 @@
public void onAnimationStart(Animator animation) {
view.setVisibility(VISIBLE);
}
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (listener != null) {
+ listener.onAnimationEnd(view);
+ }
+ }
})
.alpha(1f);
}
+ /** Shows a view using a fade-in animation. */
+ public static void showViewAnimated(@NonNull View view, int duration) {
+ showViewAnimated(view, duration, null);
+ }
+
/**
* Shows views using a fade-out animation
*
@@ -120,7 +172,7 @@
public static void showViewsAnimated(@Nullable List<View> views, int duration) {
for (View view : views) {
if (view != null) {
- showViewAnimated(view, duration);
+ showViewAnimated(view, duration, null);
}
}
}
@@ -207,6 +259,16 @@
return views;
}
+ /** Removes the view from its parent (if it has one). */
+ public static void removeFromParent(@Nullable View view) {
+ if (view != null) {
+ ViewGroup parent = (ViewGroup) view.getParent();
+ if (parent != null) {
+ parent.removeView(view);
+ }
+ }
+ }
+
/** Adds the {@code view} into the {@code container}. */
public static void setView(@Nullable View view, FrameLayout container) {
if (view != null) {
@@ -215,13 +277,11 @@
return;
}
- ViewGroup parent = (ViewGroup) view.getParent();
// As we are removing views (on BT disconnect, for example), some items will be
// shifting from expanded to collapsed (like Queue item) - remove those from the
// group before adding to the new slot
- if (view.getParent() != null) {
- parent.removeView(view);
- }
+ removeFromParent(view);
+
container.removeAllViews();
container.addView(view);
container.setVisibility(VISIBLE);
diff --git a/car-arch-common/src/com/android/car/arch/common/FutureData.java b/car-arch-common/src/com/android/car/arch/common/FutureData.java
index d2ef248..9b5c41b 100644
--- a/car-arch-common/src/com/android/car/arch/common/FutureData.java
+++ b/car-arch-common/src/com/android/car/arch/common/FutureData.java
@@ -17,18 +17,44 @@
package com.android.car.arch.common;
/**
- * Class that holds data with a loading state.
+ * Class that holds data with a loading state, and optionally the previous version of the data.
*
* @param <T> the output data type
*/
public class FutureData<T> {
private final boolean mIsLoading;
+ private final T mPastData;
private final T mData;
+ /** Returns an instance with a null data value and loading set to true. */
+ public static <T> FutureData<T> newLoadingData() {
+ return new FutureData<>(true, null);
+ }
+
+ /** Returns a loaded instance with the given data value. */
+ public static <T> FutureData<T> newLoadedData(T data) {
+ return new FutureData<>(false, data);
+ }
+
+ /** Returns a loaded instance with the given previous and current data values. */
+ public static <T> FutureData<T> newLoadedData(T oldData, T newData) {
+ return new FutureData<>(false, oldData, newData);
+ }
+
+ /**
+ * This should become private.
+ * @deprecated Use {@link #newLoadingData}, and {@link #newLoadedData} instead.
+ */
+ @Deprecated
public FutureData(boolean isLoading, T data) {
+ this(isLoading, null, data);
+ }
+
+ private FutureData(boolean isLoading, T oldData, T newData) {
mIsLoading = isLoading;
- mData = data;
+ mPastData = oldData;
+ mData = newData;
}
/**
@@ -44,4 +70,9 @@
public T getData() {
return mData;
}
+
+ /** If done loading, returns the previous version of the data, otherwise null. */
+ public T getPastData() {
+ return mPastData;
+ }
}
diff --git a/car-assist-client-lib/src/com/android/car/assist/client/CarAssistUtils.java b/car-assist-client-lib/src/com/android/car/assist/client/CarAssistUtils.java
index 1f9d915..f10d701 100644
--- a/car-assist-client-lib/src/com/android/car/assist/client/CarAssistUtils.java
+++ b/car-assist-client-lib/src/com/android/car/assist/client/CarAssistUtils.java
@@ -69,6 +69,7 @@
private final Context mContext;
private final AssistUtils mAssistUtils;
+ @Nullable
private final FallbackAssistant mFallbackAssistant;
private final String mErrorMessage;
private final boolean mIsFallbackAssistantEnabled;
@@ -106,10 +107,11 @@
public CarAssistUtils(Context context) {
mContext = context;
mAssistUtils = new AssistUtils(context);
- mFallbackAssistant = new FallbackAssistant(context);
mErrorMessage = context.getString(R.string.assist_action_failed_toast);
+
mIsFallbackAssistantEnabled =
context.getResources().getBoolean(R.bool.config_enableFallbackAssistant);
+ mFallbackAssistant = mIsFallbackAssistantEnabled ? new FallbackAssistant(context) : null;
}
/**
@@ -120,6 +122,13 @@
}
/**
+ * Returns {@code true} if the fallback assistant is enabled.
+ */
+ public boolean isFallbackAssistantEnabled() {
+ return mIsFallbackAssistantEnabled;
+ }
+
+ /**
* Returns true if the current active assistant has notification listener permissions.
*/
public boolean assistantIsNotificationListener() {
@@ -384,6 +393,10 @@
private void handleFallback(StatusBarNotification sbn, String action,
ActionRequestCallback callback) {
+ if (mFallbackAssistant == null) {
+ return;
+ }
+
FallbackAssistant.Listener listener = new FallbackAssistant.Listener() {
@Override
public void onMessageRead(boolean hasError) {
diff --git a/car-media-common/res/values/bools.xml b/car-media-common/res/values/bools.xml
index 85fce20..fee9fec 100644
--- a/car-media-common/res/values/bools.xml
+++ b/car-media-common/res/values/bools.xml
@@ -22,4 +22,9 @@
<!-- Whether to show a circular progress bar in control bar and minimized control bar or not. -->
<bool name="show_circular_progress_bar">false</bool>
+ <!-- Whether the media source logo should be used for the app selector button. If the flag is
+ set to 'true', the main logo (which by default appears on the left hand side on toolbar)
+ will be hidden. -->
+ <bool name="use_media_source_logo_for_app_selector">false</bool>
+
</resources>
diff --git a/car-media-common/src/com/android/car/media/common/PlaybackFragment.java b/car-media-common/src/com/android/car/media/common/PlaybackFragment.java
index 60f4d18..d9ff4b1 100644
--- a/car-media-common/src/com/android/car/media/common/PlaybackFragment.java
+++ b/car-media-common/src/com/android/car/media/common/PlaybackFragment.java
@@ -51,8 +51,8 @@
import com.android.car.apps.common.util.CarPackageManagerUtils;
import com.android.car.apps.common.util.ViewUtils;
import com.android.car.arch.common.FutureData;
-import com.android.car.media.common.browse.BrowsedMediaItems;
-import com.android.car.media.common.browse.MediaBrowserViewModel;
+import com.android.car.media.common.browse.MediaBrowserViewModelImpl;
+import com.android.car.media.common.browse.MediaItemsRepository;
import com.android.car.media.common.playback.PlaybackViewModel;
import com.android.car.media.common.playback.PlaybackViewModel.PlaybackStateWrapper;
import com.android.car.media.common.source.MediaSource;
@@ -88,14 +88,14 @@
mCar = Car.createCar(activity);
mCarPackageManager = (CarPackageManager) mCar.getCarManager(Car.PACKAGE_SERVICE);
- mPlaybackViewModel = PlaybackViewModel.get(activity.getApplication(),
- MEDIA_SOURCE_MODE_PLAYBACK);
- mMediaSourceViewModel = MediaSourceViewModel.get(activity.getApplication(),
- MEDIA_SOURCE_MODE_PLAYBACK);
+ Application application = activity.getApplication();
+ mPlaybackViewModel = PlaybackViewModel.get(application, MEDIA_SOURCE_MODE_PLAYBACK);
+ mMediaSourceViewModel = MediaSourceViewModel.get(application, MEDIA_SOURCE_MODE_PLAYBACK);
mAppSelectorIntent = MediaSource.getSourceSelectorIntent(getContext(), true);
mInnerViewModel = ViewModelProviders.of(activity).get(ViewModel.class);
- mInnerViewModel.init(activity, mMediaSourceViewModel, mPlaybackViewModel);
+ mInnerViewModel.init(activity, mMediaSourceViewModel, mPlaybackViewModel,
+ MediaItemsRepository.get(application, MEDIA_SOURCE_MODE_PLAYBACK));
View view = inflater.inflate(R.layout.playback_fragment, container, false);
@@ -121,8 +121,22 @@
TextView subtitle = view.findViewById(R.id.subtitle);
mInnerViewModel.getSubtitle().observe(getViewLifecycleOwner(), subtitle::setText);
+ boolean useSourceLogoForAppSelector =
+ getResources().getBoolean(R.bool.use_media_source_logo_for_app_selector);
+
ImageView appIcon = view.findViewById(R.id.app_icon);
- mInnerViewModel.getAppIcon().observe(getViewLifecycleOwner(), appIcon::setImageBitmap);
+ View appSelector = view.findViewById(R.id.app_selector_container);
+ mInnerViewModel.getAppIcon().observe(getViewLifecycleOwner(), bitmap -> {
+ if (useSourceLogoForAppSelector) {
+ ImageView appSelectorIcon = appSelector.findViewById(R.id.app_selector);
+ appSelectorIcon.setImageBitmap(bitmap);
+ appSelectorIcon.setImageTintList(null);
+
+ appIcon.setVisibility(View.GONE);
+ } else {
+ appIcon.setImageBitmap(bitmap);
+ }
+ });
mInnerViewModel.getBrowseTreeHasChildren().observe(getViewLifecycleOwner(),
this::onBrowseTreeHasChildrenChanged);
@@ -152,11 +166,9 @@
mPlaybackViewModel.getMetadata().observe(getViewLifecycleOwner(),
item -> mAlbumArtBinder.setImage(PlaybackFragment.this.getContext(),
item != null ? item.getArtworkKey() : null));
- View appSelector = view.findViewById(R.id.app_selector_container);
appSelector.setVisibility(mAppSelectorIntent != null ? View.VISIBLE : View.GONE);
appSelector.setOnClickListener(e -> getContext().startActivity(mAppSelectorIntent));
-
mErrorsHelper = new PlaybackErrorsHelper(activity) {
@Override
@@ -214,32 +226,32 @@
private LiveData<CharSequence> mSubtitle;
private MutableLiveData<Boolean> mBrowseTreeHasChildren = new MutableLiveData<>();
+ private MediaItemsRepository mMediaItemsRepository;
private PlaybackViewModel mPlaybackViewModel;
private MediaSourceViewModel mMediaSourceViewModel;
- private MediaBrowserViewModel mRootMediaBrowserViewModel;
public ViewModel(Application application) {
super(application);
}
void init(FragmentActivity activity, MediaSourceViewModel mediaSourceViewModel,
- PlaybackViewModel playbackViewModel) {
+ PlaybackViewModel playbackViewModel, MediaItemsRepository mediaItemsRepository) {
if (mMediaSourceViewModel == mediaSourceViewModel
- && mPlaybackViewModel == playbackViewModel) {
+ && mPlaybackViewModel == playbackViewModel
+ && mMediaItemsRepository == mediaItemsRepository) {
return;
}
mPlaybackViewModel = playbackViewModel;
mMediaSourceViewModel = mediaSourceViewModel;
+ mMediaItemsRepository = mediaItemsRepository;
mMediaSource = mMediaSourceViewModel.getPrimaryMediaSource();
mAppName = mapNonNull(mMediaSource, MediaSource::getDisplayName);
mAppIcon = mapNonNull(mMediaSource, MediaSource::getCroppedPackageIcon);
mTitle = mapNonNull(playbackViewModel.getMetadata(), MediaItemMetadata::getTitle);
mSubtitle = mapNonNull(playbackViewModel.getMetadata(), MediaItemMetadata::getArtist);
- mRootMediaBrowserViewModel = MediaBrowserViewModel.Factory.getInstanceForBrowseRoot(
- mMediaSourceViewModel, ViewModelProviders.of(activity));
- mRootMediaBrowserViewModel.getBrowsedMediaItems()
- .observe(activity, this::onItemsUpdate);
+ mMediaItemsRepository.getRootMediaItems()
+ .observe(activity, this::onRootMediaItemsUpdate);
}
LiveData<CharSequence> getAppName() {
@@ -262,14 +274,14 @@
return mBrowseTreeHasChildren;
}
- private void onItemsUpdate(FutureData<List<MediaItemMetadata>> futureData) {
- if (futureData.isLoading()) {
+ private void onRootMediaItemsUpdate(FutureData<List<MediaItemMetadata>> data) {
+ if (data.isLoading()) {
mBrowseTreeHasChildren.setValue(null);
return;
}
List<MediaItemMetadata> items =
- BrowsedMediaItems.filterItems(/*forRoot*/ true, futureData.getData());
+ MediaBrowserViewModelImpl.filterItems(/*forRoot*/ true, data.getData());
boolean browseTreeHasChildren = items != null && !items.isEmpty();
mBrowseTreeHasChildren.setValue(browseTreeHasChildren);
diff --git a/car-media-common/src/com/android/car/media/common/browse/BrowsedMediaItems.java b/car-media-common/src/com/android/car/media/common/browse/BrowsedMediaItems.java
deleted file mode 100644
index c9a5864..0000000
--- a/car-media-common/src/com/android/car/media/common/browse/BrowsedMediaItems.java
+++ /dev/null
@@ -1,189 +0,0 @@
-/*
- * Copyright 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.media.common.browse;
-
-import android.os.Bundle;
-import android.os.Handler;
-import android.support.v4.media.MediaBrowserCompat;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.lifecycle.LiveData;
-
-import com.android.car.media.common.MediaItemMetadata;
-
-import java.util.List;
-import java.util.Objects;
-import java.util.function.Predicate;
-import java.util.stream.Collectors;
-
-/**
- * A LiveData that provides access the a MediaBrowser's children
- */
-
-public class BrowsedMediaItems extends LiveData<List<MediaItemMetadata>> {
-
- /**
- * Number of times we will retry obtaining the list of children of a certain node
- */
- private static final int CHILDREN_SUBSCRIPTION_RETRIES = 1;
- /**
- * Time between retries while trying to obtain the list of children of a certain node
- */
- private static final int CHILDREN_SUBSCRIPTION_RETRY_TIME_MS = 5000;
-
- /** Whether to send an error after the last timeout. The subscription stays active regardless.*/
- private static final boolean LAST_RETRY_TIMEOUT_SENDS_ERROR = false;
-
- private final MediaBrowserCompat mBrowser;
- private final String mParentId;
- private final Handler mHandler = new Handler();
-
- private ChildrenSubscription mSubscription;
-
- BrowsedMediaItems(@NonNull MediaBrowserCompat mediaBrowser, @NonNull String parentId) {
- mBrowser = mediaBrowser;
- mParentId = parentId;
- }
-
- /**
- * Filters the items that are valid for the root (tabs) or the current node. Returns null when
- * the given list is null to preserve its error signal.
- */
- @Nullable
- public static List<MediaItemMetadata> filterItems(boolean forRoot,
- @Nullable List<MediaItemMetadata> items) {
- if (items == null) return null;
- Predicate<MediaItemMetadata> predicate = forRoot ? MediaItemMetadata::isBrowsable
- : item -> (item.isPlayable() || item.isBrowsable());
- return items.stream().filter(predicate).collect(Collectors.toList());
- }
-
- @Override
- protected void onActive() {
- super.onActive();
- mSubscription = new ChildrenSubscription(mParentId);
- mSubscription.start(CHILDREN_SUBSCRIPTION_RETRIES, CHILDREN_SUBSCRIPTION_RETRY_TIME_MS);
- }
-
- @Override
- protected void onInactive() {
- super.onInactive();
- mSubscription.stop();
- mSubscription = null;
- mHandler.removeCallbacksAndMessages(null);
- }
-
- /**
- * {@link MediaBrowserCompat.SubscriptionCallback} wrapper used to overcome the lack of a
- * reliable method to obtain the initial list of children of a given node.
- * <p>
- * When some 3rd party apps go through configuration changes (i.e., in the case of user-switch),
- * they leave subscriptions in an intermediate state where neither {@link
- * MediaBrowserCompat.SubscriptionCallback#onChildrenLoaded(String, List)} nor {@link
- * MediaBrowserCompat.SubscriptionCallback#onError(String)} are invoked.
- * <p>
- * This wrapper works around this problem by retrying the subscription a given number of times
- * if no data is received after a certain amount of time. This process is started by calling
- * {@link #start(int, int)}, passing the number of retries and delay between them as
- * parameters.
- * TODO: remove all this code if it's indeed not needed anymore (using retry=1 to be sure).
- */
- private class ChildrenSubscription extends MediaBrowserCompat.SubscriptionCallback {
- private final String mItemId;
-
- private boolean mIsDataLoaded;
- private int mRetries;
- private int mRetryDelay;
-
- ChildrenSubscription(String itemId) {
- mItemId = itemId;
- }
-
- private Runnable mRetryRunnable = new Runnable() {
- @Override
- public void run() {
- if (!mIsDataLoaded) {
- if (mRetries > 0) {
- mRetries--;
- mBrowser.unsubscribe(mItemId);
- mBrowser.subscribe(mItemId, ChildrenSubscription.this);
- mHandler.postDelayed(this, mRetryDelay);
- } else if (LAST_RETRY_TIMEOUT_SENDS_ERROR) {
- mIsDataLoaded = true;
- setValue(null);
- }
- }
- }
- };
-
- /**
- * Starts trying to obtain the list of children
- *
- * @param retries number of times to retry. If children are not obtained in this time
- * then the LiveData's value will be set to {@code null}
- * @param retryDelay time between retries in milliseconds
- */
- void start(int retries, int retryDelay) {
- if (mIsDataLoaded) {
- mBrowser.subscribe(mItemId, this);
- } else {
- mRetries = retries;
- mRetryDelay = retryDelay;
- mHandler.post(mRetryRunnable);
- }
- }
-
- /**
- * Stops retrying
- */
- void stop() {
- mHandler.removeCallbacks(mRetryRunnable);
- mBrowser.unsubscribe(mItemId);
- }
-
- @Override
- public void onChildrenLoaded(@NonNull String parentId,
- @NonNull List<MediaBrowserCompat.MediaItem> children) {
- mHandler.removeCallbacks(mRetryRunnable);
- mIsDataLoaded = true;
- setValue(children.stream()
- .filter(Objects::nonNull)
- .map(MediaItemMetadata::new)
- .collect(Collectors.toList()));
- }
-
- @Override
- public void onChildrenLoaded(@NonNull String parentId,
- @NonNull List<MediaBrowserCompat.MediaItem> children,
- @NonNull Bundle options) {
- onChildrenLoaded(parentId, children);
- }
-
- @Override
- public void onError(@NonNull String parentId) {
- mHandler.removeCallbacks(mRetryRunnable);
- mIsDataLoaded = true;
- setValue(null);
- }
-
- @Override
- public void onError(@NonNull String parentId, @NonNull Bundle options) {
- onError(parentId);
- }
- }
-}
diff --git a/car-media-common/src/com/android/car/media/common/browse/MediaBrowserViewModel.java b/car-media-common/src/com/android/car/media/common/browse/MediaBrowserViewModel.java
deleted file mode 100644
index 323b4ce..0000000
--- a/car-media-common/src/com/android/car/media/common/browse/MediaBrowserViewModel.java
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * Copyright 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.media.common.browse;
-
-import android.support.v4.media.MediaBrowserCompat;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
-import androidx.lifecycle.LiveData;
-import androidx.lifecycle.ViewModelProvider;
-
-import com.android.car.arch.common.FutureData;
-import com.android.car.media.common.MediaItemMetadata;
-import com.android.car.media.common.source.MediaSourceViewModel;
-
-import java.util.List;
-
-/**
- * Contains observable data needed for displaying playback and browse UI. Instances can be obtained
- * via {@link MediaBrowserViewModel.Factory}
- */
-public interface MediaBrowserViewModel {
-
- /**
- * Returns a LiveData that emits the current package name of the browser's service component.
- */
- LiveData<String> getPackageName();
-
- /**
- * Fetches the MediaItemMetadatas for the current browsed id, and the loading status of the
- * fetch operation.
- *
- * This LiveData will never emit {@code null}. If the data is loading, the data component of the
- * {@link FutureData} will be null
- * A MediaSource must be selected and its MediaBrowser connected, otherwise the FutureData will
- * always contain a {@code null} data value.
- *
- * @return a LiveData that emits a FutureData that contains the loading status and the
- * MediaItemMetadatas for the current browsed id
- */
- LiveData<FutureData<List<MediaItemMetadata>>> getBrowsedMediaItems();
-
- /**
- * Fetches the MediaItemMetadatas for the current search query, and the loading status of the
- * fetch operation.
- *
- * See {@link #getBrowsedMediaItems()}
- */
- LiveData<FutureData<List<MediaItemMetadata>>> getSearchedMediaItems();
-
-
- /**
- * Returns a LiveData that emits whether the media browser supports search. This wil never emit
- * {@code null}
- */
- LiveData<Boolean> supportsSearch();
-
- /**
- * Gets the content style display type of browsable elements in this media browser, set at the
- * browse root
- */
- LiveData<Integer> rootBrowsableHint();
-
- /**
- * Gets the content style display type of playable elements in this media browser, set at the
- * browse root
- */
- LiveData<Integer> rootPlayableHint();
-
- /**
- * A {@link MediaBrowserViewModel} whose selected browse ID may be changed.
- */
- interface WithMutableBrowseId extends MediaBrowserViewModel {
-
- /**
- * Set the current item to be browsed. If available, the list of items will be emitted by
- * {@link #getBrowsedMediaItems()}.
- */
- @UiThread
- void setCurrentBrowseId(@NonNull String browseId);
-
- /**
- * Set the current item to be searched for. If available, the list of items will be emitted
- * by {@link #getBrowsedMediaItems()}.
- */
- @UiThread
- void search(@Nullable String query);
- }
-
- /**
- * Creates and/or fetches {@link MediaBrowserViewModel} instances.
- */
- class Factory {
-
- private static final String KEY_BROWSER_ROOT =
- "com.android.car.media.common.browse.MediaBrowserViewModel.Factory.browserRoot";
-
- /**
- * Returns an initialized {@link MediaBrowserViewModel.WithMutableBrowseId} with the
- * provided connected media browser. The provided {@code mediaBrowser} does not need to be
- * from the same scope as {@code viewModelProvider}.
- */
- @NonNull
- public static MediaBrowserViewModel.WithMutableBrowseId getInstanceWithMediaBrowser(
- @NonNull String key,
- @NonNull ViewModelProvider viewModelProvider,
- @NonNull LiveData<MediaBrowserCompat> mediaBrowser) {
- MutableMediaBrowserViewModel viewModel =
- viewModelProvider.get(key, MutableMediaBrowserViewModel.class);
- initMediaBrowser(mediaBrowser, viewModel);
- return viewModel;
- }
-
- /**
- * Fetch an initialized {@link MediaBrowserViewModel}. It will get its media browser from
- * the {@link MediaSourceViewModel} provided by {@code viewModelProvider}. It will already
- * be configured to browse the root of the browser.
- *
- * @param mediaSourceVM the {@link MediaSourceViewModel} singleton.
- * @param viewModelProvider the ViewModelProvider to load ViewModels from.
- * @return an initialized MediaBrowserViewModel configured to browse the specified browseId.
- */
- @NonNull
- public static MediaBrowserViewModel getInstanceForBrowseRoot(
- MediaSourceViewModel mediaSourceVM, @NonNull ViewModelProvider viewModelProvider) {
- RootMediaBrowserViewModel viewModel =
- viewModelProvider.get(KEY_BROWSER_ROOT, RootMediaBrowserViewModel.class);
- initMediaBrowser(mediaSourceVM.getConnectedMediaBrowser(), viewModel);
- return viewModel;
- }
-
- private static void initMediaBrowser(
- @NonNull LiveData<MediaBrowserCompat> connectedMediaBrowser,
- MediaBrowserViewModelImpl viewModel) {
- if (viewModel.getMediaBrowserSource() != connectedMediaBrowser) {
- viewModel.setConnectedMediaBrowser(connectedMediaBrowser);
- }
- }
- }
-}
diff --git a/car-media-common/src/com/android/car/media/common/browse/MediaBrowserViewModelImpl.java b/car-media-common/src/com/android/car/media/common/browse/MediaBrowserViewModelImpl.java
index 949c81d..d740667 100644
--- a/car-media-common/src/com/android/car/media/common/browse/MediaBrowserViewModelImpl.java
+++ b/car-media-common/src/com/android/car/media/common/browse/MediaBrowserViewModelImpl.java
@@ -16,177 +16,124 @@
package com.android.car.media.common.browse;
-import static androidx.lifecycle.Transformations.map;
-
-import static com.android.car.arch.common.LiveDataFunctions.dataOf;
-import static com.android.car.arch.common.LiveDataFunctions.loadingSwitchMap;
-import static com.android.car.arch.common.LiveDataFunctions.pair;
-import static com.android.car.arch.common.LiveDataFunctions.split;
-
-import android.app.Application;
import android.os.Bundle;
import android.support.v4.media.MediaBrowserCompat;
-import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
-import androidx.lifecycle.AndroidViewModel;
-import androidx.lifecycle.LiveData;
-import androidx.lifecycle.MutableLiveData;
-import com.android.car.arch.common.FutureData;
-import com.android.car.arch.common.switching.SwitchingLiveData;
import com.android.car.media.common.MediaConstants;
import com.android.car.media.common.MediaItemMetadata;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
/**
- * Contains observable data needed for displaying playback and browse/search UI. Instances can be
- * obtained via {@link MediaBrowserViewModel.Factory}
+ * TODO: rename to MediaBrowserUtils.
+ * Provides utility methods for {@link MediaBrowserCompat}.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-class MediaBrowserViewModelImpl extends AndroidViewModel implements MediaBrowserViewModel {
+public class MediaBrowserViewModelImpl {
- private final boolean mIsRoot;
-
- private final SwitchingLiveData<MediaBrowserCompat> mMediaBrowserSwitch =
- SwitchingLiveData.newInstance();
-
- final MutableLiveData<String> mCurrentBrowseId = dataOf(null);
- final MutableLiveData<String> mCurrentSearchQuery = dataOf(null);
- private final LiveData<MediaBrowserCompat> mConnectedMediaBrowser =
- map(mMediaBrowserSwitch.asLiveData(), MediaBrowserViewModelImpl::requireConnected);
-
- private final LiveData<FutureData<List<MediaItemMetadata>>> mSearchedMediaItems;
- private final LiveData<FutureData<List<MediaItemMetadata>>> mBrowsedMediaItems;
- private final LiveData<String> mPackageName;
-
- MediaBrowserViewModelImpl(@NonNull Application application, boolean isRoot) {
- super(application);
-
- mIsRoot = isRoot;
-
- mPackageName = map(mConnectedMediaBrowser,
- mediaBrowser -> {
- if (mediaBrowser == null) return null;
- return mediaBrowser.getServiceComponent().getPackageName();
- });
-
- mBrowsedMediaItems =
- loadingSwitchMap(pair(mConnectedMediaBrowser, mCurrentBrowseId),
- split((mediaBrowser, browseId) -> {
- if (mediaBrowser == null || (!mIsRoot && browseId == null)) {
- return null;
- }
-
- String parentId = (mIsRoot) ? mediaBrowser.getRoot() : browseId;
- return new BrowsedMediaItems(mediaBrowser, parentId);
- }));
- mSearchedMediaItems =
- loadingSwitchMap(pair(mConnectedMediaBrowser, mCurrentSearchQuery),
- split((mediaBrowser, query) ->
- (mediaBrowser == null || TextUtils.isEmpty(query))
- ? null
- : new SearchedMediaItems(mediaBrowser, query)));
- }
-
- private static MediaBrowserCompat requireConnected(@Nullable MediaBrowserCompat mediaBrowser) {
- if (mediaBrowser != null && !mediaBrowser.isConnected()) {
- throw new IllegalStateException(
- "Only connected MediaBrowsers may be provided to MediaBrowserViewModel.");
- }
- return mediaBrowser;
+ private MediaBrowserViewModelImpl() {
}
/**
- * Set the source {@link MediaBrowserCompat} to use for browsing. If {@code mediaBrowser} emits
- * non-null, the MediaBrowser emitted must already be in a connected state.
+ * Filters the items that are valid for the root (tabs) or the current node. Returns null when
+ * the given list is null to preserve its error signal.
*/
- void setConnectedMediaBrowser(@Nullable LiveData<MediaBrowserCompat> mediaBrowser) {
- mMediaBrowserSwitch.setSource(mediaBrowser);
+ @Nullable
+ public static List<MediaItemMetadata> filterItems(boolean forRoot,
+ @Nullable List<MediaItemMetadata> items) {
+ if (items == null) return null;
+ Predicate<MediaItemMetadata> predicate = forRoot ? MediaItemMetadata::isBrowsable
+ : item -> (item.isPlayable() || item.isBrowsable());
+ return items.stream().filter(predicate).collect(Collectors.toList());
}
- LiveData<? extends MediaBrowserCompat> getMediaBrowserSource() {
- return mMediaBrowserSwitch.getSource();
+ /** Returns only the browse-able items from the given list. */
+ @Nullable
+ public static List<MediaItemMetadata> selectBrowseableItems(
+ @Nullable List<MediaItemMetadata> items) {
+ if (items == null) return null;
+ Predicate<MediaItemMetadata> predicate = MediaItemMetadata::isBrowsable;
+ return items.stream().filter(predicate).collect(Collectors.toList());
}
- @Override
- public LiveData<String> getPackageName() {
- return mPackageName;
- }
-
- @Override
- public LiveData<FutureData<List<MediaItemMetadata>>> getBrowsedMediaItems() {
- return mBrowsedMediaItems;
- }
-
- @Override
- public LiveData<FutureData<List<MediaItemMetadata>>> getSearchedMediaItems() {
- return mSearchedMediaItems;
- }
@SuppressWarnings("deprecation")
- @Override
- public LiveData<Boolean> supportsSearch() {
- return map(mConnectedMediaBrowser, mediaBrowserCompat -> {
- if (mediaBrowserCompat == null) {
- return false;
- }
- Bundle extras = mediaBrowserCompat.getExtras();
- if (extras == null) {
- return false;
- }
- if (extras.containsKey(MediaConstants.MEDIA_SEARCH_SUPPORTED)) {
- return extras.getBoolean(MediaConstants.MEDIA_SEARCH_SUPPORTED);
- }
- if (extras.containsKey(MediaConstants.MEDIA_SEARCH_SUPPORTED_PRERELEASE)) {
- return extras.getBoolean(MediaConstants.MEDIA_SEARCH_SUPPORTED_PRERELEASE);
- }
+ public static boolean getSupportsSearch(@Nullable MediaBrowserCompat mediaBrowserCompat) {
+ if (mediaBrowserCompat == null) {
return false;
- });
+ }
+ Bundle extras = mediaBrowserCompat.getExtras();
+ if (extras == null) {
+ return false;
+ }
+ if (extras.containsKey(MediaConstants.MEDIA_SEARCH_SUPPORTED)) {
+ return extras.getBoolean(MediaConstants.MEDIA_SEARCH_SUPPORTED);
+ }
+ if (extras.containsKey(MediaConstants.MEDIA_SEARCH_SUPPORTED_PRERELEASE)) {
+ return extras.getBoolean(MediaConstants.MEDIA_SEARCH_SUPPORTED_PRERELEASE);
+ }
+ return false;
}
@SuppressWarnings("deprecation")
- @Override
- public LiveData<Integer> rootBrowsableHint() {
- return map(mConnectedMediaBrowser, mediaBrowserCompat -> {
- if (mediaBrowserCompat == null) {
- return 0;
- }
- Bundle extras = mediaBrowserCompat.getExtras();
- if (extras == null) {
- return 0;
- }
- if (extras.containsKey(MediaConstants.CONTENT_STYLE_BROWSABLE_HINT)) {
- return extras.getInt(MediaConstants.CONTENT_STYLE_BROWSABLE_HINT, 0);
- }
- if (extras.containsKey(MediaConstants.CONTENT_STYLE_BROWSABLE_HINT_PRERELEASE)) {
- return extras.getInt(MediaConstants.CONTENT_STYLE_BROWSABLE_HINT_PRERELEASE, 0);
- }
+ public static int getRootBrowsableHint(@Nullable MediaBrowserCompat mediaBrowserCompat) {
+ if (mediaBrowserCompat == null) {
return 0;
- });
+ }
+ Bundle extras = mediaBrowserCompat.getExtras();
+ if (extras == null) {
+ return 0;
+ }
+ if (extras.containsKey(MediaConstants.CONTENT_STYLE_BROWSABLE_HINT)) {
+ return extras.getInt(MediaConstants.CONTENT_STYLE_BROWSABLE_HINT, 0);
+ }
+ if (extras.containsKey(MediaConstants.CONTENT_STYLE_BROWSABLE_HINT_PRERELEASE)) {
+ return extras.getInt(MediaConstants.CONTENT_STYLE_BROWSABLE_HINT_PRERELEASE, 0);
+ }
+ return 0;
}
@SuppressWarnings("deprecation")
- @Override
- public LiveData<Integer> rootPlayableHint() {
- return map(mConnectedMediaBrowser, mediaBrowserCompat -> {
- if (mediaBrowserCompat == null) {
- return 0;
- }
- Bundle extras = mediaBrowserCompat.getExtras();
- if (extras == null) {
- return 0;
- }
- if (extras.containsKey(MediaConstants.CONTENT_STYLE_PLAYABLE_HINT)) {
- return extras.getInt(MediaConstants.CONTENT_STYLE_PLAYABLE_HINT, 0);
- }
- if (extras.containsKey(MediaConstants.CONTENT_STYLE_PLAYABLE_HINT_PRERELEASE)) {
- return extras.getInt(MediaConstants.CONTENT_STYLE_PLAYABLE_HINT_PRERELEASE, 0);
- }
+ public static int getRootPlayableHint(@Nullable MediaBrowserCompat mediaBrowserCompat) {
+ if (mediaBrowserCompat == null) {
return 0;
- });
+ }
+ Bundle extras = mediaBrowserCompat.getExtras();
+ if (extras == null) {
+ return 0;
+ }
+ if (extras.containsKey(MediaConstants.CONTENT_STYLE_PLAYABLE_HINT)) {
+ return extras.getInt(MediaConstants.CONTENT_STYLE_PLAYABLE_HINT, 0);
+ }
+ if (extras.containsKey(MediaConstants.CONTENT_STYLE_PLAYABLE_HINT_PRERELEASE)) {
+ return extras.getInt(MediaConstants.CONTENT_STYLE_PLAYABLE_HINT_PRERELEASE, 0);
+ }
+ return 0;
+ }
+
+ /** Returns the elements of oldList that do NOT appear in newList. */
+ public static @NonNull Collection<MediaItemMetadata> computeRemovedItems(
+ @Nullable List<MediaItemMetadata> oldList, @Nullable List<MediaItemMetadata> newList) {
+ if (oldList == null || oldList.isEmpty()) {
+ // Nothing was removed
+ return Collections.emptyList();
+ }
+
+ if (newList == null || newList.isEmpty()) {
+ // Everything was removed
+ return new ArrayList<>(oldList);
+ }
+
+ HashSet<MediaItemMetadata> itemsById = new HashSet<>(oldList);
+ itemsById.removeAll(newList);
+ return itemsById;
}
}
diff --git a/car-media-common/src/com/android/car/media/common/browse/MediaItemsRepository.java b/car-media-common/src/com/android/car/media/common/browse/MediaItemsRepository.java
new file mode 100644
index 0000000..e01cb96
--- /dev/null
+++ b/car-media-common/src/com/android/car/media/common/browse/MediaItemsRepository.java
@@ -0,0 +1,310 @@
+/*
+ * Copyright (C) 2020 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.common.browse;
+
+import static com.android.car.arch.common.LiveDataFunctions.dataOf;
+
+import static java.util.stream.Collectors.toList;
+
+import android.app.Application;
+import android.os.Bundle;
+import android.support.v4.media.MediaBrowserCompat;
+import android.support.v4.media.MediaBrowserCompat.SearchCallback;
+import android.support.v4.media.MediaBrowserCompat.SubscriptionCallback;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import com.android.car.arch.common.FutureData;
+import com.android.car.media.common.MediaItemMetadata;
+import com.android.car.media.common.source.MediaBrowserConnector.BrowsingState;
+import com.android.car.media.common.source.MediaSource;
+import com.android.car.media.common.source.MediaSourceViewModel;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+
+/**
+ * Fulfills media items search and children queries. The latter also provides the last list of
+ * results alongside the new list so that differences can be calculated and acted upon.
+ */
+public class MediaItemsRepository {
+ private static final String TAG = "MediaItemsRepository";
+
+ /** One instance per MEDIA_SOURCE_MODE. */
+ private static MediaItemsRepository[] sInstances = new MediaItemsRepository[2];
+
+ /** Returns the MediaItemsRepository "singleton" tied to the application for the given mode. */
+ public static MediaItemsRepository get(@NonNull Application application, int mode) {
+ if (sInstances[mode] == null) {
+ sInstances[mode] = new MediaItemsRepository(
+ MediaSourceViewModel.get(application, mode).getBrowsingState());
+ }
+ return sInstances[mode];
+ }
+
+ /** The live data providing the updates for a query. */
+ public static class MediaItemsLiveData
+ extends LiveData<FutureData<List<MediaItemMetadata>>> {
+
+ private MediaItemsLiveData() {
+ this(true);
+ }
+
+ private MediaItemsLiveData(boolean initAsLoading) {
+ if (initAsLoading) {
+ setLoading();
+ } else {
+ clear();
+ }
+ }
+
+ private void onDataLoaded(List<MediaItemMetadata> old, List<MediaItemMetadata> list) {
+ setValue(FutureData.newLoadedData(old, list));
+ }
+
+ private void setLoading() {
+ setValue(FutureData.newLoadingData());
+ }
+
+ private void clear() {
+ setValue(null);
+ }
+ }
+
+ private static class MediaChildren {
+ final String mNodeId;
+ final MediaItemsLiveData mLiveData = new MediaItemsLiveData();
+ List<MediaItemMetadata> mPreviousValue = Collections.emptyList();
+
+ MediaChildren(String nodeId) {
+ mNodeId = nodeId;
+ }
+ }
+
+ private static class PerMediaSourceCache {
+ String mRootId;
+ Map<String, MediaChildren> mChildrenByNodeId = new HashMap<>();
+ }
+
+ private BrowsingState mBrowsingState;
+ private final Map<MediaSource, PerMediaSourceCache> mCaches = new HashMap<>();
+ private final MutableLiveData<BrowsingState> mBrowsingStateLiveData = dataOf(null);
+ private final MediaItemsLiveData mRootMediaItems = new MediaItemsLiveData();
+ private final MediaItemsLiveData mSearchMediaItems = new MediaItemsLiveData(/*loading*/ false);
+
+ private String mSearchQuery;
+
+ @VisibleForTesting
+ public MediaItemsRepository(LiveData<BrowsingState> browsingState) {
+ browsingState.observeForever(this::onMediaBrowsingStateChanged);
+ }
+
+ /**
+ * Rebroadcasts browsing state changes before the repository takes any action on them.
+ */
+ public LiveData<BrowsingState> getBrowsingState() {
+ return mBrowsingStateLiveData;
+ }
+
+ /**
+ * Convenience wrapper for root media items. The live data is the same instance for all
+ * media sources.
+ */
+ public MediaItemsLiveData getRootMediaItems() {
+ return mRootMediaItems;
+ }
+
+ /**
+ * Returns the results from the current search query. The live data is the same instance
+ * for all media sources.
+ */
+ public MediaItemsLiveData getSearchMediaItems() {
+ return mSearchMediaItems;
+ }
+
+ /** Returns the children of the given node. */
+ public MediaItemsLiveData getMediaChildren(String nodeId) {
+ PerMediaSourceCache cache = getCache();
+ MediaChildren items = cache.mChildrenByNodeId.get(nodeId);
+ if (items == null) {
+ items = new MediaChildren(nodeId);
+ cache.mChildrenByNodeId.put(nodeId, items);
+ }
+
+ // Always refresh the subscription (to work around bugs in media apps).
+ mBrowsingState.mBrowser.unsubscribe(nodeId);
+ mBrowsingState.mBrowser.subscribe(nodeId, mBrowseCallback);
+
+ return items.mLiveData;
+ }
+
+ /** Sets the search query. Results will be given through {@link #getSearchMediaItems}. */
+ public void setSearchQuery(String query) {
+ mSearchQuery = query;
+ if (TextUtils.isEmpty(mSearchQuery)) {
+ clearSearchResults();
+ } else {
+ mSearchMediaItems.setLoading();
+ mBrowsingState.mBrowser.search(mSearchQuery, null, mSearchCallback);
+ }
+ }
+
+ private void clearSearchResults() {
+ mSearchMediaItems.clear();
+ }
+
+ private MediaSource getMediaSource() {
+ return (mBrowsingState != null) ? mBrowsingState.mMediaSource : null;
+ }
+
+ private void onMediaBrowsingStateChanged(BrowsingState newBrowsingState) {
+ mBrowsingState = newBrowsingState;
+ if (mBrowsingState == null) {
+ Log.e(TAG, "Null browsing state (no media source!)");
+ return;
+ }
+ mBrowsingStateLiveData.setValue(mBrowsingState);
+ switch (mBrowsingState.mConnectionStatus) {
+ case CONNECTING:
+ mRootMediaItems.setLoading();
+ break;
+ case CONNECTED:
+ String rootId = mBrowsingState.mBrowser.getRoot();
+ getCache().mRootId = rootId;
+ getMediaChildren(rootId);
+ break;
+ case DISCONNECTING:
+ unsubscribeNodes();
+ clearSearchResults();
+ clearNodes();
+ break;
+ case REJECTED:
+ case SUSPENDED:
+ onBrowseData(getCache().mRootId, null);
+ clearSearchResults();
+ clearNodes();
+ }
+ }
+
+ private PerMediaSourceCache getCache() {
+ PerMediaSourceCache cache = mCaches.get(getMediaSource());
+ if (cache == null) {
+ cache = new PerMediaSourceCache();
+ mCaches.put(getMediaSource(), cache);
+ }
+ return cache;
+ }
+
+ /** Does NOT clear the cache. */
+ private void unsubscribeNodes() {
+ PerMediaSourceCache cache = getCache();
+ for (String nodeId : cache.mChildrenByNodeId.keySet()) {
+ mBrowsingState.mBrowser.unsubscribe(nodeId);
+ }
+ }
+
+ /** Does NOT unsubscribe nodes. */
+ private void clearNodes() {
+ PerMediaSourceCache cache = getCache();
+ cache.mChildrenByNodeId.clear();
+ }
+
+ private void onBrowseData(@NonNull String parentId, @Nullable List<MediaItemMetadata> list) {
+ PerMediaSourceCache cache = getCache();
+ MediaChildren children = cache.mChildrenByNodeId.get(parentId);
+ if (children == null) {
+ if (Log.isLoggable(TAG, Log.WARN)) {
+ Log.w(TAG, "Browse parent not in the cache: " + parentId);
+ }
+ return;
+ }
+
+ List<MediaItemMetadata> old = children.mPreviousValue;
+ children.mPreviousValue = list;
+ children.mLiveData.onDataLoaded(old, list);
+
+ if (Objects.equals(parentId, cache.mRootId)) {
+ mRootMediaItems.onDataLoaded(old, list);
+ }
+ }
+
+ private void onSearchData(@Nullable List<MediaItemMetadata> list) {
+ mSearchMediaItems.onDataLoaded(null, list);
+ }
+
+ private final SubscriptionCallback mBrowseCallback = new SubscriptionCallback() {
+ @Override
+ public void onChildrenLoaded(@NonNull String parentId,
+ @NonNull List<MediaBrowserCompat.MediaItem> children) {
+
+ onBrowseData(parentId, children.stream()
+ .filter(Objects::nonNull)
+ .map(MediaItemMetadata::new)
+ .collect(Collectors.toList()));
+ }
+
+ @Override
+ public void onChildrenLoaded(@NonNull String parentId,
+ @NonNull List<MediaBrowserCompat.MediaItem> children,
+ @NonNull Bundle options) {
+ onChildrenLoaded(parentId, children);
+ }
+
+ @Override
+ public void onError(@NonNull String parentId) {
+ onBrowseData(parentId, null);
+ }
+
+ @Override
+ public void onError(@NonNull String parentId, @NonNull Bundle options) {
+ onError(parentId);
+ }
+ };
+
+ private final SearchCallback mSearchCallback = new SearchCallback() {
+ @Override
+ public void onSearchResult(@NonNull String query, Bundle extras,
+ @NonNull List<MediaBrowserCompat.MediaItem> items) {
+ super.onSearchResult(query, extras, items);
+ if (Objects.equals(mSearchQuery, query)) {
+ onSearchData(items.stream()
+ .filter(Objects::nonNull)
+ .map(MediaItemMetadata::new)
+ .collect(toList()));
+ }
+ }
+
+ @Override
+ public void onError(@NonNull String query, Bundle extras) {
+ super.onError(query, extras);
+ if (Objects.equals(mSearchQuery, query)) {
+ onSearchData(null);
+ }
+ }
+ };
+}
diff --git a/car-media-common/src/com/android/car/media/common/browse/MutableMediaBrowserViewModel.java b/car-media-common/src/com/android/car/media/common/browse/MutableMediaBrowserViewModel.java
deleted file mode 100644
index 1290b20..0000000
--- a/car-media-common/src/com/android/car/media/common/browse/MutableMediaBrowserViewModel.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright 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.common.browse;
-
-import android.app.Application;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
-import androidx.annotation.UiThread;
-
-/** This isn't a comment. */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class MutableMediaBrowserViewModel extends MediaBrowserViewModelImpl implements
- MediaBrowserViewModel.WithMutableBrowseId {
- public MutableMediaBrowserViewModel(@NonNull Application application) {
- super(application, /*isRoot*/ false);
- }
-
- @UiThread
- @Override
- public void setCurrentBrowseId(@NonNull String browseId) {
- super.mCurrentBrowseId.setValue(browseId);
- }
-
- @UiThread
- @Override
- public void search(@Nullable String query) {
- super.mCurrentSearchQuery.setValue(query);
- }
-}
diff --git a/car-media-common/src/com/android/car/media/common/browse/RootMediaBrowserViewModel.java b/car-media-common/src/com/android/car/media/common/browse/RootMediaBrowserViewModel.java
deleted file mode 100644
index f1ffc64..0000000
--- a/car-media-common/src/com/android/car/media/common/browse/RootMediaBrowserViewModel.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright 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.common.browse;
-
-import android.app.Application;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-
-/** This isn't a comment. */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class RootMediaBrowserViewModel extends MediaBrowserViewModelImpl {
- public RootMediaBrowserViewModel(@NonNull Application application) {
- super(application, /*isRoot*/ true);
- }
-}
diff --git a/car-media-common/src/com/android/car/media/common/browse/SearchedMediaItems.java b/car-media-common/src/com/android/car/media/common/browse/SearchedMediaItems.java
deleted file mode 100644
index b8c7423..0000000
--- a/car-media-common/src/com/android/car/media/common/browse/SearchedMediaItems.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright 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.media.common.browse;
-
-import static java.util.stream.Collectors.toList;
-
-import android.os.Bundle;
-import android.support.v4.media.MediaBrowserCompat;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.lifecycle.LiveData;
-
-import com.android.car.media.common.MediaItemMetadata;
-
-import java.util.List;
-import java.util.Objects;
-
-/**
- * A LiveData that provides access to a MediaBrowser's search results for a given query
- */
-public class SearchedMediaItems extends LiveData<List<MediaItemMetadata>> {
-
- private final MediaBrowserCompat mBrowser;
- private final String mQuery;
-
- private final MediaBrowserCompat.SearchCallback mCallback =
- new MediaBrowserCompat.SearchCallback() {
- @Override
- public void onSearchResult(@NonNull String query, Bundle extras,
- @NonNull List<MediaBrowserCompat.MediaItem> items) {
- super.onSearchResult(query, extras, items);
- setValue(items.stream()
- .filter(Objects::nonNull)
- .map(MediaItemMetadata::new)
- .collect(toList()));
- }
-
- @Override
- public void onError(@NonNull String query, Bundle extras) {
- super.onError(query, extras);
- setValue(null);
- }
- };
-
- SearchedMediaItems(@NonNull MediaBrowserCompat mediaBrowser, @Nullable String query) {
- mBrowser = mediaBrowser;
- mQuery = query;
- }
-
- @Override
- protected void onActive() {
- super.onActive();
- mBrowser.search(mQuery, null, mCallback);
- }
-}
diff --git a/car-media-common/src/com/android/car/media/common/playback/PlaybackViewModel.java b/car-media-common/src/com/android/car/media/common/playback/PlaybackViewModel.java
index bcb9893..713caa1 100644
--- a/car-media-common/src/com/android/car/media/common/playback/PlaybackViewModel.java
+++ b/car-media-common/src/com/android/car/media/common/playback/PlaybackViewModel.java
@@ -16,8 +16,6 @@
package com.android.car.media.common.playback;
-import static android.car.media.CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK;
-
import static androidx.lifecycle.Transformations.switchMap;
import static com.android.car.arch.common.LiveDataFunctions.dataOf;
@@ -30,6 +28,7 @@
import android.graphics.drawable.Drawable;
import android.media.MediaMetadata;
import android.os.Bundle;
+import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.RatingCompat;
import android.support.v4.media.session.MediaControllerCompat;
@@ -50,6 +49,8 @@
import com.android.car.media.common.MediaConstants;
import com.android.car.media.common.MediaItemMetadata;
import com.android.car.media.common.R;
+import com.android.car.media.common.source.MediaBrowserConnector;
+import com.android.car.media.common.source.MediaBrowserConnector.ConnectionStatus;
import com.android.car.media.common.source.MediaSourceColors;
import com.android.car.media.common.source.MediaSourceViewModel;
@@ -67,7 +68,7 @@
* Observes changes to the provided MediaController to expose playback state and metadata
* observables.
* <p>
- * PlaybackViewModel is a singleton tied to the application to provide a single source of truth.
+ * PlaybackViewModel is a "singleton" tied to the application to provide a single source of truth.
*/
public class PlaybackViewModel extends AndroidViewModel {
private static final String TAG = "PlaybackViewModel";
@@ -78,15 +79,7 @@
private static PlaybackViewModel[] sInstances = new PlaybackViewModel[2];
- /**
- * Returns the PlaybackViewModel singleton tied to the application.
- * @deprecated should use get(Application application, int mode) instead
- */
- public static PlaybackViewModel get(@NonNull Application application) {
- return get(application, MEDIA_SOURCE_MODE_PLAYBACK);
- }
-
- /** Returns the PlaybackViewModel singleton tied to the application. */
+ /** Returns the PlaybackViewModel "singleton" tied to the application for the given mode. */
public static PlaybackViewModel get(@NonNull Application application, int mode) {
if (sInstances[mode] == null) {
sInstances[mode] = new PlaybackViewModel(application, mode);
@@ -119,12 +112,21 @@
*/
public static final int ACTION_PAUSE = 3;
+ /**
+ * Factory for creating dependencies. Can be swapped out for testing.
+ */
+ @VisibleForTesting
+ interface InputFactory {
+ MediaControllerCompat getControllerForBrowser(@NonNull MediaBrowserCompat browser);
+ }
+
+
/** Needs to be a MediaMetadata because the compat class doesn't implement equals... */
private static final MediaMetadata EMPTY_MEDIA_METADATA = new MediaMetadata.Builder().build();
private final MediaControllerCallback mMediaControllerCallback = new MediaControllerCallback();
- private final Observer<MediaControllerCompat> mMediaControllerObserver =
- mMediaControllerCallback::onMediaControllerChanged;
+ private final Observer<MediaBrowserConnector.BrowsingState> mMediaBrowsingObserver =
+ mMediaControllerCallback::onMediaBrowsingStateChanged;
private final MediaSourceColors.Factory mColorsFactory;
private final MutableLiveData<MediaSourceColors> mColors = dataOf(null);
@@ -147,15 +149,20 @@
state -> state == null ? dataOf(new PlaybackProgress(0L, 0L))
: new ProgressLiveData(state.mState, state.getMaxProgress()));
+ private final InputFactory mInputFactory;
+
private PlaybackViewModel(Application application, int mode) {
- this(application, MediaSourceViewModel.get(application, mode).getMediaController());
+ this(application, MediaSourceViewModel.get(application, mode).getBrowsingState(),
+ browser -> new MediaControllerCompat(application, browser.getSessionToken()));
}
@VisibleForTesting
- public PlaybackViewModel(Application application, LiveData<MediaControllerCompat> controller) {
+ public PlaybackViewModel(Application application,
+ LiveData<MediaBrowserConnector.BrowsingState> browsingState, InputFactory factory) {
super(application);
+ mInputFactory = factory;
mColorsFactory = new MediaSourceColors.Factory(application);
- controller.observeForever(mMediaControllerObserver);
+ browsingState.observeForever(mMediaBrowsingObserver);
}
/**
@@ -231,29 +238,50 @@
private class MediaControllerCallback extends MediaControllerCompat.Callback {
+ private MediaBrowserConnector.BrowsingState mBrowsingState;
private MediaControllerCompat mMediaController;
private MediaMetadataCompat mMediaMetadata;
private PlaybackStateCompat mPlaybackState;
- void onMediaControllerChanged(MediaControllerCompat controller) {
- if (mMediaController == controller) {
- Log.w(TAG, "onMediaControllerChanged noop");
+
+ void onMediaBrowsingStateChanged(MediaBrowserConnector.BrowsingState newBrowsingState) {
+ if (Objects.equals(mBrowsingState, newBrowsingState)) {
+ Log.w(TAG, "onMediaBrowsingStateChanged noop ");
return;
}
+ // Reset the old controller if any, unregistering the callback when browsing is
+ // not suspended (crashed).
if (mMediaController != null) {
- mMediaController.unregisterCallback(this);
+ switch (newBrowsingState.mConnectionStatus) {
+ case DISCONNECTING:
+ case REJECTED:
+ case CONNECTING:
+ case CONNECTED:
+ mMediaController.unregisterCallback(this);
+ // Fall through
+ case SUSPENDED:
+ setMediaController(null);
+ }
}
+ mBrowsingState = newBrowsingState;
+
+ if (mBrowsingState.mConnectionStatus == ConnectionStatus.CONNECTED) {
+ setMediaController(mInputFactory.getControllerForBrowser(mBrowsingState.mBrowser));
+ }
+ }
+
+ private void setMediaController(MediaControllerCompat mediaController) {
mMediaMetadata = null;
mPlaybackState = null;
- mMediaController = controller;
- mPlaybackControls.setValue(new PlaybackController(controller));
+ mMediaController = mediaController;
+ mPlaybackControls.setValue(new PlaybackController(mediaController));
if (mMediaController != null) {
mMediaController.registerCallback(this);
- mColors.setValue(mColorsFactory.extractColors(controller.getPackageName()));
+ mColors.setValue(mColorsFactory.extractColors(mediaController.getPackageName()));
// The apps don't always send updates so make sure we fetch the most recent values.
onMetadataChanged(mMediaController.getMetadata());
@@ -274,7 +302,9 @@
@Override
public void onSessionDestroyed() {
Log.w(TAG, "onSessionDestroyed");
- onMediaControllerChanged(null);
+ // Bypass the unregisterCallback as the controller is dead.
+ // TODO: consider keeping track of orphaned callbacks in case they are resurrected...
+ setMediaController(null);
}
@Override
diff --git a/car-media-common/src/com/android/car/media/common/source/MediaBrowserConnector.java b/car-media-common/src/com/android/car/media/common/source/MediaBrowserConnector.java
index 14271e4..c1ef1d9 100644
--- a/car-media-common/src/com/android/car/media/common/source/MediaBrowserConnector.java
+++ b/car-media-common/src/com/android/car/media/common/source/MediaBrowserConnector.java
@@ -26,30 +26,106 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.core.util.Preconditions;
import com.android.car.media.common.MediaConstants;
+import java.util.Objects;
+
/**
- * A helper class to connect to a single {@link MediaBrowserCompat}. Connecting to a new one
- * automatically disconnects the previous browser. Changes of the currently connected browser are
- * sent via {@link MediaBrowserConnector.Callback}.
+ * A helper class to connect to a single {@link MediaSource} to its {@link MediaBrowserCompat}.
+ * Connecting to a new one automatically disconnects the previous browser. Changes in the
+ * connection status are sent via {@link MediaBrowserConnector.Callback}.
*/
public class MediaBrowserConnector {
private static final String TAG = "MediaBrowserConnector";
- /** The callback to receive the currently connected {@link MediaBrowserCompat}. */
+ /**
+ * Represents the state of the connection to the media browser service given to
+ * {@link #connectTo}.
+ */
+ public enum ConnectionStatus {
+ /**
+ * The connection request to the browser is being initiated.
+ * Sent from {@link #connectTo} just before calling {@link MediaBrowserCompat#connect}.
+ */
+ CONNECTING,
+ /**
+ * The connection to the browser has been established and it can be used.
+ * Sent from {@link MediaBrowserCompat.ConnectionCallback#onConnected} if
+ * {@link MediaBrowserCompat#isConnected} also returns true.
+ */
+ CONNECTED,
+ /**
+ * The connection to the browser was refused.
+ * Sent from {@link MediaBrowserCompat.ConnectionCallback#onConnectionFailed} or from
+ * {@link MediaBrowserCompat.ConnectionCallback#onConnected} if
+ * {@link MediaBrowserCompat#isConnected} returns false.
+ */
+ REJECTED,
+ /**
+ * The browser crashed and that calls should NOT be made to it anymore.
+ * Called from {@link MediaBrowserCompat.ConnectionCallback#onConnectionSuspended} and from
+ * {@link #connectTo} when {@link MediaBrowserCompat#connect} throws
+ * {@link IllegalStateException}.
+ */
+ SUSPENDED,
+ /**
+ * The connection to the browser is being closed.
+ * When connecting to a new browser and the old browser is connected, this is sent
+ * from {@link #connectTo} just before calling {@link MediaBrowserCompat#disconnect} on the
+ * old browser.
+ */
+ DISCONNECTING
+ }
+
+ /**
+ * Encapsulates a {@link ComponentName} with its {@link MediaBrowserCompat} and the
+ * {@link ConnectionStatus}.
+ */
+ public static class BrowsingState {
+ @NonNull public final MediaSource mMediaSource;
+ @NonNull public final MediaBrowserCompat mBrowser;
+ @NonNull public final ConnectionStatus mConnectionStatus;
+
+ @VisibleForTesting
+ public BrowsingState(@NonNull MediaSource mediaSource, @NonNull MediaBrowserCompat browser,
+ @NonNull ConnectionStatus status) {
+ mMediaSource = Preconditions.checkNotNull(mediaSource, "source can't be null");
+ mBrowser = Preconditions.checkNotNull(browser, "browser can't be null");
+ mConnectionStatus = Preconditions.checkNotNull(status, "status can't be null");
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ BrowsingState that = (BrowsingState) o;
+ return mMediaSource.equals(that.mMediaSource)
+ && mBrowser.equals(that.mBrowser)
+ && mConnectionStatus == that.mConnectionStatus;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mMediaSource, mBrowser, mConnectionStatus);
+ }
+ }
+
+ /** The callback to receive the current {@link MediaBrowserCompat} and its connection state. */
public interface Callback {
- /** When disconnecting, the given browser will be null. */
- void onConnectedBrowserChanged(@Nullable MediaBrowserCompat browser);
+ /** Notifies the listener of connection status changes. */
+ void onBrowserConnectionChanged(@NonNull BrowsingState state);
}
private final Context mContext;
private final Callback mCallback;
private final int mMaxBitmapSizePx;
- @Nullable private ComponentName mBrowseService;
+ @Nullable private MediaSource mMediaSource;
@Nullable private MediaBrowserCompat mBrowser;
/**
@@ -64,22 +140,34 @@
com.android.car.media.common.R.integer.media_items_bitmap_max_size_px);
}
+ private String getSourcePackage() {
+ if (mMediaSource == null) return null;
+ return mMediaSource.getBrowseServiceComponentName().getPackageName();
+ }
+
/** Counter so callbacks from obsolete connections can be ignored. */
private int mBrowserConnectionCallbackCounter = 0;
private class BrowserConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
private final int mSequenceNumber = ++mBrowserConnectionCallbackCounter;
- private final String mCallbackPackage = mBrowseService.getPackageName();
+ private final String mCallbackPackage = getSourcePackage();
+
+ private BrowserConnectionCallback() {
+ if (Log.isLoggable(TAG, Log.INFO)) {
+ Log.i(TAG, "New Callback: " + idHash(this));
+ }
+ }
private boolean isValidCall(String method) {
if (mSequenceNumber != mBrowserConnectionCallbackCounter) {
- Log.e(TAG, "Ignoring callback " + method + " for " + mCallbackPackage + " seq: "
+ Log.e(TAG, "Callback: " + idHash(this) + " ignoring " + method + " for "
+ + mCallbackPackage + " seq: "
+ mSequenceNumber + " current: " + mBrowserConnectionCallbackCounter
- + " current: " + mBrowseService.getPackageName());
+ + " package: " + getSourcePackage());
return false;
} else if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, method + " " + mBrowseService.getPackageName() + idHash(mBrowser));
+ Log.d(TAG, method + " " + getSourcePackage() + " mBrowser: " + idHash(mBrowser));
}
return true;
}
@@ -87,48 +175,67 @@
@Override
public void onConnected() {
if (isValidCall("onConnected")) {
- mCallback.onConnectedBrowserChanged(mBrowser);
+ if (mBrowser != null && mBrowser.isConnected()) {
+ sendNewState(ConnectionStatus.CONNECTED);
+ } else {
+ sendNewState(ConnectionStatus.REJECTED);
+ }
}
}
@Override
public void onConnectionFailed() {
if (isValidCall("onConnectionFailed")) {
- mCallback.onConnectedBrowserChanged(null);
+ sendNewState(ConnectionStatus.REJECTED);
}
}
@Override
public void onConnectionSuspended() {
if (isValidCall("onConnectionSuspended")) {
- mCallback.onConnectedBrowserChanged(null);
+ sendNewState(ConnectionStatus.SUSPENDED);
}
}
}
+ private void sendNewState(ConnectionStatus cnx) {
+ if (mMediaSource == null) {
+ Log.e(TAG, "sendNewState mMediaSource is null!");
+ return;
+ }
+ if (mBrowser == null) {
+ Log.e(TAG, "sendNewState mBrowser is null!");
+ return;
+ }
+ mCallback.onBrowserConnectionChanged(new BrowsingState(mMediaSource, mBrowser, cnx));
+ }
+
/**
- * Creates and connects a new {@link MediaBrowserCompat} if the given {@link ComponentName}
+ * Creates and connects a new {@link MediaBrowserCompat} if the given {@link MediaSource}
* isn't null. If needed, the previous browser is disconnected.
- * @param browseService the ComponentName of the media browser service.
+ * @param mediaSource the media source to connect to.
* @see MediaBrowserCompat#MediaBrowserCompat(Context, ComponentName,
* MediaBrowserCompat.ConnectionCallback, android.os.Bundle)
*/
- public void connectTo(@Nullable ComponentName browseService) {
+ public void connectTo(@Nullable MediaSource mediaSource) {
if (mBrowser != null && mBrowser.isConnected()) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Disconnecting: " + mBrowseService.getPackageName() + idHash(mBrowser));
+ Log.d(TAG, "Disconnecting: " + getSourcePackage()
+ + " mBrowser: " + idHash(mBrowser));
}
- mCallback.onConnectedBrowserChanged(null);
+ sendNewState(ConnectionStatus.DISCONNECTING);
mBrowser.disconnect();
}
- mBrowseService = browseService;
- if (mBrowseService != null) {
- mBrowser = createMediaBrowser(mBrowseService, new BrowserConnectionCallback());
+ mMediaSource = mediaSource;
+ if (mMediaSource != null) {
+ mBrowser = createMediaBrowser(mMediaSource, new BrowserConnectionCallback());
if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Connecting to: " + mBrowseService.getPackageName() + idHash(mBrowser));
+ Log.d(TAG, "Connecting to: " + getSourcePackage()
+ + " mBrowser: " + idHash(mBrowser));
}
try {
+ sendNewState(ConnectionStatus.CONNECTING);
mBrowser.connect();
} catch (IllegalStateException ex) {
// Is this comment still valid ?
@@ -136,6 +243,7 @@
// 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);
+ sendNewState(ConnectionStatus.SUSPENDED);
}
} else {
mBrowser = null;
@@ -144,10 +252,11 @@
// Override for testing.
@NonNull
- protected MediaBrowserCompat createMediaBrowser(@NonNull ComponentName browseService,
+ protected MediaBrowserCompat createMediaBrowser(@NonNull MediaSource mediaSource,
@NonNull MediaBrowserCompat.ConnectionCallback callback) {
Bundle rootHints = new Bundle();
rootHints.putInt(MediaConstants.EXTRA_MEDIA_ART_SIZE_HINT_PIXELS, mMaxBitmapSizePx);
+ ComponentName browseService = mediaSource.getBrowseServiceComponentName();
return new MediaBrowserCompat(mContext, browseService, callback, rootHints);
}
}
diff --git a/car-media-common/src/com/android/car/media/common/source/MediaSource.java b/car-media-common/src/com/android/car/media/common/source/MediaSource.java
index e09c248..90a5b57 100644
--- a/car-media-common/src/com/android/car/media/common/source/MediaSource.java
+++ b/car-media-common/src/com/android/car/media/common/source/MediaSource.java
@@ -31,6 +31,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import com.android.car.apps.common.BitmapUtils;
import com.android.car.apps.common.IconCropper;
@@ -84,7 +85,8 @@
}
}
- private MediaSource(@NonNull ComponentName browseService, @NonNull CharSequence displayName,
+ @VisibleForTesting
+ public MediaSource(@NonNull ComponentName browseService, @NonNull CharSequence displayName,
@NonNull Drawable icon, @NonNull IconCropper iconCropper) {
mBrowseService = browseService;
mDisplayName = displayName;
diff --git a/car-media-common/src/com/android/car/media/common/source/MediaSourceViewModel.java b/car-media-common/src/com/android/car/media/common/source/MediaSourceViewModel.java
index 4fbb202..8c93426 100644
--- a/car-media-common/src/com/android/car/media/common/source/MediaSourceViewModel.java
+++ b/car-media-common/src/com/android/car/media/common/source/MediaSourceViewModel.java
@@ -16,9 +16,6 @@
package com.android.car.media.common.source;
-import static android.car.media.CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK;
-
-import static com.android.car.apps.common.util.CarAppsDebugUtils.idHash;
import static com.android.car.arch.common.LiveDataFunctions.dataOf;
import android.app.Application;
@@ -26,20 +23,17 @@
import android.car.CarNotConnectedException;
import android.car.media.CarMediaManager;
import android.content.ComponentName;
-import android.media.session.MediaController;
import android.os.Handler;
-import android.support.v4.media.MediaBrowserCompat;
-import android.support.v4.media.session.MediaControllerCompat;
-import android.support.v4.media.session.MediaSessionCompat;
import android.util.Log;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
+import com.android.car.media.common.source.MediaBrowserConnector.BrowsingState;
+
import java.util.Objects;
/**
@@ -56,10 +50,8 @@
// Primary media source.
private final MutableLiveData<MediaSource> mPrimaryMediaSource = dataOf(null);
- // Connected browser for the primary media source.
- private final MutableLiveData<MediaBrowserCompat> mConnectedMediaBrowser = dataOf(null);
- // Media controller for the connected browser.
- private final MutableLiveData<MediaControllerCompat> mMediaController = dataOf(null);
+ // Browser for the primary media source and its connection state.
+ private final MutableLiveData<BrowsingState> mBrowsingState = dataOf(null);
private final Handler mHandler;
private final CarMediaManager.MediaSourceChangedListener mMediaSourceListener;
@@ -72,8 +64,6 @@
MediaBrowserConnector createMediaBrowserConnector(@NonNull Application application,
@NonNull MediaBrowserConnector.Callback connectedBrowserCallback);
- MediaControllerCompat getControllerForSession(@Nullable MediaSessionCompat.Token session);
-
Car getCarApi();
CarMediaManager getCarMediaManager(Car carApi) throws CarNotConnectedException;
@@ -81,14 +71,6 @@
MediaSource getMediaSource(ComponentName componentName);
}
- /**
- * Returns the MediaSourceViewModel singleton tied to the application.
- * @deprecated should use get(Application application, int mode) instead
- */
- public static MediaSourceViewModel get(@NonNull Application application) {
- return get(application, MEDIA_SOURCE_MODE_PLAYBACK);
- }
-
/** Returns the MediaSourceViewModel singleton tied to the application. */
public static MediaSourceViewModel get(@NonNull Application application, int mode) {
if (sInstances[mode] == null) {
@@ -112,12 +94,6 @@
}
@Override
- public MediaControllerCompat getControllerForSession(
- @Nullable MediaSessionCompat.Token token) {
- return token == null ? null : new MediaControllerCompat(application, token);
- }
-
- @Override
public Car getCarApi() {
return Car.createCar(application);
}
@@ -137,7 +113,7 @@
private final InputFactory mInputFactory;
private final MediaBrowserConnector mBrowserConnector;
- private final MediaBrowserConnector.Callback mConnectedBrowserCallback;
+ private final MediaBrowserConnector.Callback mBrowserCallback = mBrowsingState::setValue;
@VisibleForTesting
MediaSourceViewModel(@NonNull Application application, int mode,
@@ -147,23 +123,7 @@
mInputFactory = inputFactory;
mCar = inputFactory.getCarApi();
- mConnectedBrowserCallback = browser -> {
- mConnectedMediaBrowser.setValue(browser);
- if (browser != null) {
- if (!browser.isConnected()) {
- Log.e(TAG, "Browser is NOT connected !! "
- + mPrimaryMediaSource.getValue().toString() + idHash(browser));
- mMediaController.setValue(null);
- } else {
- mMediaController.setValue(mInputFactory.getControllerForSession(
- browser.getSessionToken()));
- }
- } else {
- mMediaController.setValue(null);
- }
- };
- mBrowserConnector = inputFactory.createMediaBrowserConnector(application,
- mConnectedBrowserCallback);
+ mBrowserConnector = inputFactory.createMediaBrowserConnector(application, mBrowserCallback);
mHandler = new Handler(application.getMainLooper());
mMediaSourceListener = componentName -> mHandler.post(
@@ -185,8 +145,8 @@
}
@VisibleForTesting
- MediaBrowserConnector.Callback getConnectedBrowserCallback() {
- return mConnectedBrowserCallback;
+ MediaBrowserConnector.Callback getBrowserCallback() {
+ return mBrowserCallback;
}
/**
@@ -204,21 +164,11 @@
}
/**
- * Returns a LiveData that emits the currently connected MediaBrowser. Emits {@code null} if no
- * MediaSource is set, if the MediaSource does not support browsing, or if the MediaBrowser is
- * not connected.
+ * Returns a LiveData that emits a {@link BrowsingState}, or {@code null} if there is no media
+ * source.
*/
- public LiveData<MediaBrowserCompat> getConnectedMediaBrowser() {
- return mConnectedMediaBrowser;
- }
-
- /**
- * Returns a LiveData that emits a {@link MediaController} that allows controlling this media
- * source, or emits {@code null} if the media source doesn't support browsing or the browser is
- * not connected.
- */
- public LiveData<MediaControllerCompat> getMediaController() {
- return mMediaController;
+ public LiveData<BrowsingState> getBrowsingState() {
+ return mBrowsingState;
}
private void updateModelState(MediaSource newMediaSource) {
@@ -233,8 +183,7 @@
// Recompute dependent values
if (newMediaSource != null) {
- ComponentName browseService = newMediaSource.getBrowseServiceComponentName();
- mBrowserConnector.connectTo(browseService);
+ mBrowserConnector.connectTo(newMediaSource);
}
}
}
diff --git a/car-media-common/tests/robotests/src/com/android/car/media/common/MediaTestUtils.java b/car-media-common/tests/robotests/src/com/android/car/media/common/MediaTestUtils.java
new file mode 100644
index 0000000..84643c4
--- /dev/null
+++ b/car-media-common/tests/robotests/src/com/android/car/media/common/MediaTestUtils.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.media.common;
+
+import android.content.ComponentName;
+import android.graphics.Path;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+
+import androidx.annotation.NonNull;
+
+import com.android.car.apps.common.IconCropper;
+import com.android.car.media.common.source.MediaSource;
+
+public class MediaTestUtils {
+
+ private MediaTestUtils() {
+ }
+
+ /** Creates a fake {@link MediaSource}. */
+ public static MediaSource newFakeMediaSource(@NonNull String pkg, @NonNull String cls) {
+ return newFakeMediaSource(new ComponentName(pkg, cls));
+ }
+
+ /** Creates a fake {@link MediaSource}. */
+ public static MediaSource newFakeMediaSource(@NonNull ComponentName browseService) {
+ String displayName = browseService.getClassName();
+ Drawable icon = new ColorDrawable();
+ IconCropper iconCropper = new IconCropper(new Path());
+ return new MediaSource(browseService, displayName, icon, iconCropper);
+ }
+}
diff --git a/car-media-common/tests/robotests/src/com/android/car/media/common/playback/PlaybackViewModelTest.java b/car-media-common/tests/robotests/src/com/android/car/media/common/playback/PlaybackViewModelTest.java
index 9d00628..2d65528 100644
--- a/car-media-common/tests/robotests/src/com/android/car/media/common/playback/PlaybackViewModelTest.java
+++ b/car-media-common/tests/robotests/src/com/android/car/media/common/playback/PlaybackViewModelTest.java
@@ -17,6 +17,7 @@
package com.android.car.media.common.playback;
import static com.android.car.arch.common.LiveDataFunctions.dataOf;
+import static com.android.car.media.common.MediaTestUtils.newFakeMediaSource;
import static com.google.common.truth.Truth.assertThat;
@@ -25,7 +26,7 @@
import static org.mockito.Mockito.when;
import static org.robolectric.RuntimeEnvironment.application;
-import android.media.MediaDescription;
+import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaControllerCompat;
@@ -39,6 +40,9 @@
import com.android.car.arch.common.testing.InstantTaskExecutorRule;
import com.android.car.arch.common.testing.TestLifecycleOwner;
import com.android.car.media.common.MediaItemMetadata;
+import com.android.car.media.common.source.MediaBrowserConnector.BrowsingState;
+import com.android.car.media.common.source.MediaBrowserConnector.ConnectionStatus;
+import com.android.car.media.common.source.MediaSource;
import org.junit.Before;
import org.junit.Rule;
@@ -53,7 +57,9 @@
import java.util.Arrays;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
@RunWith(RobolectricTestRunner.class)
public class PlaybackViewModelTest {
@@ -65,13 +71,15 @@
@Rule
public final TestLifecycleOwner mLifecycleOwner = new TestLifecycleOwner();
+ private final MediaSource mMediaSource = newFakeMediaSource("test", "test");
+
+ @Mock
+ public MediaBrowserCompat mMediaBrowser;
@Mock
public MediaControllerCompat mMediaController;
@Mock
public MediaMetadataCompat mMediaMetadata;
@Mock
- public MediaDescription mMediaDescription;
- @Mock
public MediaDescriptionCompat mMediaDescriptionCompat;
@Mock
public PlaybackStateCompat mPlaybackState;
@@ -80,19 +88,23 @@
private PlaybackViewModel mPlaybackViewModel;
- private MutableLiveData<MediaControllerCompat> mMediaControllerLiveData;
+ private MutableLiveData<BrowsingState> mBrowsingStateLD;
+
+ private Map<MediaBrowserCompat, MediaControllerCompat> mBrowserToController = new HashMap<>();
@Before
public void setUp() {
+ mBrowserToController.put(mMediaBrowser, mMediaController);
doNothing().when(mMediaController).registerCallback(mCapturedCallback.capture());
- when(mMediaDescriptionCompat.getMediaDescription()).thenReturn(mMediaDescription);
- when(mMediaMetadata.getDescription()).thenReturn(mMediaDescriptionCompat);
- mMediaControllerLiveData = dataOf(mMediaController);
- mPlaybackViewModel = new PlaybackViewModel(application, mMediaControllerLiveData);
+ mBrowsingStateLD = dataOf(
+ new BrowsingState(mMediaSource, mMediaBrowser, ConnectionStatus.CONNECTED));
+ mPlaybackViewModel = new PlaybackViewModel(application, mBrowsingStateLD,
+ browser -> mBrowserToController.get(browser));
}
@Test
public void testGetMetadata() {
+ when(mMediaMetadata.getDescription()).thenReturn(mMediaDescriptionCompat);
CaptureObserver<MediaItemMetadata> observer = new CaptureObserver<>();
mPlaybackViewModel.getMetadata().observe(mLifecycleOwner, observer);
observer.reset();
@@ -187,13 +199,13 @@
@Test
public void testChangeMediaSource_consistentController() {
- // Ensure getters are consistent with values delivered by callback
- when(mMediaController.getMetadata()).thenReturn(mMediaMetadata);
- when(mMediaController.getPlaybackState()).thenReturn(mPlaybackState);
deliverValuesToCallbacks(mCapturedCallback, mMediaMetadata, mPlaybackState);
- // Create new MediaController and associated callback captor
+ // Create new MediaBrowser, new MediaController and associated callback captor
+ MediaBrowserCompat newMediaBrowser = mock(MediaBrowserCompat.class);
MediaControllerCompat newController = mock(MediaControllerCompat.class);
+ mBrowserToController.put(newMediaBrowser, newController);
+
ArgumentCaptor<MediaControllerCompat.Callback> newCallbackCaptor =
ArgumentCaptor.forClass(MediaControllerCompat.Callback.class);
doNothing().when(newController).registerCallback(newCallbackCaptor.capture());
@@ -201,8 +213,6 @@
// Wire up new data for new MediaController
MediaMetadataCompat newMetadata = mock(MediaMetadataCompat.class);
PlaybackStateCompat newPlaybackState = mock(PlaybackStateCompat.class);
- when(newController.getMetadata()).thenReturn(newMetadata);
- when(newController.getPlaybackState()).thenReturn(newPlaybackState);
// Ensure that all values are coming from the correct MediaController.
mPlaybackViewModel.getMetadata().observe(mLifecycleOwner, mediaItemMetadata -> {
@@ -225,7 +235,8 @@
}
});
- mMediaControllerLiveData.setValue(newController);
+ mBrowsingStateLD.setValue(
+ new BrowsingState(mMediaSource, newMediaBrowser, ConnectionStatus.CONNECTED));
deliverValuesToCallbacks(newCallbackCaptor, newMetadata, newPlaybackState);
}
diff --git a/car-media-common/tests/robotests/src/com/android/car/media/common/source/MediaBrowserConnectorTest.java b/car-media-common/tests/robotests/src/com/android/car/media/common/source/MediaBrowserConnectorTest.java
index eeccb5e..fdf5a56 100644
--- a/car-media-common/tests/robotests/src/com/android/car/media/common/source/MediaBrowserConnectorTest.java
+++ b/car-media-common/tests/robotests/src/com/android/car/media/common/source/MediaBrowserConnectorTest.java
@@ -16,21 +16,24 @@
package com.android.car.media.common.source;
+import static com.android.car.media.common.MediaTestUtils.newFakeMediaSource;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
import static org.robolectric.RuntimeEnvironment.application;
-import android.content.ComponentName;
import android.support.v4.media.MediaBrowserCompat;
import androidx.annotation.NonNull;
import com.android.car.arch.common.testing.InstantTaskExecutorRule;
import com.android.car.arch.common.testing.TestLifecycleOwner;
+import com.android.car.media.common.source.MediaBrowserConnector.BrowsingState;
+import com.android.car.media.common.source.MediaBrowserConnector.ConnectionStatus;
import org.junit.Before;
import org.junit.Rule;
@@ -62,10 +65,10 @@
@Mock
public MediaBrowserCompat mMediaBrowser2;
- private final ComponentName mBrowseService1 = new ComponentName("mediaService1", "className1");
- private final ComponentName mBrowseService2 = new ComponentName("mediaService2", "className2");
+ private final MediaSource mMediaSource1 = newFakeMediaSource("mediaService1", "className1");
+ private final MediaSource mMediaSource2 = newFakeMediaSource("mediaService2", "className2");
- private final Map<ComponentName, MediaBrowserCompat> mBrowsers = new HashMap<>(2);
+ private final Map<MediaSource, MediaBrowserCompat> mBrowsers = new HashMap<>(2);
private MediaBrowserConnector mBrowserConnector;
private MediaBrowserCompat.ConnectionCallback mConnectionCallback;
@@ -73,24 +76,22 @@
@Mock
public MediaBrowserConnector.Callback mConnectedBrowserCallback;
@Captor
- private ArgumentCaptor<MediaBrowserCompat> mConnectedBrowserCaptor;
+ private ArgumentCaptor<BrowsingState> mBrowsingStateCaptor;
@Before
public void setUp() {
- mBrowsers.put(mBrowseService1, mMediaBrowser1);
- mBrowsers.put(mBrowseService2, mMediaBrowser2);
- when(mMediaBrowser1.isConnected()).thenReturn(false);
- when(mMediaBrowser2.isConnected()).thenReturn(false);
+ mBrowsers.put(mMediaSource1, mMediaBrowser1);
+ mBrowsers.put(mMediaSource2, mMediaBrowser2);
- doNothing().when(mConnectedBrowserCallback).onConnectedBrowserChanged(
- mConnectedBrowserCaptor.capture());
+ doNothing().when(mConnectedBrowserCallback).onBrowserConnectionChanged(
+ mBrowsingStateCaptor.capture());
mBrowserConnector = new MediaBrowserConnector(application, mConnectedBrowserCallback) {
@Override
- protected MediaBrowserCompat createMediaBrowser(@NonNull ComponentName browseService,
+ protected MediaBrowserCompat createMediaBrowser(@NonNull MediaSource mediaSource,
@NonNull MediaBrowserCompat.ConnectionCallback callback) {
mConnectionCallback = callback;
- return mBrowsers.get(browseService);
+ return mBrowsers.get(mediaSource);
}
};
}
@@ -101,54 +102,77 @@
throw new IllegalStateException("expected");
});
- mBrowserConnector.connectTo(mBrowseService1);
+ mBrowserConnector.connectTo(mMediaSource1);
verify(mMediaBrowser1).connect();
}
@Test
public void testConnectionCallback_onConnected() {
+ doReturn(true).when(mMediaBrowser1).isConnected();
setConnectionAction(() -> mConnectionCallback.onConnected());
- mBrowserConnector.connectTo(mBrowseService1);
+ mBrowserConnector.connectTo(mMediaSource1);
- assertThat(mConnectedBrowserCaptor.getValue()).isEqualTo(mMediaBrowser1);
+ BrowsingState state = mBrowsingStateCaptor.getValue();
+ assertThat(state.mBrowser).isEqualTo(mMediaBrowser1);
+ assertThat(state.mConnectionStatus).isEqualTo(ConnectionStatus.CONNECTED);
+ }
+
+ @Test
+ public void testConnectionCallback_onConnected_inconsistentState() {
+ doReturn(false).when(mMediaBrowser1).isConnected();
+ setConnectionAction(() -> mConnectionCallback.onConnected());
+
+ mBrowserConnector.connectTo(mMediaSource1);
+
+ BrowsingState state = mBrowsingStateCaptor.getValue();
+ assertThat(state.mBrowser).isEqualTo(mMediaBrowser1);
+ assertThat(state.mConnectionStatus).isEqualTo(ConnectionStatus.REJECTED);
}
@Test
public void testConnectionCallback_onConnectionFailed() {
setConnectionAction(() -> mConnectionCallback.onConnectionFailed());
- mBrowserConnector.connectTo(mBrowseService1);
+ mBrowserConnector.connectTo(mMediaSource1);
- assertThat(mConnectedBrowserCaptor.getValue()).isNull();
+ BrowsingState state = mBrowsingStateCaptor.getValue();
+ assertThat(state.mBrowser).isEqualTo(mMediaBrowser1);
+ assertThat(state.mConnectionStatus).isEqualTo(ConnectionStatus.REJECTED);
}
@Test
public void testConnectionCallback_onConnectionSuspended() {
+ doReturn(true).when(mMediaBrowser1).isConnected();
setConnectionAction(() -> {
mConnectionCallback.onConnected();
mConnectionCallback.onConnectionSuspended();
});
- mBrowserConnector.connectTo(mBrowseService1);
+ mBrowserConnector.connectTo(mMediaSource1);
- List<MediaBrowserCompat> browsers = mConnectedBrowserCaptor.getAllValues();
- assertThat(browsers.get(0)).isEqualTo(mMediaBrowser1);
- assertThat(browsers.get(1)).isNull();
+ List<BrowsingState> browsingStates = mBrowsingStateCaptor.getAllValues();
+ assertThat(browsingStates.get(0).mBrowser).isEqualTo(mMediaBrowser1);
+ assertThat(browsingStates.get(1).mBrowser).isEqualTo(mMediaBrowser1);
+ assertThat(browsingStates.get(2).mBrowser).isEqualTo(mMediaBrowser1);
+
+ assertThat(browsingStates.get(0).mConnectionStatus).isEqualTo(ConnectionStatus.CONNECTING);
+ assertThat(browsingStates.get(1).mConnectionStatus).isEqualTo(ConnectionStatus.CONNECTED);
+ assertThat(browsingStates.get(2).mConnectionStatus).isEqualTo(ConnectionStatus.SUSPENDED);
}
@Test
public void testConnectionCallback_onConnectedIgnoredWhenLate() {
- mBrowserConnector.connectTo(mBrowseService1);
+ mBrowserConnector.connectTo(mMediaSource1);
MediaBrowserCompat.ConnectionCallback cb1 = mConnectionCallback;
- mBrowserConnector.connectTo(mBrowseService2);
+ mBrowserConnector.connectTo(mMediaSource2);
MediaBrowserCompat.ConnectionCallback cb2 = mConnectionCallback;
cb2.onConnected();
cb1.onConnected();
- assertThat(mConnectedBrowserCaptor.getValue()).isEqualTo(mMediaBrowser2);
+ assertThat(mBrowsingStateCaptor.getValue().mBrowser).isEqualTo(mMediaBrowser2);
}
private void setConnectionAction(@NonNull Runnable action) {
diff --git a/car-media-common/tests/robotests/src/com/android/car/media/common/source/MediaSourceViewModelTest.java b/car-media-common/tests/robotests/src/com/android/car/media/common/source/MediaSourceViewModelTest.java
index b22bba2..313f275 100644
--- a/car-media-common/tests/robotests/src/com/android/car/media/common/source/MediaSourceViewModelTest.java
+++ b/car-media-common/tests/robotests/src/com/android/car/media/common/source/MediaSourceViewModelTest.java
@@ -18,9 +18,11 @@
import static android.car.media.CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK;
+import static com.android.car.apps.common.util.CarAppsDebugUtils.idHash;
+import static com.android.car.media.common.MediaTestUtils.newFakeMediaSource;
+
import static com.google.common.truth.Truth.assertThat;
-import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.robolectric.RuntimeEnvironment.application;
@@ -29,15 +31,15 @@
import android.car.media.CarMediaManager;
import android.content.ComponentName;
import android.support.v4.media.MediaBrowserCompat;
-import android.support.v4.media.session.MediaControllerCompat;
-import android.support.v4.media.session.MediaSessionCompat;
+import android.util.Log;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import com.android.car.arch.common.testing.CaptureObserver;
import com.android.car.arch.common.testing.InstantTaskExecutorRule;
import com.android.car.arch.common.testing.TestLifecycleOwner;
+import com.android.car.media.common.source.MediaBrowserConnector.BrowsingState;
+import com.android.car.media.common.source.MediaBrowserConnector.ConnectionStatus;
import org.junit.Before;
import org.junit.Rule;
@@ -48,11 +50,12 @@
import org.mockito.junit.MockitoRule;
import org.robolectric.RobolectricTestRunner;
+
@RunWith(RobolectricTestRunner.class)
public class MediaSourceViewModelTest {
- private static final String BROWSER_CONTROLLER_PACKAGE_NAME = "browser";
- private static final String SESSION_MANAGER_CONTROLLER_PACKAGE_NAME = "mediaSessionManager";
+ private static final String TAG = "MediaSourceVMTest";
+
@Rule
public final MockitoRule mMockitoRule = MockitoJUnit.rule();
@Rule
@@ -62,10 +65,6 @@
@Mock
public MediaBrowserCompat mMediaBrowser;
- @Mock
- public MediaControllerCompat mMediaControllerFromBrowser;
- @Mock
- public MediaControllerCompat mMediaControllerFromSessionManager;
@Mock
public Car mCar;
@@ -74,17 +73,12 @@
private MediaSourceViewModel mViewModel;
- private ComponentName mRequestedBrowseService;
+ private MediaSource mRequestedSource;
private MediaSource mMediaSource;
@Before
public void setUp() {
- when(mMediaControllerFromBrowser.getPackageName())
- .thenReturn(BROWSER_CONTROLLER_PACKAGE_NAME);
- when(mMediaControllerFromSessionManager.getPackageName())
- .thenReturn(SESSION_MANAGER_CONTROLLER_PACKAGE_NAME);
-
- mRequestedBrowseService = null;
+ mRequestedSource = null;
mMediaSource = null;
}
@@ -97,21 +91,17 @@
@NonNull MediaBrowserConnector.Callback connectedBrowserCallback) {
return new MediaBrowserConnector(application, connectedBrowserCallback) {
@Override
- protected MediaBrowserCompat createMediaBrowser(ComponentName browseService,
+ protected MediaBrowserCompat createMediaBrowser(MediaSource mediaSource,
MediaBrowserCompat.ConnectionCallback callback) {
- mRequestedBrowseService = browseService;
- return super.createMediaBrowser(browseService, callback);
+ mRequestedSource = mediaSource;
+ MediaBrowserCompat bro = super.createMediaBrowser(mediaSource, callback);
+ Log.i(TAG, "createMediaBrowser: " + idHash(bro) + " for: " + mediaSource);
+ return bro;
}
};
}
@Override
- public MediaControllerCompat getControllerForSession(
- @Nullable MediaSessionCompat.Token token) {
- return mMediaControllerFromBrowser;
- }
-
- @Override
public Car getCarApi() {
return mCar;
}
@@ -140,33 +130,35 @@
@Test
public void testGetMediaController_connectedBrowser() {
- CaptureObserver<MediaControllerCompat> observer = new CaptureObserver<>();
- ComponentName testComponent = new ComponentName("test", "test");
- mMediaSource = mock(MediaSource.class);
- when(mMediaSource.getBrowseServiceComponentName()).thenReturn(testComponent);
+ CaptureObserver<BrowsingState> observer = new CaptureObserver<>();
+ mMediaSource = newFakeMediaSource("test", "test");
when(mMediaBrowser.isConnected()).thenReturn(true);
initializeViewModel();
- mViewModel.getConnectedBrowserCallback().onConnectedBrowserChanged(mMediaBrowser);
- mViewModel.getMediaController().observe(mLifecycleOwner, observer);
+ mViewModel.getBrowserCallback().onBrowserConnectionChanged(
+ new BrowsingState(mMediaSource, mMediaBrowser, ConnectionStatus.CONNECTED));
+ mViewModel.getBrowsingState().observe(mLifecycleOwner, observer);
- assertThat(observer.getObservedValue()).isSameAs(mMediaControllerFromBrowser);
- assertThat(mRequestedBrowseService).isEqualTo(testComponent);
+ BrowsingState browsingState = observer.getObservedValue();
+ assertThat(browsingState.mBrowser).isSameAs(mMediaBrowser);
+ assertThat(browsingState.mConnectionStatus).isEqualTo(ConnectionStatus.CONNECTED);
+ assertThat(mRequestedSource).isEqualTo(mMediaSource);
}
@Test
public void testGetMediaController_noActiveSession_notConnected() {
- CaptureObserver<MediaControllerCompat> observer = new CaptureObserver<>();
- ComponentName testComponent = new ComponentName("test", "test");
- mMediaSource = mock(MediaSource.class);
- when(mMediaSource.getBrowseServiceComponentName()).thenReturn(testComponent);
+ CaptureObserver<BrowsingState> observer = new CaptureObserver<>();
+ mMediaSource = newFakeMediaSource("test", "test");
when(mMediaBrowser.isConnected()).thenReturn(false);
initializeViewModel();
- mViewModel.getMediaController().observe(mLifecycleOwner, observer);
+ mViewModel.getBrowserCallback().onBrowserConnectionChanged(
+ new BrowsingState(mMediaSource, mMediaBrowser, ConnectionStatus.REJECTED));
+ mViewModel.getBrowsingState().observe(mLifecycleOwner, observer);
- assertThat(observer.hasBeenNotified()).isTrue();
- assertThat(observer.getObservedValue()).isNull();
- assertThat(mRequestedBrowseService).isEqualTo(testComponent);
+ BrowsingState browsingState = observer.getObservedValue();
+ assertThat(browsingState.mBrowser).isSameAs(mMediaBrowser);
+ assertThat(browsingState.mConnectionStatus).isEqualTo(ConnectionStatus.REJECTED);
+ assertThat(mRequestedSource).isEqualTo(mMediaSource);
}
}
diff --git a/car-messenger-common/Android.bp b/car-messenger-common/Android.bp
index 6c7a82f..7b4493f 100644
--- a/car-messenger-common/Android.bp
+++ b/car-messenger-common/Android.bp
@@ -34,7 +34,6 @@
"car-apps-common",
"car-messenger-protos",
"car-telephony-common",
- "connected-device-protos",
"libphonenumber",
],
}
diff --git a/car-telephony-common/src/com/android/car/telephony/common/AsyncQueryLiveData.java b/car-telephony-common/src/com/android/car/telephony/common/AsyncQueryLiveData.java
index ecacb51..c4f6170 100644
--- a/car-telephony-common/src/com/android/car/telephony/common/AsyncQueryLiveData.java
+++ b/car-telephony-common/src/com/android/car/telephony/common/AsyncQueryLiveData.java
@@ -51,8 +51,7 @@
public AsyncQueryLiveData(Context context, QueryParam.Provider provider,
ExecutorService executorService) {
- mObservableAsyncQuery = new ObservableAsyncQuery(provider, context.getContentResolver(),
- this::onCursorLoaded);
+ mObservableAsyncQuery = new ObservableAsyncQuery(context, provider, this::onCursorLoaded);
mExecutorService = executorService;
}
diff --git a/car-telephony-common/src/com/android/car/telephony/common/InMemoryPhoneBook.java b/car-telephony-common/src/com/android/car/telephony/common/InMemoryPhoneBook.java
index 8ed7e73..3bdf52a 100644
--- a/car-telephony-common/src/com/android/car/telephony/common/InMemoryPhoneBook.java
+++ b/car-telephony-common/src/com/android/car/telephony/common/InMemoryPhoneBook.java
@@ -16,6 +16,7 @@
package com.android.car.telephony.common;
+import android.Manifest;
import android.content.Context;
import android.database.Cursor;
import android.provider.ContactsContract;
@@ -119,7 +120,8 @@
ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE,
ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE,
ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE},
- ContactsContract.Contacts.DISPLAY_NAME + " ASC ");
+ ContactsContract.Contacts.DISPLAY_NAME + " ASC ",
+ Manifest.permission.READ_CONTACTS);
mContactListAsyncQueryLiveData = new AsyncQueryLiveData<List<Contact>>(mContext,
QueryParam.of(contactListQueryParam), Executors.newSingleThreadExecutor()) {
@Override
diff --git a/car-telephony-common/src/com/android/car/telephony/common/ObservableAsyncQuery.java b/car-telephony-common/src/com/android/car/telephony/common/ObservableAsyncQuery.java
index 394b6d4..9f2f0cb 100644
--- a/car-telephony-common/src/com/android/car/telephony/common/ObservableAsyncQuery.java
+++ b/car-telephony-common/src/com/android/car/telephony/common/ObservableAsyncQuery.java
@@ -18,12 +18,15 @@
import android.content.AsyncQueryHandler;
import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.PackageManager;
import android.database.ContentObserver;
import android.database.Cursor;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
import com.android.car.apps.common.log.L;
@@ -48,6 +51,7 @@
void onQueryFinished(@Nullable Cursor cursor);
}
+ private Context mContext;
private AsyncQueryHandler mAsyncQueryHandler;
private QueryParam.Provider mQueryParamProvider;
private OnQueryFinishedListener mOnQueryFinishedListener;
@@ -61,11 +65,12 @@
* @param listener Listener which will be called when data is available.
*/
public ObservableAsyncQuery(
+ @NonNull Context context,
@NonNull QueryParam.Provider queryParamProvider,
- @NonNull ContentResolver cr,
@NonNull OnQueryFinishedListener listener) {
- mAsyncQueryHandler = new AsyncQueryHandlerImpl(this, cr);
- mContentResolver = cr;
+ mContext = context;
+ mContentResolver = context.getContentResolver();
+ mAsyncQueryHandler = new AsyncQueryHandlerImpl(this, mContentResolver);
mContentObserver = new ContentObserver(mAsyncQueryHandler) {
@Override
public void onChange(boolean selfChange) {
@@ -88,7 +93,8 @@
mToken++;
QueryParam queryParam = mQueryParamProvider.getQueryParam();
- if (queryParam != null) {
+ if (queryParam != null && ContextCompat.checkSelfPermission(mContext,
+ queryParam.mPermission) == PackageManager.PERMISSION_GRANTED) {
mAsyncQueryHandler.startQuery(
mToken,
null,
diff --git a/car-telephony-common/src/com/android/car/telephony/common/QueryParam.java b/car-telephony-common/src/com/android/car/telephony/common/QueryParam.java
index 9628124..6ceb7c5 100644
--- a/car-telephony-common/src/com/android/car/telephony/common/QueryParam.java
+++ b/car-telephony-common/src/com/android/car/telephony/common/QueryParam.java
@@ -53,17 +53,21 @@
final String[] mSelectionArgs;
/** Used by {@link ObservableAsyncQuery#startQuery()} as query param. */
final String mOrderBy;
+ /** Used by {@link ObservableAsyncQuery#startQuery()} to check query permission. */
+ final String mPermission;
public QueryParam(
@NonNull Uri uri,
@Nullable String[] projection,
@Nullable String selection,
@Nullable String[] selectionArgs,
- @Nullable String orderBy) {
+ @Nullable String orderBy,
+ @NonNull String permission) {
mUri = uri;
mProjection = projection;
mSelection = selection;
mSelectionArgs = selectionArgs;
mOrderBy = orderBy;
+ mPermission = permission;
}
}
diff --git a/car-telephony-common/src/com/android/car/telephony/common/TelecomUtils.java b/car-telephony-common/src/com/android/car/telephony/common/TelecomUtils.java
index 40d93a7..df1713b 100644
--- a/car-telephony-common/src/com/android/car/telephony/common/TelecomUtils.java
+++ b/car-telephony-common/src/com/android/car/telephony/common/TelecomUtils.java
@@ -42,6 +42,7 @@
import android.widget.ImageView;
import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.RoundedBitmapDrawable;
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
@@ -187,17 +188,21 @@
public static final class PhoneNumberInfo {
private final String mPhoneNumber;
private final String mDisplayName;
+ private final String mDisplayNameAlt;
private final String mInitials;
private final Uri mAvatarUri;
private final String mTypeLabel;
+ private final String mLookupKey;
- public PhoneNumberInfo(String phoneNumber, String displayName,
- String initials, Uri avatarUri, String typeLabel) {
+ public PhoneNumberInfo(String phoneNumber, String displayName, String displayNameAlt,
+ String initials, Uri avatarUri, String typeLabel, String lookupKey) {
mPhoneNumber = phoneNumber;
mDisplayName = displayName;
+ mDisplayNameAlt = displayNameAlt;
mInitials = initials;
mAvatarUri = avatarUri;
mTypeLabel = typeLabel;
+ mLookupKey = lookupKey;
}
public String getPhoneNumber() {
@@ -208,6 +213,10 @@
return mDisplayName;
}
+ public String getDisplayNameAlt() {
+ return mDisplayNameAlt;
+ }
+
/**
* Returns the initials of the contact related to the phone number. Returns null if there is
* no related contact.
@@ -226,6 +235,12 @@
return mTypeLabel;
}
+ /** Returns the lookup key of the contact if any is found. */
+ @Nullable
+ public String getLookupKey() {
+ return mLookupKey;
+ }
+
}
/**
@@ -240,97 +255,131 @@
return CompletableFuture.completedFuture(new PhoneNumberInfo(
number,
context.getString(R.string.unknown),
+ context.getString(R.string.unknown),
null,
null,
- ""));
+ "",
+ null));
}
if (isVoicemailNumber(context, number)) {
return CompletableFuture.completedFuture(new PhoneNumberInfo(
number,
context.getString(R.string.voicemail),
+ context.getString(R.string.voicemail),
null,
makeResourceUri(context, R.drawable.ic_voicemail),
- ""));
+ "",
+ null));
+ }
+
+ return CompletableFuture.supplyAsync(() -> lookupNumberInBackground(context, number));
+ }
+
+ /** Lookup phone number info in background. */
+ @WorkerThread
+ public static PhoneNumberInfo lookupNumberInBackground(Context context, String number) {
+ if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS)
+ != PackageManager.PERMISSION_GRANTED) {
+ String readableNumber = getReadableNumber(context, number);
+ return new PhoneNumberInfo(number, readableNumber, readableNumber, null, null, null,
+ null);
}
if (InMemoryPhoneBook.isInitialized()) {
Contact contact = InMemoryPhoneBook.get().lookupContactEntry(number);
if (contact != null) {
String name = contact.getDisplayName();
- if (name == null) {
- name = getFormattedNumber(context, number);
+ String nameAlt = contact.getDisplayNameAlt();
+ if (TextUtils.isEmpty(name)) {
+ name = getReadableNumber(context, number);
}
-
- if (name == null) {
- name = context.getString(R.string.unknown);
+ if (TextUtils.isEmpty(nameAlt)) {
+ nameAlt = name;
}
PhoneNumber phoneNumber = contact.getPhoneNumber(context, number);
- CharSequence typeLabel = "";
- if (phoneNumber != null) {
- typeLabel = Phone.getTypeLabel(context.getResources(),
- phoneNumber.getType(),
- phoneNumber.getLabel());
- }
+ CharSequence typeLabel = phoneNumber == null ? "" : phoneNumber.getReadableLabel(
+ context.getResources());
- return CompletableFuture.completedFuture(new PhoneNumberInfo(
+ return new PhoneNumberInfo(
number,
name,
+ nameAlt,
contact.getInitials(),
contact.getAvatarUri(),
- typeLabel.toString()));
+ typeLabel.toString(),
+ contact.getLookupKey());
+ }
+ } else {
+ L.d(TAG, "InMemoryPhoneBook not initialized.");
+ }
+
+ String name = null;
+ String nameAlt = null;
+ String initials = null;
+ String photoUriString = null;
+ CharSequence typeLabel = "";
+ String lookupKey = null;
+
+ ContentResolver cr = context.getContentResolver();
+ try (Cursor cursor = cr.query(
+ Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)),
+ new String[]{
+ PhoneLookup.DISPLAY_NAME,
+ PhoneLookup.DISPLAY_NAME_ALTERNATIVE,
+ PhoneLookup.PHOTO_URI,
+ PhoneLookup.TYPE,
+ PhoneLookup.LABEL,
+ PhoneLookup.LOOKUP_KEY,
+ },
+ null, null, null)) {
+
+ if (cursor != null && cursor.moveToFirst()) {
+ int nameColumn = cursor.getColumnIndex(PhoneLookup.DISPLAY_NAME);
+ int altNameColumn = cursor.getColumnIndex(PhoneLookup.DISPLAY_NAME_ALTERNATIVE);
+ int photoUriColumn = cursor.getColumnIndex(PhoneLookup.PHOTO_URI);
+ int typeColumn = cursor.getColumnIndex(PhoneLookup.TYPE);
+ int labelColumn = cursor.getColumnIndex(PhoneLookup.LABEL);
+ int lookupKeyColumn = cursor.getColumnIndex(PhoneLookup.LOOKUP_KEY);
+
+ name = cursor.getString(nameColumn);
+ nameAlt = cursor.getString(altNameColumn);
+ photoUriString = cursor.getString(photoUriColumn);
+ initials = getInitials(name, nameAlt);
+
+ int type = cursor.getInt(typeColumn);
+ String label = cursor.getString(labelColumn);
+ typeLabel = Phone.getTypeLabel(context.getResources(), type, label);
+
+ lookupKey = cursor.getString(lookupKeyColumn);
}
}
- return CompletableFuture.supplyAsync(() -> {
- String name = null;
- String nameAlt = null;
- String photoUriString = null;
- CharSequence typeLabel = "";
- ContentResolver cr = context.getContentResolver();
- String initials;
- try (Cursor cursor = cr.query(
- Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)),
- new String[]{
- PhoneLookup.DISPLAY_NAME,
- PhoneLookup.DISPLAY_NAME_ALTERNATIVE,
- PhoneLookup.PHOTO_URI,
- PhoneLookup.TYPE,
- PhoneLookup.LABEL,
- },
- null, null, null)) {
+ if (TextUtils.isEmpty(name)) {
+ name = getReadableNumber(context, number);
+ }
+ if (TextUtils.isEmpty(nameAlt)) {
+ nameAlt = name;
+ }
- if (cursor != null && cursor.moveToFirst()) {
- int nameColumn = cursor.getColumnIndex(PhoneLookup.DISPLAY_NAME);
- int altNameColumn = cursor.getColumnIndex(PhoneLookup.DISPLAY_NAME_ALTERNATIVE);
- int photoUriColumn = cursor.getColumnIndex(PhoneLookup.PHOTO_URI);
- int typeColumn = cursor.getColumnIndex(PhoneLookup.TYPE);
- int labelColumn = cursor.getColumnIndex(PhoneLookup.LABEL);
+ return new PhoneNumberInfo(
+ number,
+ name,
+ nameAlt,
+ initials,
+ TextUtils.isEmpty(photoUriString) ? null : Uri.parse(photoUriString),
+ typeLabel.toString(),
+ lookupKey);
+ }
- name = cursor.getString(nameColumn);
- nameAlt = cursor.getString(altNameColumn);
- photoUriString = cursor.getString(photoUriColumn);
- int type = cursor.getInt(typeColumn);
- String label = cursor.getString(labelColumn);
- typeLabel = Phone.getTypeLabel(context.getResources(), type, label);
- }
- }
+ private static String getReadableNumber(Context context, String number) {
+ String readableNumber = getFormattedNumber(context, number);
- initials = getInitials(name, nameAlt);
-
- if (name == null) {
- name = getFormattedNumber(context, number);
- }
-
- if (name == null) {
- name = context.getString(R.string.unknown);
- }
-
- return new PhoneNumberInfo(number, name, initials,
- TextUtils.isEmpty(photoUriString) ? null : Uri.parse(photoUriString),
- typeLabel.toString());
- });
+ if (readableNumber == null) {
+ readableNumber = context.getString(R.string.unknown);
+ }
+ return readableNumber;
}
/**
@@ -476,6 +525,11 @@
* Set the given phone number as the primary phone number for its associated contact.
*/
public static void setAsPrimaryPhoneNumber(Context context, PhoneNumber phoneNumber) {
+ if (context.checkSelfPermission(Manifest.permission.WRITE_CONTACTS)
+ != PackageManager.PERMISSION_GRANTED) {
+ L.w(TAG, "Missing WRITE_CONTACTS permission, not setting primary number.");
+ return;
+ }
// Update the primary values in the data record.
ContentValues values = new ContentValues(1);
values.put(ContactsContract.Data.IS_SUPER_PRIMARY, 1);
@@ -487,23 +541,6 @@
}
/**
- * Add a contact to favorite or remove it from favorite.
- */
- public static int setAsFavoriteContact(Context context, Contact contact, boolean isFavorite) {
- if (contact.isStarred() == isFavorite) {
- return 0;
- }
-
- ContentValues values = new ContentValues(1);
- values.put(ContactsContract.Contacts.STARRED, isFavorite ? 1 : 0);
-
- String where = ContactsContract.Contacts._ID + " = ?";
- String[] selectionArgs = new String[]{Long.toString(contact.getId())};
- return context.getContentResolver().update(ContactsContract.Contacts.CONTENT_URI, values,
- where, selectionArgs);
- }
-
- /**
* Mark missed call log matching given phone number as read. If phone number string is not
* valid, it will mark all new missed call log as read.
*/
diff --git a/car-ui-lib/Android.bp b/car-ui-lib/Android.bp
index 4e59773..c630252 100644
--- a/car-ui-lib/Android.bp
+++ b/car-ui-lib/Android.bp
@@ -94,6 +94,7 @@
libs: [
"android.test.runner",
"android.test.base",
+ "android.test.mock.stubs",
"android.car"
],
manifest: "car-ui-lib/src/androidTest/AndroidManifest.xml",
@@ -132,11 +133,11 @@
platform_apis: true,
certificate: "platform",
privileged: true,
+ libs: ["android.car-stubs"],
static_libs: [
"car-ui-lib",
- "android.car.userlib",
- "guava",
- "gson-prebuilt-jar",
+ "guava",
+ "gson-prebuilt-jar",
],
optimize: {
enabled: false,
diff --git a/car-ui-lib/car-ui-lib/AndroidManifest-gradle.xml b/car-ui-lib/car-ui-lib/AndroidManifest-gradle.xml
index 36ffbb8..8ffb21c 100644
--- a/car-ui-lib/car-ui-lib/AndroidManifest-gradle.xml
+++ b/car-ui-lib/car-ui-lib/AndroidManifest-gradle.xml
@@ -17,10 +17,18 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.car.ui">
- <application>
+
+ <application>
<provider
android:name="com.android.car.ui.core.CarUiInstaller"
android:authorities="${applicationId}.CarUiInstaller"
android:exported="false"/>
+
+ <provider
+ android:name="com.android.car.ui.core.SearchResultsProvider"
+ android:authorities="${applicationId}.SearchResultsProvider"
+ android:exported="true"
+ android:process="@string/car_ui_installer_process_name"
+ android:readPermission="com.android.car.ui.READ_SEARCH_RESULTS"/>
</application>
</manifest>
diff --git a/car-ui-lib/car-ui-lib/AndroidManifest.xml b/car-ui-lib/car-ui-lib/AndroidManifest.xml
index a85261a..2a3d658 100644
--- a/car-ui-lib/car-ui-lib/AndroidManifest.xml
+++ b/car-ui-lib/car-ui-lib/AndroidManifest.xml
@@ -33,6 +33,7 @@
android:name="com.android.car.ui.core.SearchResultsProvider"
android:authorities="${applicationId}.SearchResultsProvider"
android:exported="true"
- android:process="@string/car_ui_installer_process_name"/>
+ android:process="@string/car_ui_installer_process_name"
+ android:readPermission="com.android.car.ui.READ_SEARCH_RESULTS"/>
</application>
</manifest>
diff --git a/car-ui-lib/car-ui-lib/build.gradle b/car-ui-lib/car-ui-lib/build.gradle
index 201a8d6..6a18ed4 100644
--- a/car-ui-lib/car-ui-lib/build.gradle
+++ b/car-ui-lib/car-ui-lib/build.gradle
@@ -49,6 +49,10 @@
// This is the gradle equivalent of the libs: ["android.car"] in the Android.bp
// Which will be in the SDK 30 R3+
useLibrary 'android.car'
+
+ useLibrary 'android.test.runner'
+ useLibrary 'android.test.base'
+ useLibrary 'android.test.mock'
}
dependencies {
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/AndroidManifest.xml b/car-ui-lib/car-ui-lib/src/androidTest/AndroidManifest.xml
index 79b51bb..9bb5dfa 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/AndroidManifest.xml
+++ b/car-ui-lib/car-ui-lib/src/androidTest/AndroidManifest.xml
@@ -31,6 +31,12 @@
<activity
android:name="com.android.car.ui.toolbar.ToolbarTestActivity"
android:theme="@style/Theme.CarUi.WithToolbar"/>
+ <provider
+ android:name="com.android.car.ui.core.SearchResultsProvider"
+ android:authorities="${applicationId}.SearchResultsProvider"
+ android:exported="true"
+ android:process="@string/car_ui_installer_process_name"
+ android:readPermission="com.android.car.ui.READ_SEARCH_RESULTS"/>
</application>
<instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
android:targetPackage="com.android.car.ui.test"
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FocusAreaTest.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FocusAreaTest.java
index b0e7c88..eff475c 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FocusAreaTest.java
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FocusAreaTest.java
@@ -35,6 +35,7 @@
import static com.android.car.ui.utils.RotaryConstants.NUDGE_DIRECTION;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
import android.os.Bundle;
import android.view.View;
@@ -66,6 +67,7 @@
private TestFocusArea mFocusArea2;
private TestFocusArea mFocusArea3;
private TestFocusArea mFocusArea4;
+ private TestFocusArea mFocusArea5;
private FocusParkingView mFpv;
private View mView1;
private Button mButton1;
@@ -74,6 +76,8 @@
private View mView3;
private View mNudgeShortcut3;
private View mView4;
+ private View mView5;
+ private Button mButton5;
@Before
public void setUp() {
@@ -82,6 +86,7 @@
mFocusArea2 = mActivity.findViewById(R.id.focus_area2);
mFocusArea3 = mActivity.findViewById(R.id.focus_area3);
mFocusArea4 = mActivity.findViewById(R.id.focus_area4);
+ mFocusArea5 = mActivity.findViewById(R.id.focus_area5);
mFpv = mActivity.findViewById(R.id.fpv);
mView1 = mActivity.findViewById(R.id.view1);
mButton1 = mActivity.findViewById(R.id.button1);
@@ -90,6 +95,8 @@
mView3 = mActivity.findViewById(R.id.view3);
mNudgeShortcut3 = mActivity.findViewById(R.id.nudge_shortcut3);
mView4 = mActivity.findViewById(R.id.view4);
+ mView5 = mActivity.findViewById(R.id.view5);
+ mButton5 = mActivity.findViewById(R.id.button5);
}
@Test
@@ -344,7 +351,6 @@
RotaryCache cache =
new RotaryCache(CACHE_TYPE_NEVER_EXPIRE, 0, CACHE_TYPE_NEVER_EXPIRE, 0);
mFocusArea2.setRotaryCache(cache);
- mFocusArea2.setDefaultFocusOverridesHistory(false);
mView2.requestFocus();
assertThat(mView2.isFocused()).isTrue();
@@ -507,6 +513,44 @@
});
}
+ @Test
+ public void testWrapAround() {
+ mFocusArea1.post(() -> {
+ mView5.requestFocus();
+ assertThat(mView5.isFocused()).isTrue();
+
+ View focusSearch = mView5.focusSearch(View.FOCUS_FORWARD);
+
+ assertWithMessage("Forward wrap-around").that(focusSearch).isEqualTo(mButton5);
+
+ mButton5.requestFocus();
+ assertThat(mButton5.isFocused()).isTrue();
+
+ focusSearch = mButton5.focusSearch(View.FOCUS_BACKWARD);
+
+ assertWithMessage("Backward wrap-around").that(focusSearch).isEqualTo(mView5);
+ });
+ }
+
+ @Test
+ public void testNoWrapAround() {
+ mFocusArea1.post(() -> {
+ mButton1.requestFocus();
+ assertThat(mButton1.isFocused()).isTrue();
+
+ View focusSearch = mButton1.focusSearch(View.FOCUS_FORWARD);
+
+ assertWithMessage("Forward wrap-around").that(focusSearch).isNotEqualTo(mView1);
+
+ mView1.requestFocus();
+ assertThat(mView1.isFocused()).isTrue();
+
+ focusSearch = mView1.focusSearch(View.FOCUS_BACKWARD);
+
+ assertWithMessage("Backward wrap-around").that(focusSearch).isNotEqualTo(mButton1);
+ });
+ }
+
private void assertBoundsOffset(
@NonNull AccessibilityNodeInfo node, int leftPx, int topPx, int rightPx, int bottomPx) {
Bundle extras = node.getExtras();
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/core/ResolverRenamingMockContext.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/core/ResolverRenamingMockContext.java
new file mode 100644
index 0000000..95bf762
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/core/ResolverRenamingMockContext.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2020 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.ui.core;
+
+import android.content.ContentProvider;
+import android.content.Context;
+import android.test.IsolatedContext;
+import android.test.mock.MockContentResolver;
+import android.test.mock.MockContext;
+
+public class ResolverRenamingMockContext extends IsolatedContext {
+
+ /**
+ * The renaming prefix.
+ */
+ private static final String PREFIX = "test.";
+
+
+ /**
+ * The resolver.
+ */
+ private static final MockContentResolver RESOLVER = new MockContentResolver();
+
+ /**
+ * Constructor.
+ */
+ public ResolverRenamingMockContext(Context context) {
+ super(RESOLVER, new DelegatedMockContext(context));
+ }
+
+ public MockContentResolver getResolver() {
+ return RESOLVER;
+ }
+
+ public void addProvider(String name, ContentProvider provider) {
+ RESOLVER.addProvider(name, provider);
+ }
+
+ /**
+ * The DelegatedMockContext.
+ */
+ private static class DelegatedMockContext extends MockContext {
+
+ private Context mDelegatedContext;
+
+ DelegatedMockContext(Context context) {
+ mDelegatedContext = context;
+ }
+
+ @Override
+ public String getPackageName() {
+ return "com.android.car.ui.test";
+ }
+ }
+
+}
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/core/SearchResultsProviderTest.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/core/SearchResultsProviderTest.java
new file mode 100644
index 0000000..f93e788
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/core/SearchResultsProviderTest.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2020 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.ui.core;
+
+import static com.android.car.ui.core.SearchResultsProvider.CONTENT;
+import static com.android.car.ui.core.SearchResultsProvider.ITEM_ID;
+import static com.android.car.ui.core.SearchResultsProvider.PRIMARY_IMAGE_BLOB;
+import static com.android.car.ui.core.SearchResultsProvider.SEARCH_RESULTS_PROVIDER;
+import static com.android.car.ui.core.SearchResultsProvider.SECONDARY_IMAGE_BLOB;
+import static com.android.car.ui.core.SearchResultsProvider.SECONDARY_IMAGE_ID;
+import static com.android.car.ui.core.SearchResultsProvider.SUBTITLE;
+import static com.android.car.ui.core.SearchResultsProvider.TITLE;
+
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.net.Uri;
+import android.os.Parcel;
+import android.test.ProviderTestCase2;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.car.ui.test.R;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Unit tests for {@link SearchResultsProvider}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class SearchResultsProviderTest extends ProviderTestCase2<SearchResultsProvider> {
+
+ public static final String AUTHORITY =
+ CONTENT + "com.android.car.ui.test" + SEARCH_RESULTS_PROVIDER;
+
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+ private ResolverRenamingMockContext mProviderContext;
+ private Class<SearchResultsProvider> mProviderClass;
+ private SearchResultsProvider mProvider;
+
+ public SearchResultsProviderTest() {
+ super(SearchResultsProvider.class, AUTHORITY);
+ setName("ProviderSampleTests");
+ mProviderClass = SearchResultsProvider.class;
+ }
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ mProviderContext = new ResolverRenamingMockContext(getContext());
+ mProvider = mProviderClass.newInstance();
+ assertNotNull(mProvider);
+ mProvider.attachInfo(mProviderContext, null);
+ mProviderContext.addProvider(AUTHORITY, mProvider);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Override
+ public SearchResultsProvider getProvider() {
+ return mProvider;
+ }
+
+ @Test
+ public void insert_shouldInsertTheSearchResult() {
+ ContentValues values = getRecord();
+
+ SearchResultsProvider provider = getProvider();
+
+ Uri uri = provider.insert(Uri.parse(AUTHORITY), values);
+ // length - 1
+ assertEquals(0L, ContentUris.parseId(uri));
+ }
+
+ @Test
+ public void query_shouldHaveValidData() {
+ ContentValues values = getRecord();
+
+ SearchResultsProvider provider = getProvider();
+ provider.insert(Uri.parse(AUTHORITY), values);
+ Cursor cursor = provider.query(Uri.parse(AUTHORITY), null, null, null, null);
+
+ assertNotNull(cursor);
+ assertEquals(1, cursor.getCount());
+ assertTrue(cursor.moveToFirst());
+ assertEquals("1", cursor.getString(cursor.getColumnIndex(ITEM_ID)));
+ assertEquals("1", cursor.getString(cursor.getColumnIndex(SECONDARY_IMAGE_ID)));
+ assertNotNull(cursor.getBlob(cursor.getColumnIndex(PRIMARY_IMAGE_BLOB)));
+ assertNotNull(cursor.getBlob(cursor.getColumnIndex(SECONDARY_IMAGE_BLOB)));
+ assertEquals("Title", cursor.getString(cursor.getColumnIndex(TITLE)));
+ assertEquals("SubTitle", cursor.getString(cursor.getColumnIndex(SUBTITLE)));
+ }
+
+ private byte[] bitmapToByteArray(Bitmap bitmap) {
+ Parcel parcel = Parcel.obtain();
+ bitmap.writeToParcel(parcel, 0);
+ byte[] bytes = parcel.marshall();
+ parcel.recycle();
+ return bytes;
+ }
+
+ private ContentValues getRecord() {
+ ContentValues values = new ContentValues();
+ int id = 1;
+ values.put(ITEM_ID, id);
+ values.put(SECONDARY_IMAGE_ID, id);
+ BitmapDrawable icon = (BitmapDrawable) mContext.getDrawable(R.drawable.ic_launcher);
+ values.put(PRIMARY_IMAGE_BLOB,
+ icon != null ? bitmapToByteArray(icon.getBitmap()) : null);
+ values.put(SECONDARY_IMAGE_BLOB,
+ icon != null ? bitmapToByteArray(icon.getBitmap()) : null);
+ values.put(TITLE, "Title");
+ values.put(SUBTITLE, "SubTitle");
+ return values;
+ }
+}
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/imewidescreen/CarUiImeWideScreenControllerTest.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/imewidescreen/CarUiImeWideScreenControllerTest.java
index 11d33f4..352a73a 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/imewidescreen/CarUiImeWideScreenControllerTest.java
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/imewidescreen/CarUiImeWideScreenControllerTest.java
@@ -19,59 +19,95 @@
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.assertThat;
+import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
+import static com.android.car.ui.core.SearchResultsProvider.ITEM_ID;
+import static com.android.car.ui.core.SearchResultsProvider.SECONDARY_IMAGE_ID;
+import static com.android.car.ui.core.SearchResultsProvider.SUBTITLE;
+import static com.android.car.ui.core.SearchResultsProvider.TITLE;
+import static com.android.car.ui.core.SearchResultsProviderTest.AUTHORITY;
+import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.ADD_DESC_TITLE_TO_CONTENT_AREA;
+import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.ADD_DESC_TO_CONTENT_AREA;
+import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.ADD_ERROR_DESC_TO_INPUT_AREA;
+import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.CONTENT_AREA_SURFACE_PACKAGE;
import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.REQUEST_RENDER_CONTENT_AREA;
import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.WIDE_SCREEN_ACTION;
+import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.WIDE_SCREEN_SEARCH_RESULTS;
+import static com.android.car.ui.imewidescreen.CarUiImeWideScreenTestActivity.sCarUiImeWideScreenController;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;
import android.app.Dialog;
+import android.content.ContentResolver;
+import android.content.ContentValues;
import android.content.Context;
import android.inputmethodservice.ExtractEditText;
import android.inputmethodservice.InputMethodService;
import android.inputmethodservice.InputMethodService.Insets;
+import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
+import android.view.SurfaceControlViewHost;
import android.view.View;
import android.view.Window;
+import android.view.inputmethod.EditorInfo;
import android.widget.FrameLayout;
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.test.InstrumentationRegistry;
import androidx.test.core.app.ApplicationProvider;
+import androidx.test.espresso.matcher.BoundedMatcher;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.rule.ActivityTestRule;
import com.android.car.ui.test.R;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
+import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
/**
* Unit tests for {@link CarUiImeWideScreenController}.
*/
+@RunWith(AndroidJUnit4.class)
public class CarUiImeWideScreenControllerTest {
- private Context mContext = ApplicationProvider.getApplicationContext();
+ private final Context mContext = ApplicationProvider.getApplicationContext();
@Mock
- Context mMockContext;
+ private EditorInfo mEditorInfoMock;
@Mock
- InputMethodService mInputMethodService;
+ private SurfaceControlViewHost.SurfacePackage mSurfacePackageMock;
@Mock
- Dialog mDialog;
+ private InputMethodService mInputMethodService;
@Mock
- Window mWindow;
+ private Dialog mDialog;
+
+ @Mock
+ private Window mWindow;
private CarUiImeWideScreenTestActivity mActivity;
@@ -103,6 +139,7 @@
onView(withId(R.id.car_ui_wideScreenErrorMessage)).check(matches(not(isDisplayed())));
onView(withId(R.id.car_ui_wideScreenError)).check(matches(not(isDisplayed())));
onView(withId(R.id.car_ui_contentAreaAutomotive)).check(matches(not(isDisplayed())));
+ onView(withId(R.id.car_ui_ime_surface)).check(matches(not(isDisplayed())));
onView(withId(R.id.car_ui_wideScreenExtractedTextIcon)).check(matches(isDisplayed()));
onView(withId(R.id.car_ui_wideScreenClearData)).check(matches(isDisplayed()));
@@ -160,6 +197,123 @@
assertThat(outInsets.visibleTopInsets, is(200));
}
+ @Test
+ public void onAppPrivateCommand_shouldShowTitleAndDesc() {
+ when(mInputMethodService.getWindow()).thenReturn(mDialog);
+ when(mDialog.getWindow()).thenReturn(mWindow);
+
+ sCarUiImeWideScreenController.setExtractEditText(new ExtractEditText(mContext));
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ Bundle bundle = new Bundle();
+ bundle.putString(ADD_DESC_TITLE_TO_CONTENT_AREA, "Title");
+ bundle.putString(ADD_DESC_TO_CONTENT_AREA, "Description");
+ sCarUiImeWideScreenController.onAppPrivateCommand(WIDE_SCREEN_ACTION, bundle);
+ });
+
+ onView(withId(R.id.car_ui_wideScreenDescriptionTitle)).check(matches(isDisplayed()));
+ onView(withId(R.id.car_ui_wideScreenDescriptionTitle)).check(
+ matches(withText(containsString("Title"))));
+
+ onView(withId(R.id.car_ui_wideScreenDescription)).check(matches(isDisplayed()));
+ onView(withId(R.id.car_ui_wideScreenDescription)).check(
+ matches(withText(containsString("Description"))));
+ }
+
+ @Test
+ public void onAppPrivateCommand_shouldShowErrorMessage() {
+ when(mInputMethodService.getWindow()).thenReturn(mDialog);
+ when(mDialog.getWindow()).thenReturn(mWindow);
+
+ sCarUiImeWideScreenController.setExtractEditText(new ExtractEditText(mContext));
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ Bundle bundle = new Bundle();
+ bundle.putString(ADD_ERROR_DESC_TO_INPUT_AREA, "Error Message");
+ sCarUiImeWideScreenController.onAppPrivateCommand(WIDE_SCREEN_ACTION, bundle);
+ });
+
+ onView(withId(R.id.car_ui_wideScreenErrorMessage)).check(matches(isDisplayed()));
+ onView(withId(R.id.car_ui_wideScreenErrorMessage)).check(
+ matches(withText(containsString("Error Message"))));
+ onView(withId(R.id.car_ui_wideScreenError)).check(matches(isDisplayed()));
+ }
+
+ @Test
+ public void onAppPrivateCommand_shouldShowSearchResults() {
+ ContentResolver cr = ApplicationProvider.getApplicationContext().getContentResolver();
+ cr.insert(Uri.parse(AUTHORITY), getRecord());
+
+ when(mInputMethodService.getWindow()).thenReturn(mDialog);
+ when(mDialog.getWindow()).thenReturn(mWindow);
+
+ sCarUiImeWideScreenController.setExtractEditText(new ExtractEditText(mContext));
+ sCarUiImeWideScreenController.setEditorInfo(mEditorInfoMock);
+
+ CarUiImeWideScreenController spy = Mockito.spy(sCarUiImeWideScreenController);
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ doReturn("com.android.car.ui.test").when(spy).getPackageName(ArgumentMatchers.any());
+ Bundle bundle = new Bundle();
+ spy.onAppPrivateCommand(WIDE_SCREEN_SEARCH_RESULTS, bundle);
+ });
+
+ onView(withId(R.id.car_ui_wideScreenSearchResultList)).check(matches(isDisplayed()));
+ onView(withId(R.id.car_ui_wideScreenSearchResultList))
+ .check(matches(atPosition(0, hasDescendant(withText("Title")))));
+ onView(withId(R.id.car_ui_wideScreenSearchResultList))
+ .check(matches(atPosition(0, hasDescendant(withText("SubTitle")))));
+ }
+
+ @Test
+ public void onAppPrivateCommand_shouldShowSurfaceView() {
+ when(mInputMethodService.getWindow()).thenReturn(mDialog);
+ when(mDialog.getWindow()).thenReturn(mWindow);
+
+ sCarUiImeWideScreenController.setExtractEditText(new ExtractEditText(mContext));
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(CONTENT_AREA_SURFACE_PACKAGE, mSurfacePackageMock);
+ sCarUiImeWideScreenController.onAppPrivateCommand(WIDE_SCREEN_ACTION, bundle);
+ });
+
+ onView(withId(R.id.car_ui_ime_surface)).check(matches(isDisplayed()));
+ }
+
+ private static ContentValues getRecord() {
+ ContentValues values = new ContentValues();
+ int id = 1;
+ values.put(ITEM_ID, id);
+ values.put(SECONDARY_IMAGE_ID, id);
+ values.put(TITLE, "Title");
+ values.put(SUBTITLE, "SubTitle");
+ return values;
+ }
+
+ private static Matcher<View> atPosition(final int position,
+ @NonNull final Matcher<View> itemMatcher) {
+ checkNotNull(itemMatcher);
+ return new BoundedMatcher<View, RecyclerView>(RecyclerView.class) {
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("has item at position " + position + ": ");
+ itemMatcher.describeTo(description);
+ }
+
+ @Override
+ protected boolean matchesSafely(final RecyclerView view) {
+ RecyclerView.ViewHolder viewHolder = view.findViewHolderForAdapterPosition(
+ position);
+ if (viewHolder == null) {
+ // has no item on such position
+ return false;
+ }
+ return itemMatcher.matches(viewHolder.itemView);
+ }
+ };
+ }
+
private CarUiImeWideScreenController getController() {
return new CarUiImeWideScreenController(mContext, mInputMethodService) {
@Override
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/preference/PreferenceTest.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/preference/PreferenceTest.java
index a955cfd..b116706 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/preference/PreferenceTest.java
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/preference/PreferenceTest.java
@@ -85,15 +85,20 @@
mActivity.setOnPreferenceChangeListener("list", mockListener);
// Check that no option is initially selected.
- onView(withIndex(withId(R.id.radio_button_widget), 1)).check(matches(isNotChecked()));
- onView(withIndex(withId(R.id.radio_button_widget), 2)).check(matches(isNotChecked()));
+ onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 1))
+ .check(matches(isNotChecked()));
+ onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 2))
+ .check(matches(isNotChecked()));
// Select first option.
onView(withText(mEntries[0])).perform(click());
// Check that first option is selected.
- onView(withIndex(withId(R.id.radio_button_widget), 0)).check(matches(isChecked()));
- onView(withIndex(withId(R.id.radio_button_widget), 1)).check(matches(isNotChecked()));
- onView(withIndex(withId(R.id.radio_button_widget), 2)).check(matches(isNotChecked()));
+ onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 0))
+ .check(matches(isChecked()));
+ onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 1))
+ .check(matches(isNotChecked()));
+ onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 2))
+ .check(matches(isNotChecked()));
// Press back to save selection.
onView(withId(R.id.car_ui_toolbar_nav_icon)).perform(click());
@@ -103,16 +108,22 @@
onView(withText(R.string.title_list_preference)).perform(click());
// Check that first option is selected.
- onView(withIndex(withId(R.id.radio_button_widget), 0)).check(matches(isChecked()));
- onView(withIndex(withId(R.id.radio_button_widget), 1)).check(matches(isNotChecked()));
- onView(withIndex(withId(R.id.radio_button_widget), 2)).check(matches(isNotChecked()));
+ onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 0))
+ .check(matches(isChecked()));
+ onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 1))
+ .check(matches(isNotChecked()));
+ onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 2))
+ .check(matches(isNotChecked()));
// Select second option.
onView(withText(mEntries[1])).perform(click());
// Check that second option is selected.
- onView(withIndex(withId(R.id.radio_button_widget), 0)).check(matches(isNotChecked()));
- onView(withIndex(withId(R.id.radio_button_widget), 1)).check(matches(isChecked()));
- onView(withIndex(withId(R.id.radio_button_widget), 2)).check(matches(isNotChecked()));
+ onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 0))
+ .check(matches(isNotChecked()));
+ onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 1))
+ .check(matches(isChecked()));
+ onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 2))
+ .check(matches(isNotChecked()));
// Press back to save selection.
onView(withId(R.id.car_ui_toolbar_nav_icon)).perform(click());
@@ -122,9 +133,12 @@
onView(withText(R.string.title_list_preference)).perform(click());
// Check that second option is selected.
- onView(withIndex(withId(R.id.radio_button_widget), 0)).check(matches(isNotChecked()));
- onView(withIndex(withId(R.id.radio_button_widget), 1)).check(matches(isChecked()));
- onView(withIndex(withId(R.id.radio_button_widget), 2)).check(matches(isNotChecked()));
+ onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 0))
+ .check(matches(isNotChecked()));
+ onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 1))
+ .check(matches(isChecked()));
+ onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 2))
+ .check(matches(isNotChecked()));
}
@Test
@@ -141,18 +155,24 @@
mActivity.setOnPreferenceChangeListener("multi_select_list", mockListener);
// Check that no option is initially selected.
- onView(withIndex(withId(R.id.checkbox_widget), 0)).check(matches(isNotChecked()));
- onView(withIndex(withId(R.id.checkbox_widget), 1)).check(matches(isNotChecked()));
- onView(withIndex(withId(R.id.checkbox_widget), 2)).check(matches(isNotChecked()));
+ onView(withIndex(withId(R.id.car_ui_list_item_checkbox_widget), 0))
+ .check(matches(isNotChecked()));
+ onView(withIndex(withId(R.id.car_ui_list_item_checkbox_widget), 1))
+ .check(matches(isNotChecked()));
+ onView(withIndex(withId(R.id.car_ui_list_item_checkbox_widget), 2))
+ .check(matches(isNotChecked()));
// Select options 1 and 3.
onView(withText(mEntries[0])).perform(click());
onView(withText(mEntries[2])).perform(click());
// Check that selections are correctly reflected.
- onView(withIndex(withId(R.id.checkbox_widget), 0)).check(matches(isChecked()));
- onView(withIndex(withId(R.id.checkbox_widget), 1)).check(matches(isNotChecked()));
- onView(withIndex(withId(R.id.checkbox_widget), 2)).check(matches(isChecked()));
+ onView(withIndex(withId(R.id.car_ui_list_item_checkbox_widget), 0))
+ .check(matches(isChecked()));
+ onView(withIndex(withId(R.id.car_ui_list_item_checkbox_widget), 1))
+ .check(matches(isNotChecked()));
+ onView(withIndex(withId(R.id.car_ui_list_item_checkbox_widget), 2))
+ .check(matches(isChecked()));
// Press back to save selection.
onView(withId(R.id.car_ui_toolbar_nav_icon)).perform(click());
@@ -166,9 +186,12 @@
onView(withText(R.string.title_multi_list_preference)).perform(click());
// Check that selections are correctly reflected.
- onView(withIndex(withId(R.id.checkbox_widget), 0)).check(matches(isChecked()));
- onView(withIndex(withId(R.id.checkbox_widget), 1)).check(matches(isNotChecked()));
- onView(withIndex(withId(R.id.checkbox_widget), 2)).check(matches(isChecked()));
+ onView(withIndex(withId(R.id.car_ui_list_item_checkbox_widget), 0))
+ .check(matches(isChecked()));
+ onView(withIndex(withId(R.id.car_ui_list_item_checkbox_widget), 1))
+ .check(matches(isNotChecked()));
+ onView(withIndex(withId(R.id.car_ui_list_item_checkbox_widget), 2))
+ .check(matches(isChecked()));
}
@Test
@@ -277,16 +300,22 @@
mActivity.setOnPreferenceChangeListener("dropdown", mockListener);
// Check that first option is initially selected.
- onView(withIndex(withId(R.id.radio_button_widget), 0)).check(matches(isChecked()));
- onView(withIndex(withId(R.id.radio_button_widget), 1)).check(matches(isNotChecked()));
- onView(withIndex(withId(R.id.radio_button_widget), 2)).check(matches(isNotChecked()));
+ onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 0))
+ .check(matches(isChecked()));
+ onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 1))
+ .check(matches(isNotChecked()));
+ onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 2))
+ .check(matches(isNotChecked()));
// Select third option.
onView(withText(mEntries[2])).perform(click());
// Check that first option is selected.
- onView(withIndex(withId(R.id.radio_button_widget), 0)).check(matches(isNotChecked()));
- onView(withIndex(withId(R.id.radio_button_widget), 1)).check(matches(isNotChecked()));
- onView(withIndex(withId(R.id.radio_button_widget), 2)).check(matches(isChecked()));
+ onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 0))
+ .check(matches(isNotChecked()));
+ onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 1))
+ .check(matches(isNotChecked()));
+ onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 2))
+ .check(matches(isChecked()));
// Press back to save selection.
onView(withId(R.id.car_ui_toolbar_nav_icon)).perform(click());
@@ -296,9 +325,12 @@
onView(withText(R.string.title_dropdown_preference)).perform(click());
// Check that first option is selected.
- onView(withIndex(withId(R.id.radio_button_widget), 0)).check(matches(isNotChecked()));
- onView(withIndex(withId(R.id.radio_button_widget), 1)).check(matches(isNotChecked()));
- onView(withIndex(withId(R.id.radio_button_widget), 2)).check(matches(isChecked()));
+ onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 0))
+ .check(matches(isNotChecked()));
+ onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 1))
+ .check(matches(isNotChecked()));
+ onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 2))
+ .check(matches(isChecked()));
}
@Test
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/recyclerview/CarUiListItemTest.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/recyclerview/CarUiListItemTest.java
index da446e1..3480ac9 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/recyclerview/CarUiListItemTest.java
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/recyclerview/CarUiListItemTest.java
@@ -71,10 +71,10 @@
mCarUiRecyclerView.post(
() -> mCarUiRecyclerView.setAdapter(new CarUiListItemAdapter(items)));
- onView(withId(R.id.title)).check(matches(isDisplayed()));
- onView(withId(R.id.body)).check(matches(not(isDisplayed())));
- onView(withId(R.id.icon_container)).check(matches(not(isDisplayed())));
- onView(withId(R.id.action_container)).check(matches(not(isDisplayed())));
+ onView(withId(R.id.car_ui_list_item_title)).check(matches(isDisplayed()));
+ onView(withId(R.id.car_ui_list_item_body)).check(matches(not(isDisplayed())));
+ onView(withId(R.id.car_ui_list_item_icon_container)).check(matches(not(isDisplayed())));
+ onView(withId(R.id.car_ui_list_item_action_container)).check(matches(not(isDisplayed())));
}
@Test
@@ -88,10 +88,10 @@
mCarUiRecyclerView.post(
() -> mCarUiRecyclerView.setAdapter(new CarUiListItemAdapter(items)));
- onView(withId(R.id.body)).check(matches(isDisplayed()));
- onView(withId(R.id.title)).check(matches(not(isDisplayed())));
- onView(withId(R.id.icon_container)).check(matches(not(isDisplayed())));
- onView(withId(R.id.action_container)).check(matches(not(isDisplayed())));
+ onView(withId(R.id.car_ui_list_item_body)).check(matches(isDisplayed()));
+ onView(withId(R.id.car_ui_list_item_title)).check(matches(not(isDisplayed())));
+ onView(withId(R.id.car_ui_list_item_icon_container)).check(matches(not(isDisplayed())));
+ onView(withId(R.id.car_ui_list_item_action_container)).check(matches(not(isDisplayed())));
}
@Test
@@ -107,10 +107,10 @@
mCarUiRecyclerView.post(
() -> mCarUiRecyclerView.setAdapter(new CarUiListItemAdapter(items)));
- onView(withId(R.id.title)).check(matches(isDisplayed()));
- onView(withId(R.id.body)).check(matches(isDisplayed()));
- onView(withId(R.id.icon_container)).check(matches(isDisplayed()));
- onView(withId(R.id.action_container)).check(matches(not(isDisplayed())));
+ onView(withId(R.id.car_ui_list_item_title)).check(matches(isDisplayed()));
+ onView(withId(R.id.car_ui_list_item_body)).check(matches(isDisplayed()));
+ onView(withId(R.id.car_ui_list_item_icon_container)).check(matches(isDisplayed()));
+ onView(withId(R.id.car_ui_list_item_action_container)).check(matches(not(isDisplayed())));
}
@Test
@@ -128,21 +128,21 @@
mCarUiRecyclerView.post(
() -> mCarUiRecyclerView.setAdapter(new CarUiListItemAdapter(items)));
- onView(withId(R.id.title)).check(matches(isDisplayed()));
- onView(withId(R.id.checkbox_widget)).check(matches(isDisplayed()));
- onView(withId(R.id.action_divider)).check(matches(not(isDisplayed())));
+ onView(withId(R.id.car_ui_list_item_title)).check(matches(isDisplayed()));
+ onView(withId(R.id.car_ui_list_item_checkbox_widget)).check(matches(isDisplayed()));
+ onView(withId(R.id.car_ui_list_item_action_divider)).check(matches(not(isDisplayed())));
// List item with checkbox should be initially unchecked.
- onView(withId(R.id.checkbox_widget)).check(matches(isNotChecked()));
+ onView(withId(R.id.car_ui_list_item_checkbox_widget)).check(matches(isNotChecked()));
// Clicks anywhere on the item should toggle the checkbox
- onView(withId(R.id.title)).perform(click());
- onView(withId(R.id.checkbox_widget)).check(matches(isChecked()));
+ onView(withId(R.id.car_ui_list_item_title)).perform(click());
+ onView(withId(R.id.car_ui_list_item_checkbox_widget)).check(matches(isChecked()));
// Check that onCheckChangedListener was invoked.
verify(mockOnCheckedChangeListener, times(1)).onCheckedChanged(item, true);
// Uncheck checkbox with click on the action container
- onView(withId(R.id.action_container)).perform(click());
- onView(withId(R.id.checkbox_widget)).check(matches(isNotChecked()));
+ onView(withId(R.id.car_ui_list_item_action_container)).perform(click());
+ onView(withId(R.id.car_ui_list_item_checkbox_widget)).check(matches(isNotChecked()));
// Check that onCheckChangedListener was invoked.
verify(mockOnCheckedChangeListener, times(1)).onCheckedChanged(item, false);
}
@@ -160,18 +160,18 @@
mCarUiRecyclerView.post(
() -> mCarUiRecyclerView.setAdapter(new CarUiListItemAdapter(items)));
- onView(withId(R.id.body)).check(matches(isDisplayed()));
- onView(withId(R.id.switch_widget)).check(matches(isDisplayed()));
- onView(withId(R.id.action_divider)).check(matches(isDisplayed()));
+ onView(withId(R.id.car_ui_list_item_body)).check(matches(isDisplayed()));
+ onView(withId(R.id.car_ui_list_item_switch_widget)).check(matches(isDisplayed()));
+ onView(withId(R.id.car_ui_list_item_action_divider)).check(matches(isDisplayed()));
// List item with checkbox should be initially checked.
- onView(withId(R.id.switch_widget)).check(matches(isChecked()));
+ onView(withId(R.id.car_ui_list_item_switch_widget)).check(matches(isChecked()));
// Clicks anywhere on the item should toggle the switch
- onView(withId(R.id.switch_widget)).perform(click());
- onView(withId(R.id.switch_widget)).check(matches(isNotChecked()));
+ onView(withId(R.id.car_ui_list_item_switch_widget)).perform(click());
+ onView(withId(R.id.car_ui_list_item_switch_widget)).check(matches(isNotChecked()));
// Uncheck checkbox with click on the action container
- onView(withId(R.id.body)).perform(click());
- onView(withId(R.id.switch_widget)).check(matches(isChecked()));
+ onView(withId(R.id.car_ui_list_item_body)).perform(click());
+ onView(withId(R.id.car_ui_list_item_switch_widget)).check(matches(isChecked()));
}
@Test
@@ -187,18 +187,18 @@
mCarUiRecyclerView.post(
() -> mCarUiRecyclerView.setAdapter(new CarUiListItemAdapter(items)));
- onView(withId(R.id.title)).check(matches(isDisplayed()));
- onView(withId(R.id.radio_button_widget)).check(matches(isDisplayed()));
+ onView(withId(R.id.car_ui_list_item_title)).check(matches(isDisplayed()));
+ onView(withId(R.id.car_ui_list_item_radio_button_widget)).check(matches(isDisplayed()));
// List item with checkbox should be initially not checked.
- onView(withId(R.id.radio_button_widget)).check(matches(isNotChecked()));
+ onView(withId(R.id.car_ui_list_item_radio_button_widget)).check(matches(isNotChecked()));
// Clicks anywhere on the item should toggle the radio button.
- onView(withId(R.id.radio_button_widget)).perform(click());
- onView(withId(R.id.radio_button_widget)).check(matches(isChecked()));
+ onView(withId(R.id.car_ui_list_item_radio_button_widget)).perform(click());
+ onView(withId(R.id.car_ui_list_item_radio_button_widget)).check(matches(isChecked()));
// Repeated clicks on a selected radio button should not toggle the element once checked.
- onView(withId(R.id.title)).perform(click());
- onView(withId(R.id.radio_button_widget)).check(matches(isChecked()));
+ onView(withId(R.id.car_ui_list_item_title)).perform(click());
+ onView(withId(R.id.car_ui_list_item_radio_button_widget)).check(matches(isChecked()));
}
@Test
@@ -219,16 +219,16 @@
mCarUiRecyclerView.post(
() -> mCarUiRecyclerView.setAdapter(new CarUiListItemAdapter(items)));
- onView(withId(R.id.title)).check(matches(isDisplayed()));
+ onView(withId(R.id.car_ui_list_item_title)).check(matches(isDisplayed()));
// Clicks anywhere on the item should toggle the listener
- onView(withId(R.id.title)).perform(click());
+ onView(withId(R.id.car_ui_list_item_title)).perform(click());
verify(mockOnCheckedChangeListener, times(1)).onClick(item);
- onView(withId(R.id.body)).perform(click());
+ onView(withId(R.id.car_ui_list_item_body)).perform(click());
verify(mockOnCheckedChangeListener, times(2)).onClick(item);
- onView(withId(R.id.icon_container)).perform(click());
+ onView(withId(R.id.car_ui_list_item_icon_container)).perform(click());
verify(mockOnCheckedChangeListener, times(3)).onClick(item);
}
@@ -261,7 +261,7 @@
verify(clickListener, times(1)).onClick(item);
verify(supplementalIconClickListener, times(0)).onClick(item);
- onView(withId(R.id.supplemental_icon)).perform(click());
+ onView(withId(R.id.car_ui_list_item_supplemental_icon)).perform(click());
// Check that icon is argument for single call to click listener.
verify(supplementalIconClickListener, times(1)).onClick(item);
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/recyclerview/CarUiRecyclerViewTest.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/recyclerview/CarUiRecyclerViewTest.java
index ea4a8c3..de7d34a 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/recyclerview/CarUiRecyclerViewTest.java
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/recyclerview/CarUiRecyclerViewTest.java
@@ -60,9 +60,11 @@
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
+import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.FrameLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
@@ -921,6 +923,108 @@
assertThat(carUiRecyclerView.animate(), is(equalTo(recyclerViewContainer.animate())));
}
+ @Test
+ public void testScrollbarVisibility_tooSmallHeight() {
+ doReturn(true).when(mTestableResources).getBoolean(R.bool.car_ui_scrollbar_enable);
+
+ // Set to anything less than 2 * (minTouchSize + margin)
+ // minTouchSize = R.dimen.car_ui_touch_target_size
+ // margin is button up top margin or button down bottom margin
+ int recyclerviewHeight = 1;
+
+ CarUiRecyclerView carUiRecyclerView = new CarUiRecyclerView(mTestableContext);
+
+ ViewGroup container = mActivity.findViewById(R.id.test_container);
+ container.post(() -> {
+ FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(-1, recyclerviewHeight);
+ container.addView(carUiRecyclerView, lp);
+ carUiRecyclerView.setAdapter(new TestAdapter(100));
+ });
+
+ onView(withId(R.id.car_ui_scroll_bar)).check(matches(not(isDisplayed())));
+
+ OrientationHelper orientationHelper =
+ OrientationHelper.createVerticalHelper(carUiRecyclerView.getLayoutManager());
+ int screenHeight = orientationHelper.getTotalSpace();
+
+ assertEquals(recyclerviewHeight, screenHeight);
+ }
+
+ @Test
+ public void testScrollbarVisibility_justEnoughToShowOnlyButtons() {
+ doReturn(true).when(mTestableResources).getBoolean(R.bool.car_ui_scrollbar_enable);
+
+ // R.dimen.car_ui_touch_target_size
+ float minTouchSize = mTestableResources.getDimension(R.dimen.car_ui_touch_target_size);
+ // This value is hardcoded to 15dp in the layout.
+ int margin = (int) dpToPixel(mTestableContext, 15)
+ + (int) mTestableResources.getDimension(R.dimen.car_ui_scrollbar_separator_margin);
+ // Set to 2 * (minTouchSize + margin)
+ int recyclerviewHeight = 2 * (int) (minTouchSize + margin);
+
+ CarUiRecyclerView carUiRecyclerView = new CarUiRecyclerView(mTestableContext);
+
+ ViewGroup container = mActivity.findViewById(R.id.test_container);
+ container.post(() -> {
+ FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(-1, recyclerviewHeight);
+ container.addView(carUiRecyclerView, lp);
+ carUiRecyclerView.setAdapter(new TestAdapter(100));
+ });
+
+ onView(withId(R.id.car_ui_scroll_bar)).check(matches(isDisplayed()));
+ onView(withId(R.id.car_ui_scrollbar_thumb)).check(matches(not(isDisplayed())));
+ onView(withId(R.id.car_ui_scrollbar_track)).check(matches(not(isDisplayed())));
+ onView(withId(R.id.car_ui_scrollbar_page_down)).check(matches(isDisplayed()));
+ onView(withId(R.id.car_ui_scrollbar_page_up)).check(matches(isDisplayed()));
+
+ OrientationHelper orientationHelper =
+ OrientationHelper.createVerticalHelper(carUiRecyclerView.getLayoutManager());
+ int screenHeight = orientationHelper.getTotalSpace();
+
+ assertEquals(recyclerviewHeight, screenHeight);
+ }
+
+ @Test
+ public void testScrollbarVisibility_enoughToShowEverything() {
+ doReturn(true).when(mTestableResources).getBoolean(R.bool.car_ui_scrollbar_enable);
+
+ // R.dimen.car_ui_touch_target_size
+ float minTouchSize = mTestableResources.getDimension(R.dimen.car_ui_touch_target_size);
+ // This value is hardcoded to 15dp in the layout.
+ int margin = (int) dpToPixel(mTestableContext, 15)
+ + (int) mTestableResources.getDimension(R.dimen.car_ui_scrollbar_separator_margin);
+ // Set to anything greater or equal to 3 * minTouchSize + 2 * margin
+ int recyclerviewHeight = 3 * (int) minTouchSize + 2 * (int) margin;
+
+ CarUiRecyclerView carUiRecyclerView = new CarUiRecyclerView(mTestableContext);
+
+ ViewGroup container = mActivity.findViewById(R.id.test_container);
+ container.post(() -> {
+ FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(-1, recyclerviewHeight);
+ container.addView(carUiRecyclerView, lp);
+ carUiRecyclerView.setAdapter(new TestAdapter(100));
+ });
+
+ onView(withId(R.id.car_ui_scroll_bar)).check(matches(isDisplayed()));
+ onView(withId(R.id.car_ui_scrollbar_thumb)).check(matches(isDisplayed()));
+ onView(withId(R.id.car_ui_scrollbar_track)).check(matches(isDisplayed()));
+ onView(withId(R.id.car_ui_scrollbar_page_down)).check(matches(isDisplayed()));
+ onView(withId(R.id.car_ui_scrollbar_page_up)).check(matches(isDisplayed()));
+
+ OrientationHelper orientationHelper =
+ OrientationHelper.createVerticalHelper(carUiRecyclerView.getLayoutManager());
+ int screenHeight = orientationHelper.getTotalSpace();
+
+ assertEquals(recyclerviewHeight, screenHeight);
+ }
+
+ private static float dpToPixel(Context context, int dp) {
+ return TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ dp,
+ context.getResources().getDisplayMetrics());
+ }
+
/**
* Returns an item in the current list view whose height is taller than that of
* the CarUiRecyclerView. If that item exists, then it is returned; otherwise an {@link
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/utils/ViewUtilsTest.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/utils/ViewUtilsTest.java
index 3cc9b7e..9cd5484 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/utils/ViewUtilsTest.java
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/utils/ViewUtilsTest.java
@@ -204,6 +204,22 @@
}
@Test
+ public void testFindImplicitDefaultFocusView_selectedItem_inFocusArea() {
+ mRoot.post(() -> mList5.getViewTreeObserver().addOnGlobalLayoutListener(
+ new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ mList5.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ View selectedItem = mList5.getLayoutManager().findViewByPosition(1);
+ selectedItem.setSelected(true);
+ View implicitDefaultFocus =
+ ViewUtils.findImplicitDefaultFocusView(mFocusArea5);
+ assertThat(implicitDefaultFocus).isEqualTo(selectedItem);
+ }
+ }));
+ }
+
+ @Test
public void testFindFirstFocusableDescendant() {
mRoot.post(() -> {
mFocusArea2.setFocusable(true);
@@ -286,6 +302,20 @@
}
@Test
+ public void testIsImplicitDefaultFocusView_selectedItem() {
+ mRoot.post(() -> mList5.getViewTreeObserver().addOnGlobalLayoutListener(
+ new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ mList5.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ View selectedItem = mList5.getLayoutManager().findViewByPosition(1);
+ selectedItem.setSelected(true);
+ assertThat(ViewUtils.isImplicitDefaultFocusView(selectedItem)).isTrue();
+ }
+ }));
+ }
+
+ @Test
public void testRequestFocus() {
mRoot.post(() -> assertRequestFocus(mView2, true));
}
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/res/layout/focus_area_test_activity.xml b/car-ui-lib/car-ui-lib/src/androidTest/res/layout/focus_area_test_activity.xml
index da1255d..5eba881 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/res/layout/focus_area_test_activity.xml
+++ b/car-ui-lib/car-ui-lib/src/androidTest/res/layout/focus_area_test_activity.xml
@@ -95,4 +95,19 @@
android:layout_width="100dp"
android:layout_height="100dp"/>
</com.android.car.ui.TestFocusArea>
+ <com.android.car.ui.TestFocusArea
+ android:id="@+id/focus_area5"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:wrapAround="true">
+ <View
+ android:id="@+id/view5"
+ android:focusable="true"
+ android:layout_width="100dp"
+ android:layout_height="100dp"/>
+ <Button
+ android:id="@+id/button5"
+ android:layout_width="100dp"
+ android:layout_height="100dp"/>
+ </com.android.car.ui.TestFocusArea>
</LinearLayout>
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/FocusArea.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/FocusArea.java
index 0488d28..b20758a 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/FocusArea.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/FocusArea.java
@@ -25,8 +25,6 @@
import static com.android.car.ui.utils.RotaryConstants.FOCUS_AREA_RIGHT_BOUND_OFFSET;
import static com.android.car.ui.utils.RotaryConstants.FOCUS_AREA_TOP_BOUND_OFFSET;
import static com.android.car.ui.utils.RotaryConstants.NUDGE_DIRECTION;
-import static com.android.car.ui.utils.ViewUtils.NO_FOCUS;
-import static com.android.car.ui.utils.ViewUtils.REGULAR_FOCUS;
import android.content.Context;
import android.content.res.Resources;
@@ -38,9 +36,11 @@
import android.os.SystemClock;
import android.util.AttributeSet;
import android.util.Log;
+import android.view.FocusFinder;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
+import android.view.ViewTreeObserver.OnGlobalFocusChangeListener;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.LinearLayout;
@@ -137,8 +137,8 @@
private View mDefaultFocusView;
/**
- * Whether to focus on the {@code app:defaultFocus} view when nudging to the FocusArea, even if
- * there was another view in the FocusArea focused before.
+ * Whether to focus on the default focus view when nudging to the FocusArea, even if there was
+ * another view in the FocusArea focused before.
*/
private boolean mDefaultFocusOverridesHistory;
@@ -160,6 +160,9 @@
/** Map of specified nudge target FocusAreas. */
private Map<Integer, FocusArea> mSpecifiedNudgeFocusAreaMap;
+ /** Whether wrap-around is enabled. */
+ private boolean mWrapAround;
+
/**
* Cache of focus history and nudge history of the rotary controller.
* <p>
@@ -193,6 +196,16 @@
/** The focused view in this FocusArea, if any. */
private View mFocusedView;
+ private final OnGlobalFocusChangeListener mFocusChangeListener =
+ (oldFocus, newFocus) -> {
+ boolean hasFocus = hasFocus();
+ saveFocusHistory(hasFocus);
+ maybeUpdatePreviousFocusArea(hasFocus, oldFocus);
+ maybeClearFocusAreaHistory(hasFocus, oldFocus);
+ maybeUpdateFocusAreaHighlight(hasFocus);
+ mHasFocus = hasFocus;
+ };
+
public FocusArea(Context context) {
super(context);
init(context, null);
@@ -225,8 +238,6 @@
mBackgroundHighlight = resources.getDrawable(
R.drawable.car_ui_focus_area_background_highlight, getContext().getTheme());
- mDefaultFocusOverridesHistory = resources.getBoolean(
- R.bool.car_ui_focus_area_default_focus_overrides_history);
mClearFocusAreaHistoryWhenRotating = resources.getBoolean(
R.bool.car_ui_clear_focus_area_history_when_rotating);
@@ -249,29 +260,20 @@
// should enable it since we override these methods.
setWillNotDraw(false);
- registerFocusChangeListener();
-
initAttrs(context, attrs);
}
- private void registerFocusChangeListener() {
- getViewTreeObserver().addOnGlobalFocusChangeListener(
- (oldFocus, newFocus) -> {
- boolean hasFocus = hasFocus();
- saveFocusHistory(hasFocus);
- maybeUpdatePreviousFocusArea(hasFocus, oldFocus);
- maybeClearFocusAreaHistory(hasFocus, oldFocus);
- maybeUpdateFocusAreaHighlight(hasFocus);
- mHasFocus = hasFocus;
- });
- }
-
private void saveFocusHistory(boolean hasFocus) {
+ // Save focus history and clear mFocusedView if focus is leaving this FocusArea.
if (!hasFocus) {
- mRotaryCache.saveFocusedView(mFocusedView, SystemClock.uptimeMillis());
- mFocusedView = null;
+ if (mHasFocus) {
+ mRotaryCache.saveFocusedView(mFocusedView, SystemClock.uptimeMillis());
+ mFocusedView = null;
+ }
return;
}
+
+ // Update mFocusedView if a view inside this FocusArea is focused.
View v = getFocusedChild();
while (v != null) {
if (v.isFocused()) {
@@ -425,6 +427,11 @@
a.getResourceId(R.styleable.FocusArea_nudgeUp, View.NO_ID));
mSpecifiedNudgeIdMap.put(FOCUS_DOWN,
a.getResourceId(R.styleable.FocusArea_nudgeDown, View.NO_ID));
+
+ mDefaultFocusOverridesHistory = a.getBoolean(
+ R.styleable.FocusArea_defaultFocusOverridesHistory, false);
+
+ mWrapAround = a.getBoolean(R.styleable.FocusArea_wrapAround, false);
} finally {
a.recycle();
}
@@ -459,18 +466,40 @@
}
@Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ getViewTreeObserver().addOnGlobalFocusChangeListener(mFocusChangeListener);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ getViewTreeObserver().removeOnGlobalFocusChangeListener(mFocusChangeListener);
+ super.onDetachedFromWindow();
+ }
+
+ @Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
// To ensure the focus is initialized properly in rotary mode when there is a window focus
- // change, this FocusArea will grab the focus from the currently focused view if one of this
- // FocusArea's descendants is a better focus candidate than the currently focused view.
+ // change, this FocusArea will grab the focus if nothing is focused or the currently
+ // focused view's FocusLevel is lower than REGULAR_FOCUS.
if (hasWindowFocus && !isInTouchMode()) {
- maybeAdjustFocus();
+ maybeInitFocus();
}
super.onWindowFocusChanged(hasWindowFocus);
}
/**
- * Focuses on another view in this FocusArea if the view is a better focus candidate than the
+ * Focuses on another view in this FocusArea if nothing is focused or the currently focused
+ * view's FocusLevel is lower than REGULAR_FOCUS.
+ */
+ private boolean maybeInitFocus() {
+ View root = getRootView();
+ View focus = root.findFocus();
+ return ViewUtils.initFocus(root, focus);
+ }
+
+ /**
+ * Focuses on a view in this FocusArea if the view is a better focus candidate than the
* currently focused view.
*/
private boolean maybeAdjustFocus() {
@@ -505,22 +534,8 @@
}
private boolean focusOnDescendant() {
- if (mDefaultFocusOverridesHistory) {
- // Check mDefaultFocus before last focused view.
- if (focusDefaultFocusView() || focusOnLastFocusedView()) {
- return true;
- }
- } else {
- // Check last focused view before mDefaultFocus.
- if (focusOnLastFocusedView() || focusDefaultFocusView()) {
- return true;
- }
- }
- return focusOnFirstFocusableView();
- }
-
- private boolean focusDefaultFocusView() {
- return ViewUtils.adjustFocus(this, /* currentLevel= */ REGULAR_FOCUS);
+ View lastFocusedView = mRotaryCache.getFocusedView(SystemClock.uptimeMillis());
+ return ViewUtils.adjustFocus(this, lastFocusedView, mDefaultFocusOverridesHistory);
}
/**
@@ -532,15 +547,6 @@
return mDefaultFocusView;
}
- private boolean focusOnLastFocusedView() {
- View lastFocusedView = mRotaryCache.getFocusedView(SystemClock.uptimeMillis());
- return ViewUtils.requestFocus(lastFocusedView);
- }
-
- private boolean focusOnFirstFocusableView() {
- return ViewUtils.adjustFocus(this, /* currentLevel= */ NO_FOCUS);
- }
-
private boolean nudgeToShortcutView(Bundle arguments) {
if (mNudgeShortcutDirection == INVALID_DIRECTION) {
// No nudge shortcut configured for this FocusArea.
@@ -720,11 +726,30 @@
mBottomOffset = bottom;
}
+ /** Sets whether wrap-around is enabled for this FocusArea. */
+ public void setWrapAround(boolean wrapAround) {
+ mWrapAround = wrapAround;
+ }
+
/** Sets the default focus view in this FocusArea. */
public void setDefaultFocus(@NonNull View defaultFocus) {
mDefaultFocusView = defaultFocus;
}
+ /**
+ * @inheritDoc
+ * <p>
+ * When {@link #mWrapAround} is true, the search is restricted to descendants of this
+ * {@link FocusArea}.
+ */
+ @Override
+ public View focusSearch(View focused, int direction) {
+ if (mWrapAround) {
+ return FocusFinder.getInstance().findNextFocus(/* root= */ this, focused, direction);
+ }
+ return super.focusSearch(focused, direction);
+ }
+
@VisibleForTesting
void enableForegroundHighlight() {
mEnableForegroundHighlight = true;
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/FocusParkingView.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/FocusParkingView.java
index 9d5cfeb..aac5c03 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/FocusParkingView.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/FocusParkingView.java
@@ -22,13 +22,16 @@
import static com.android.car.ui.utils.RotaryConstants.ACTION_RESTORE_DEFAULT_FOCUS;
import android.content.Context;
+import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Rect;
import android.os.Bundle;
+import android.os.SystemClock;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
+import android.view.ViewTreeObserver.OnGlobalFocusChangeListener;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.inputmethod.InputMethodManager;
import androidx.annotation.Nullable;
@@ -73,6 +76,9 @@
@Nullable
private View mFocusedView;
+ /** The cache to save the previously focused view. */
+ private RotaryCache.FocusCache mFocusCache;
+
/** The scrollable container that contains the {@link #mFocusedView}, if any. */
@Nullable
ViewGroup mScrollableContainer;
@@ -84,6 +90,20 @@
*/
private boolean mShouldRestoreFocus;
+ /**
+ * Whether to focus on the default focus view when nudging to the explicit focus area containing
+ * this FocusParkingView, even if there was another view in the explicit focus area focused
+ * before.
+ */
+ private boolean mDefaultFocusOverridesHistory;
+
+ private final OnGlobalFocusChangeListener mFocusChangeListener =
+ (oldFocus, newFocus) -> {
+ // Keep track of the focused view so that we can recover focus when it's removed.
+ View focusedView = newFocus instanceof FocusParkingView ? null : newFocus;
+ updateFocusedView(focusedView);
+ };
+
public FocusParkingView(Context context) {
super(context);
init(context, /* attrs= */ null);
@@ -127,11 +147,24 @@
// Prevent Android from drawing the default focus highlight for this view when it's focused.
setDefaultFocusHighlightEnabled(false);
- // Keep track of the focused view so that we can recover focus when it's removed.
- getViewTreeObserver().addOnGlobalFocusChangeListener((oldFocus, newFocus) -> {
- mFocusedView = newFocus instanceof FocusParkingView ? null : newFocus;
- mScrollableContainer = ViewUtils.getAncestorScrollableContainer(mFocusedView);
- });
+ Resources resources = getResources();
+ @RotaryCache.CacheType
+ int focusHistoryCacheType = resources.getInteger(R.integer.car_ui_focus_history_cache_type);
+ int focusHistoryExpirationPeriodMs =
+ resources.getInteger(R.integer.car_ui_focus_history_expiration_period_ms);
+ mFocusCache =
+ new RotaryCache.FocusCache(focusHistoryCacheType, focusHistoryExpirationPeriodMs);
+
+ mDefaultFocusOverridesHistory = resources.getBoolean(
+ R.bool.car_ui_focus_area_default_focus_overrides_history);
+ }
+
+ private void updateFocusedView(@Nullable View focusedView) {
+ if (mFocusedView != null) {
+ mFocusCache.setFocusedView(mFocusedView, SystemClock.uptimeMillis());
+ }
+ mFocusedView = focusedView;
+ mScrollableContainer = ViewUtils.getAncestorScrollableContainer(focusedView);
}
@Override
@@ -143,6 +176,18 @@
}
@Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ getViewTreeObserver().addOnGlobalFocusChangeListener(mFocusChangeListener);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ getViewTreeObserver().removeOnGlobalFocusChangeListener(mFocusChangeListener);
+ super.onDetachedFromWindow();
+ }
+
+ @Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
if (!hasWindowFocus) {
// We need to clear the focus highlight(by parking the focus on the FocusParkingView)
@@ -154,8 +199,7 @@
// OnGlobalFocusChangeListener won't be triggered when the window lost focus, so reset
// the focused view here.
- mFocusedView = null;
- mScrollableContainer = null;
+ updateFocusedView(null);
} else if (isFocused()) {
// When FocusParkingView is focused and the window just gets focused, transfer the view
// focus to a non-FocusParkingView in the window.
@@ -229,10 +273,14 @@
if (maybeFocusOnScrollableContainer()) {
return true;
}
+
// Otherwise try to find the best target view to focus.
- if (ViewUtils.adjustFocus(getRootView(), /* currentFocus= */ null)) {
+ View cachedFocusedView = mFocusCache.getFocusedView(SystemClock.uptimeMillis());
+ if (ViewUtils.adjustFocus(
+ getRootView(), cachedFocusedView, mDefaultFocusOverridesHistory)) {
return true;
}
+
// It failed to find a target view (e.g., all the views are not shown), so focus on this
// FocusParkingView as fallback.
return super.requestFocus(FOCUS_DOWN, /* previouslyFocusedRect= */ null);
@@ -256,7 +304,7 @@
// layout is not ready. So wait until its layout is ready then dispatch the
// event.
getViewTreeObserver().addOnGlobalLayoutListener(
- new ViewTreeObserver.OnGlobalLayoutListener() {
+ new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
// At this point the layout is complete and the dimensions of
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/RotaryCache.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/RotaryCache.java
index 18f9fb0..97de7c3 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/RotaryCache.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/RotaryCache.java
@@ -58,6 +58,7 @@
/** A record of when a View was focused. */
private static class FocusHistory {
/** The focused view. */
+ @NonNull
final View mFocusedView;
/** The {@link SystemClock#uptimeMillis} when this history was recorded. */
final long mTimestamp;
@@ -69,14 +70,14 @@
}
/** Cache of focused view. */
- private static class FocusCache {
+ static class FocusCache {
/** The cache type. */
@CacheType
final int mCacheType;
/** How many milliseconds before the entry in the cache expires. */
long mExpirationPeriodMs;
/** The record of focused view. */
- @NonNull
+ @Nullable
FocusHistory mFocusHistory;
FocusCache(@CacheType int cacheType, final long expirationPeriodMs) {
@@ -93,11 +94,13 @@
return isValidHistory(elapsedRealtime) ? mFocusHistory.mFocusedView : null;
}
- void setFocusedView(@NonNull View focusedView, long elapsedRealtime) {
+ void setFocusedView(@Nullable View focusedView, long elapsedRealtime) {
if (mCacheType == CACHE_TYPE_DISABLED) {
return;
}
- mFocusHistory = new FocusHistory(focusedView, elapsedRealtime);
+ mFocusHistory = focusedView != null
+ ? new FocusHistory(focusedView, elapsedRealtime)
+ : null;
}
boolean isValidHistory(long elapsedRealtime) {
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/core/SearchResultsProvider.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/core/SearchResultsProvider.java
index f20c6b4..5f30b70 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/core/SearchResultsProvider.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/core/SearchResultsProvider.java
@@ -96,7 +96,6 @@
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
- getContext().getContentResolver().notifyChange(uri, null);
mSearchResults.clear();
return 0;
}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/imewidescreen/CarUiImeWideScreenController.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/imewidescreen/CarUiImeWideScreenController.java
index be8aeee..2d10605 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/imewidescreen/CarUiImeWideScreenController.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/imewidescreen/CarUiImeWideScreenController.java
@@ -112,6 +112,8 @@
// Action name of action that will be used by IMS to notify the application to clear the data
// in the EditText.
public static final String WIDE_SCREEN_CLEAR_DATA_ACTION = "automotive_wide_screen_clear_data";
+ public static final String WIDE_SCREEN_POST_LOAD_SEARCH_RESULTS_ACTION =
+ "automotive_wide_screen_post_load_search_results";
// Action name used by applications to notify that new search results are available.
public static final String WIDE_SCREEN_SEARCH_RESULTS = "wide_screen_search_results";
// Key to provide the resource id for the icon that will be displayed in the input area. If
@@ -395,52 +397,57 @@
Log.w(TAG, "Result can't be loaded, input InputEditorInfo not available ");
return;
}
- String url = CONTENT + mInputEditorInfo.packageName + SEARCH_RESULTS_PROVIDER;
+ String url = CONTENT + getPackageName(mInputEditorInfo) + SEARCH_RESULTS_PROVIDER;
Uri contentUrl = Uri.parse(url);
ContentResolver cr = mContext.getContentResolver();
- Cursor c = cr.query(contentUrl, null, null, null, null);
- mAutomotiveSearchItems = new ArrayList<>();
- if (c != null && c.moveToFirst()) {
- do {
- CarUiContentListItem searchItem = new CarUiContentListItem(
- CarUiContentListItem.Action.ICON);
- searchItem.setOnItemClickedListener(v -> {
- Bundle bundle = new Bundle();
- bundle.putString(SEARCH_RESULT_ITEM_ID_LIST,
- c.getString(c.getColumnIndex(SearchResultsProvider.ITEM_ID)));
- mInputConnection.performPrivateCommand(WIDE_SCREEN_ACTION, bundle);
- });
- searchItem.setTitle(c.getString(c.getColumnIndex(SearchResultsProvider.TITLE)));
- searchItem.setBody(c.getString(c.getColumnIndex(SearchResultsProvider.SUBTITLE)));
- searchItem.setPrimaryIconType(CarUiContentListItem.IconType.CONTENT);
- byte[] primaryBlob = c.getBlob(
- c.getColumnIndex(SearchResultsProvider.PRIMARY_IMAGE_BLOB));
- if (primaryBlob != null) {
- Bitmap primaryBitmap = Bitmap.CREATOR.createFromParcel(
- byteArrayToParcel(primaryBlob));
- searchItem.setIcon(
- new BitmapDrawable(mContext.getResources(), primaryBitmap));
- }
- byte[] secondaryBlob = c.getBlob(
- c.getColumnIndex(SearchResultsProvider.SECONDARY_IMAGE_BLOB));
+ try (Cursor c = cr.query(contentUrl, null, null, null, null)) {
+ mAutomotiveSearchItems = new ArrayList<>();
+ if (c != null && c.moveToFirst()) {
+ do {
+ CarUiContentListItem searchItem = new CarUiContentListItem(
+ CarUiContentListItem.Action.ICON);
+ String itemId = c.getString(c.getColumnIndex(SearchResultsProvider.ITEM_ID));
+ searchItem.setOnItemClickedListener(v -> {
+ Bundle bundle = new Bundle();
+ bundle.putString(SEARCH_RESULT_ITEM_ID_LIST, itemId);
+ mInputConnection.performPrivateCommand(WIDE_SCREEN_ACTION, bundle);
+ });
+ searchItem.setTitle(c.getString(
+ c.getColumnIndex(SearchResultsProvider.TITLE)));
+ searchItem.setBody(c.getString(
+ c.getColumnIndex(SearchResultsProvider.SUBTITLE)));
+ searchItem.setPrimaryIconType(CarUiContentListItem.IconType.CONTENT);
+ byte[] primaryBlob = c.getBlob(
+ c.getColumnIndex(
+ SearchResultsProvider.PRIMARY_IMAGE_BLOB));
+ if (primaryBlob != null) {
+ Bitmap primaryBitmap = Bitmap.CREATOR.createFromParcel(
+ byteArrayToParcel(primaryBlob));
+ searchItem.setIcon(
+ new BitmapDrawable(mContext.getResources(), primaryBitmap));
+ }
+ byte[] secondaryBlob = c.getBlob(
+ c.getColumnIndex(
+ SearchResultsProvider.SECONDARY_IMAGE_BLOB));
- if (secondaryBlob != null) {
- Bitmap secondaryBitmap = Bitmap.CREATOR.createFromParcel(
- byteArrayToParcel(secondaryBlob));
- searchItem.setSupplementalIcon(
- new BitmapDrawable(mContext.getResources(), secondaryBitmap), v -> {
- Bundle bundle = new Bundle();
- bundle.putString(SEARCH_RESULT_SUPPLEMENTAL_ICON_ID_LIST,
- c.getString(c.getColumnIndex(
- SearchResultsProvider.SECONDARY_IMAGE_ID)));
- mInputConnection.performPrivateCommand(WIDE_SCREEN_ACTION, bundle);
- });
- }
- mAutomotiveSearchItems.add(searchItem);
- } while (c.moveToNext());
+ if (secondaryBlob != null) {
+ Bitmap secondaryBitmap = Bitmap.CREATOR.createFromParcel(
+ byteArrayToParcel(secondaryBlob));
+ searchItem.setSupplementalIcon(
+ new BitmapDrawable(mContext.getResources(), secondaryBitmap), v -> {
+ Bundle bundle = new Bundle();
+ bundle.putString(SEARCH_RESULT_SUPPLEMENTAL_ICON_ID_LIST,
+ c.getString(c.getColumnIndex(
+ SearchResultsProvider.SECONDARY_IMAGE_ID)));
+ mInputConnection.performPrivateCommand(WIDE_SCREEN_ACTION,
+ bundle);
+ });
+ }
+ mAutomotiveSearchItems.add(searchItem);
+ } while (c.moveToNext());
+ }
}
- // delete the results.
- cr.delete(contentUrl, null, null);
+ mInputConnection.performPrivateCommand(WIDE_SCREEN_POST_LOAD_SEARCH_RESULTS_ACTION, null);
}
private static Parcel byteArrayToParcel(byte[] bytes) {
@@ -533,7 +540,9 @@
intiExtractAction(textForImeAction);
}
- sendSurfaceInfo();
+ if (mContentAreaSurfaceView.getVisibility() == View.GONE) {
+ sendSurfaceInfo();
+ }
}
/**
@@ -694,6 +703,16 @@
mExtractEditText = editText;
}
+ @VisibleForTesting
+ void setEditorInfo(EditorInfo editorInfo) {
+ mInputEditorInfo = editorInfo;
+ }
+
+ @VisibleForTesting
+ String getPackageName(EditorInfo editorInfo) {
+ return editorInfo.packageName;
+ }
+
/**
* Sets the icon in the input area. If the icon resource Id is not provided by the application
* then application icon will be used instead.
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiDropDownPreference.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiDropDownPreference.java
index 56134b3..7c4add1 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiDropDownPreference.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiDropDownPreference.java
@@ -19,37 +19,41 @@
import android.content.Context;
import android.util.AttributeSet;
+import androidx.annotation.Nullable;
import androidx.preference.DropDownPreference;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceViewHolder;
import com.android.car.ui.R;
+import com.android.car.ui.utils.ViewUtils;
+
+import java.util.function.Consumer;
/**
* This class extends the base {@link DropDownPreference} class. Adds the drawable icon to
* the preference.
*/
-public class CarUiDropDownPreference extends DropDownPreference {
+public class CarUiDropDownPreference extends DropDownPreference
+ implements UxRestrictablePreference {
- private final Context mContext;
+ private Consumer<Preference> mRestrictedClickListener;
+ private boolean mUxRestricted = false;
public CarUiDropDownPreference(Context context) {
super(context);
- mContext = context;
}
public CarUiDropDownPreference(Context context, AttributeSet attrs) {
super(context, attrs);
- mContext = context;
}
public CarUiDropDownPreference(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
- mContext = context;
}
public CarUiDropDownPreference(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
- mContext = context;
}
/**
@@ -65,7 +69,7 @@
public void onAttached() {
super.onAttached();
- boolean showChevron = mContext.getResources().getBoolean(
+ boolean showChevron = getContext().getResources().getBoolean(
R.bool.car_ui_preference_show_chevron);
if (!showChevron) {
@@ -74,4 +78,47 @@
setWidgetLayoutResource(R.layout.car_ui_preference_chevron);
}
+
+ @Override
+ public void onBindViewHolder(PreferenceViewHolder holder) {
+ super.onBindViewHolder(holder);
+
+ ViewUtils.makeAllViewsUxRestricted(holder.itemView, isUxRestricted());
+ }
+
+ @Override
+ @SuppressWarnings("RestrictTo")
+ public void performClick() {
+ if ((isEnabled() || isSelectable()) && isUxRestricted()) {
+ if (mRestrictedClickListener != null) {
+ mRestrictedClickListener.accept(this);
+ }
+ } else {
+ super.performClick();
+ }
+ }
+
+ @Override
+ public void setUxRestricted(boolean restricted) {
+ if (restricted != mUxRestricted) {
+ mUxRestricted = restricted;
+ notifyChanged();
+ }
+ }
+
+ @Override
+ public boolean isUxRestricted() {
+ return mUxRestricted;
+ }
+
+ @Override
+ public void setOnClickWhileRestrictedListener(@Nullable Consumer<Preference> listener) {
+ mRestrictedClickListener = listener;
+ }
+
+ @Nullable
+ @Override
+ public Consumer<Preference> getOnClickWhileRestrictedListener() {
+ return mRestrictedClickListener;
+ }
}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiEditTextPreference.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiEditTextPreference.java
index 3882e03..bc21b43 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiEditTextPreference.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiEditTextPreference.java
@@ -20,41 +20,44 @@
import android.util.AttributeSet;
import android.view.View;
+import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.preference.EditTextPreference;
+import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
import com.android.car.ui.R;
import com.android.car.ui.utils.CarUiUtils;
+import com.android.car.ui.utils.ViewUtils;
+
+import java.util.function.Consumer;
/**
* This class extends the base {@link EditTextPreference} class. Adds the drawable icon to
* the preference.
*/
-public class CarUiEditTextPreference extends EditTextPreference {
+public class CarUiEditTextPreference extends EditTextPreference
+ implements UxRestrictablePreference {
- private final Context mContext;
+ private Consumer<Preference> mRestrictedClickListener;
+ private boolean mUxRestricted = false;
private boolean mShowChevron = true;
public CarUiEditTextPreference(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
- mContext = context;
}
public CarUiEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
- mContext = context;
}
public CarUiEditTextPreference(Context context, AttributeSet attrs) {
super(context, attrs);
- mContext = context;
}
public CarUiEditTextPreference(Context context) {
super(context);
- mContext = context;
}
protected void setTwoActionLayout() {
@@ -73,7 +76,7 @@
public void onAttached() {
super.onAttached();
- boolean allowChevron = mContext.getResources().getBoolean(
+ boolean allowChevron = getContext().getResources().getBoolean(
R.bool.car_ui_preference_show_chevron);
if (!allowChevron || !mShowChevron) {
@@ -86,4 +89,47 @@
public void setShowChevron(boolean showChevron) {
mShowChevron = showChevron;
}
+
+ @Override
+ public void onBindViewHolder(PreferenceViewHolder holder) {
+ super.onBindViewHolder(holder);
+
+ ViewUtils.makeAllViewsUxRestricted(holder.itemView, isUxRestricted());
+ }
+
+ @Override
+ @SuppressWarnings("RestrictTo")
+ public void performClick() {
+ if ((isEnabled() || isSelectable()) && isUxRestricted()) {
+ if (mRestrictedClickListener != null) {
+ mRestrictedClickListener.accept(this);
+ }
+ } else {
+ super.performClick();
+ }
+ }
+
+ @Override
+ public void setUxRestricted(boolean restricted) {
+ if (restricted != mUxRestricted) {
+ mUxRestricted = restricted;
+ notifyChanged();
+ }
+ }
+
+ @Override
+ public boolean isUxRestricted() {
+ return mUxRestricted;
+ }
+
+ @Override
+ public void setOnClickWhileRestrictedListener(@Nullable Consumer<Preference> listener) {
+ mRestrictedClickListener = listener;
+ }
+
+ @Nullable
+ @Override
+ public Consumer<Preference> getOnClickWhileRestrictedListener() {
+ return mRestrictedClickListener;
+ }
}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiListPreference.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiListPreference.java
index 24d940c..81f11a8 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiListPreference.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiListPreference.java
@@ -19,44 +19,47 @@
import android.content.Context;
import android.util.AttributeSet;
+import androidx.annotation.Nullable;
import androidx.preference.ListPreference;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceViewHolder;
import com.android.car.ui.R;
+import com.android.car.ui.utils.ViewUtils;
+
+import java.util.function.Consumer;
/**
* This class extends the base {@link ListPreference} class. Adds the drawable icon to
* the preference.
*/
-public class CarUiListPreference extends ListPreference {
+public class CarUiListPreference extends ListPreference implements UxRestrictablePreference {
- private final Context mContext;
+ private Consumer<Preference> mRestrictedClickListener;
+ private boolean mUxRestricted = false;
public CarUiListPreference(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
- mContext = context;
}
public CarUiListPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
- mContext = context;
}
public CarUiListPreference(Context context, AttributeSet attrs) {
super(context, attrs);
- mContext = context;
}
public CarUiListPreference(Context context) {
super(context);
- mContext = context;
}
@Override
public void onAttached() {
super.onAttached();
- boolean showChevron = mContext.getResources().getBoolean(
+ boolean showChevron = getContext().getResources().getBoolean(
R.bool.car_ui_preference_show_chevron);
if (!showChevron) {
@@ -65,4 +68,47 @@
setWidgetLayoutResource(R.layout.car_ui_preference_chevron);
}
+
+ @Override
+ public void onBindViewHolder(PreferenceViewHolder holder) {
+ super.onBindViewHolder(holder);
+
+ ViewUtils.makeAllViewsUxRestricted(holder.itemView, isUxRestricted());
+ }
+
+ @Override
+ @SuppressWarnings("RestrictTo")
+ public void performClick() {
+ if ((isEnabled() || isSelectable()) && isUxRestricted()) {
+ if (mRestrictedClickListener != null) {
+ mRestrictedClickListener.accept(this);
+ }
+ } else {
+ super.performClick();
+ }
+ }
+
+ @Override
+ public void setUxRestricted(boolean restricted) {
+ if (restricted != mUxRestricted) {
+ mUxRestricted = restricted;
+ notifyChanged();
+ }
+ }
+
+ @Override
+ public boolean isUxRestricted() {
+ return mUxRestricted;
+ }
+
+ @Override
+ public void setOnClickWhileRestrictedListener(@Nullable Consumer<Preference> listener) {
+ mRestrictedClickListener = listener;
+ }
+
+ @Nullable
+ @Override
+ public Consumer<Preference> getOnClickWhileRestrictedListener() {
+ return mRestrictedClickListener;
+ }
}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiMultiSelectListPreference.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiMultiSelectListPreference.java
index bc626a0..b938325 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiMultiSelectListPreference.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiMultiSelectListPreference.java
@@ -19,37 +19,41 @@
import android.content.Context;
import android.util.AttributeSet;
+import androidx.annotation.Nullable;
import androidx.preference.MultiSelectListPreference;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceViewHolder;
import com.android.car.ui.R;
+import com.android.car.ui.utils.ViewUtils;
+
+import java.util.function.Consumer;
/**
* This class extends the base {@link CarUiMultiSelectListPreference} class. Adds the drawable icon
* to the preference.
*/
-public class CarUiMultiSelectListPreference extends MultiSelectListPreference {
+public class CarUiMultiSelectListPreference extends MultiSelectListPreference
+ implements UxRestrictablePreference {
- private final Context mContext;
+ private Consumer<Preference> mRestrictedClickListener;
+ private boolean mUxRestricted = false;
public CarUiMultiSelectListPreference(Context context) {
super(context);
- mContext = context;
}
public CarUiMultiSelectListPreference(Context context, AttributeSet attrs) {
super(context, attrs);
- mContext = context;
}
public CarUiMultiSelectListPreference(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
- mContext = context;
}
public CarUiMultiSelectListPreference(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
- mContext = context;
}
/**
@@ -65,7 +69,7 @@
public void onAttached() {
super.onAttached();
- boolean showChevron = mContext.getResources().getBoolean(
+ boolean showChevron = getContext().getResources().getBoolean(
R.bool.car_ui_preference_show_chevron);
if (!showChevron) {
@@ -74,4 +78,47 @@
setWidgetLayoutResource(R.layout.car_ui_preference_chevron);
}
+
+ @Override
+ public void onBindViewHolder(PreferenceViewHolder holder) {
+ super.onBindViewHolder(holder);
+
+ ViewUtils.makeAllViewsUxRestricted(holder.itemView, isUxRestricted());
+ }
+
+ @Override
+ @SuppressWarnings("RestrictTo")
+ public void performClick() {
+ if ((isEnabled() || isSelectable()) && isUxRestricted()) {
+ if (mRestrictedClickListener != null) {
+ mRestrictedClickListener.accept(this);
+ }
+ } else {
+ super.performClick();
+ }
+ }
+
+ @Override
+ public void setUxRestricted(boolean restricted) {
+ if (restricted != mUxRestricted) {
+ mUxRestricted = restricted;
+ notifyChanged();
+ }
+ }
+
+ @Override
+ public boolean isUxRestricted() {
+ return mUxRestricted;
+ }
+
+ @Override
+ public void setOnClickWhileRestrictedListener(@Nullable Consumer<Preference> listener) {
+ mRestrictedClickListener = listener;
+ }
+
+ @Nullable
+ @Override
+ public Consumer<Preference> getOnClickWhileRestrictedListener() {
+ return mRestrictedClickListener;
+ }
}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiPreference.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiPreference.java
index 8db5733..348b082 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiPreference.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiPreference.java
@@ -18,36 +18,31 @@
import android.content.Context;
import android.content.res.TypedArray;
-import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
-import android.view.View;
-import android.widget.Toast;
-import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
import com.android.car.ui.R;
-import com.android.car.ui.utils.CarUiUtils;
+import com.android.car.ui.utils.ViewUtils;
+
+import java.util.function.Consumer;
/**
* This class extends the base {@link Preference} class. Adds the support to add a drawable icon to
* the preference if there is one of fragment, intent or onPreferenceClickListener set.
*/
public class CarUiPreference extends Preference implements DisabledPreferenceCallback {
-
- private Context mContext;
private boolean mShowChevron;
- private String mMessageToShowWhenDisabledPreferenceClicked;
- private boolean mShouldShowRippleOnDisabledPreference;
- private Drawable mBackground;
- private View mPreference;
+ private Consumer<Preference> mRestrictedClickListener;
+ private boolean mUxRestricted = false;
public CarUiPreference(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
- init(context, attrs, defStyleAttr, defStyleRes);
+ init(attrs, defStyleAttr, defStyleRes);
}
public CarUiPreference(Context context, AttributeSet attrs, int defStyleAttr) {
@@ -62,9 +57,7 @@
this(context, null);
}
- public void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- mContext = context;
-
+ private void init(AttributeSet attrs, int defStyleAttr, int defStyleRes) {
TypedArray a = getContext().obtainStyledAttributes(
attrs,
R.styleable.CarUiPreference,
@@ -72,27 +65,23 @@
defStyleRes);
mShowChevron = a.getBoolean(R.styleable.CarUiPreference_showChevron, true);
- mShouldShowRippleOnDisabledPreference = a.getBoolean(
- R.styleable.CarUiPreference_showRippleOnDisabledPreference, false);
+ mUxRestricted = a.getBoolean(R.styleable.CarUiPreference_car_ui_ux_restricted, false);
a.recycle();
}
-
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
- boolean viewEnabled = isEnabled();
- mPreference = holder.itemView;
- mBackground = CarUiUtils.setPreferenceViewEnabled(viewEnabled, holder.itemView, mBackground,
- mShouldShowRippleOnDisabledPreference);
+
+ ViewUtils.makeAllViewsUxRestricted(holder.itemView, isUxRestricted());
}
@Override
public void onAttached() {
super.onAttached();
- boolean allowChevron = mContext.getResources().getBoolean(
+ boolean allowChevron = getContext().getResources().getBoolean(
R.bool.car_ui_preference_show_chevron);
if (!allowChevron || !mShowChevron) {
@@ -105,29 +94,15 @@
}
}
- /**
- * An exact copy of {@link androidx.preference.Preference#performClick(View)}
- * This method was added here because super.performClick(View) is not open
- * for app usage.
- */
- @SuppressWarnings("RestrictTo")
- void performClickUnrestricted(View v) {
- performClick();
- }
-
- /**
- * This is similar to {@link Preference#performClick()} with the only difference that we do not
- * return when view is not enabled.
- */
@Override
@SuppressWarnings("RestrictTo")
public void performClick() {
- if (isEnabled()) {
+ if ((isEnabled() || isSelectable()) && isUxRestricted()) {
+ if (mRestrictedClickListener != null) {
+ mRestrictedClickListener.accept(this);
+ }
+ } else {
super.performClick();
- } else if (mMessageToShowWhenDisabledPreferenceClicked != null
- && !mMessageToShowWhenDisabledPreferenceClicked.isEmpty()) {
- Toast.makeText(mContext, mMessageToShowWhenDisabledPreferenceClicked,
- Toast.LENGTH_LONG).show();
}
}
@@ -135,18 +110,25 @@
mShowChevron = showChevron;
}
- /**
- * Sets the ripple on the disabled preference.
- */
@Override
- public void setShouldShowRippleOnDisabledPreference(boolean showRipple) {
- mShouldShowRippleOnDisabledPreference = showRipple;
- CarUiUtils.updateRippleStateOnDisabledPreference(isEnabled(),
- mShouldShowRippleOnDisabledPreference, mBackground, mPreference);
+ public boolean isUxRestricted() {
+ return mUxRestricted;
}
@Override
- public void setMessageToShowWhenDisabledPreferenceClicked(@NonNull String message) {
- mMessageToShowWhenDisabledPreferenceClicked = message;
+ public void setOnClickWhileRestrictedListener(@Nullable Consumer<Preference> listener) {
+ mRestrictedClickListener = listener;
+ }
+
+ @Nullable
+ @Override
+ public Consumer<Preference> getOnClickWhileRestrictedListener() {
+ return mRestrictedClickListener;
+ }
+
+ @Override
+ public void setUxRestricted(boolean restricted) {
+ mUxRestricted = restricted;
+ notifyChanged();
}
}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiRadioButtonPreference.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiRadioButtonPreference.java
index f02f105..8b10fc3 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiRadioButtonPreference.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiRadioButtonPreference.java
@@ -20,14 +20,24 @@
import android.util.AttributeSet;
import android.widget.RadioButton;
+import androidx.annotation.Nullable;
+import androidx.core.content.res.TypedArrayUtils;
+import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
import androidx.preference.TwoStatePreference;
import com.android.car.ui.R;
import com.android.car.ui.utils.CarUiUtils;
+import com.android.car.ui.utils.ViewUtils;
+
+import java.util.function.Consumer;
/** A preference which shows a radio button at the start of the preference. */
-public class CarUiRadioButtonPreference extends TwoStatePreference {
+public class CarUiRadioButtonPreference extends TwoStatePreference
+ implements UxRestrictablePreference {
+
+ private Consumer<Preference> mRestrictedClickListener;
+ private boolean mUxRestricted = false;
public CarUiRadioButtonPreference(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
@@ -36,18 +46,18 @@
}
public CarUiRadioButtonPreference(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- init();
+ this(context, attrs, defStyleAttr, /* defStyleRes= */ 0);
}
public CarUiRadioButtonPreference(Context context, AttributeSet attrs) {
- super(context, attrs);
- init();
+ // Reusing preferenceStyle since there is no separate style for TwoStatePreference or
+ // CarUiRadioButtonPreference.
+ this(context, attrs, TypedArrayUtils.getAttr(context, R.attr.preferenceStyle,
+ android.R.attr.preferenceStyle));
}
public CarUiRadioButtonPreference(Context context) {
- super(context);
- init();
+ this(context, /* attrs= */ null);
}
private void init() {
@@ -56,11 +66,49 @@
}
@Override
+ @SuppressWarnings("RestrictTo")
+ public void performClick() {
+ if ((isEnabled() || isSelectable()) && isUxRestricted()) {
+ if (mRestrictedClickListener != null) {
+ mRestrictedClickListener.accept(this);
+ }
+ } else {
+ super.performClick();
+ }
+ }
+
+ @Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
RadioButton radioButton = (RadioButton) CarUiUtils.findViewByRefId(holder.itemView,
R.id.radio_button);
radioButton.setChecked(isChecked());
+
+ ViewUtils.makeAllViewsUxRestricted(holder.itemView, mUxRestricted);
+ }
+
+ @Override
+ public void setUxRestricted(boolean restricted) {
+ if (restricted != mUxRestricted) {
+ mUxRestricted = restricted;
+ notifyChanged();
+ }
+ }
+
+ @Override
+ public boolean isUxRestricted() {
+ return mUxRestricted;
+ }
+
+ @Override
+ public void setOnClickWhileRestrictedListener(@Nullable Consumer<Preference> listener) {
+ mRestrictedClickListener = listener;
+ }
+
+ @Nullable
+ @Override
+ public Consumer<Preference> getOnClickWhileRestrictedListener() {
+ return mRestrictedClickListener;
}
}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiSeekBarDialogPreference.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiSeekBarDialogPreference.java
index 29e9e33..04f73ff 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiSeekBarDialogPreference.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiSeekBarDialogPreference.java
@@ -25,13 +25,18 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.DialogPreference;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceViewHolder;
import com.android.car.ui.R;
import com.android.car.ui.utils.CarUiUtils;
+import com.android.car.ui.utils.ViewUtils;
+
+import java.util.function.Consumer;
/** A class implements some basic methods of a seekbar dialog preference. */
public class CarUiSeekBarDialogPreference extends DialogPreference
- implements DialogFragmentCallbacks {
+ implements DialogFragmentCallbacks, UxRestrictablePreference {
private int mSeekBarProgress;
private SeekBar mSeekBar;
@@ -51,6 +56,9 @@
private SeekBar.OnSeekBarChangeListener mOnSeekBarChangeListener;
private int mMaxProgress = 100;
+ private Consumer<Preference> mRestrictedClickListener;
+ private boolean mUxRestricted = false;
+
public CarUiSeekBarDialogPreference(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
@@ -265,4 +273,47 @@
}
mMaxProgress = maxProgress;
}
+
+ @Override
+ public void onBindViewHolder(PreferenceViewHolder holder) {
+ super.onBindViewHolder(holder);
+
+ ViewUtils.makeAllViewsUxRestricted(holder.itemView, isUxRestricted());
+ }
+
+ @Override
+ @SuppressWarnings("RestrictTo")
+ public void performClick() {
+ if ((isEnabled() || isSelectable()) && isUxRestricted()) {
+ if (mRestrictedClickListener != null) {
+ mRestrictedClickListener.accept(this);
+ }
+ } else {
+ super.performClick();
+ }
+ }
+
+ @Override
+ public void setUxRestricted(boolean restricted) {
+ if (restricted != mUxRestricted) {
+ mUxRestricted = restricted;
+ notifyChanged();
+ }
+ }
+
+ @Override
+ public boolean isUxRestricted() {
+ return mUxRestricted;
+ }
+
+ @Override
+ public void setOnClickWhileRestrictedListener(@Nullable Consumer<Preference> listener) {
+ mRestrictedClickListener = listener;
+ }
+
+ @Nullable
+ @Override
+ public Consumer<Preference> getOnClickWhileRestrictedListener() {
+ return mRestrictedClickListener;
+ }
}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiSwitchPreference.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiSwitchPreference.java
index e2b141d..bfd7a1a 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiSwitchPreference.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiSwitchPreference.java
@@ -18,97 +18,94 @@
import android.content.Context;
import android.content.res.TypedArray;
-import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
-import android.view.View;
-import android.widget.Toast;
-import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
import androidx.preference.SwitchPreference;
import com.android.car.ui.R;
-import com.android.car.ui.utils.CarUiUtils;
+import com.android.car.ui.utils.ViewUtils;
+
+import java.util.function.Consumer;
/**
- * This class extends the base {@link SwitchPreference} class. Adds the functionality to show
- * message when preference is disabled.
+ * This class is the same as the base {@link SwitchPreference} class, except it implements
+ * {@link UxRestrictablePreference}
*/
public class CarUiSwitchPreference extends SwitchPreference implements DisabledPreferenceCallback {
- private String mMessageToShowWhenDisabledPreferenceClicked;
-
- private boolean mShouldShowRippleOnDisabledPreference;
- private Drawable mBackground;
- private View mPreference;
- private Context mContext;
+ private Consumer<Preference> mRestrictedClickListener;
+ private boolean mUxRestricted = false;
public CarUiSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
- init(context, attrs);
+ init(attrs);
}
public CarUiSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
- init(context, attrs);
+ init(attrs);
}
public CarUiSwitchPreference(Context context, AttributeSet attrs) {
super(context, attrs);
- init(context, attrs);
+ init(attrs);
}
public CarUiSwitchPreference(Context context) {
super(context);
- init(context, null);
+ init(null);
}
- private void init(Context context, AttributeSet attrs) {
- mContext = context;
- TypedArray preferenceAttributes = getContext().obtainStyledAttributes(attrs,
- R.styleable.CarUiPreference);
- mShouldShowRippleOnDisabledPreference = preferenceAttributes.getBoolean(
- R.styleable.CarUiPreference_showRippleOnDisabledPreference, false);
- preferenceAttributes.recycle();
+ private void init(AttributeSet attrs) {
+ TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.CarUiPreference);
+ mUxRestricted = a.getBoolean(R.styleable.CarUiPreference_car_ui_ux_restricted, false);
+ a.recycle();
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
- mPreference = holder.itemView;
- mBackground = CarUiUtils.setPreferenceViewEnabled(isEnabled(), holder.itemView, mBackground,
- mShouldShowRippleOnDisabledPreference);
+
+ ViewUtils.makeAllViewsUxRestricted(holder.itemView, isUxRestricted());
}
- /**
- * This is similar to {@link Preference#performClick()} with the only difference that we do not
- * return when view is not enabled.
- */
@Override
@SuppressWarnings("RestrictTo")
public void performClick() {
- if (isEnabled()) {
+ if ((isEnabled() || isSelectable()) && isUxRestricted()) {
+ if (mRestrictedClickListener != null) {
+ mRestrictedClickListener.accept(this);
+ }
+ } else {
super.performClick();
- } else if (mMessageToShowWhenDisabledPreferenceClicked != null
- && !mMessageToShowWhenDisabledPreferenceClicked.isEmpty()) {
- Toast.makeText(mContext, mMessageToShowWhenDisabledPreferenceClicked,
- Toast.LENGTH_LONG).show();
}
}
- /**
- * Sets the ripple on the disabled preference.
- */
@Override
- public void setShouldShowRippleOnDisabledPreference(boolean showRipple) {
- mShouldShowRippleOnDisabledPreference = showRipple;
- CarUiUtils.updateRippleStateOnDisabledPreference(isEnabled(),
- mShouldShowRippleOnDisabledPreference, mBackground, mPreference);
+ public void setUxRestricted(boolean restricted) {
+ if (mUxRestricted != restricted) {
+ mUxRestricted = restricted;
+ notifyChanged();
+ }
}
@Override
- public void setMessageToShowWhenDisabledPreferenceClicked(@NonNull String message) {
- mMessageToShowWhenDisabledPreferenceClicked = message;
+ public boolean isUxRestricted() {
+ return mUxRestricted;
+ }
+
+ @Override
+ public void setOnClickWhileRestrictedListener(@Nullable Consumer<Preference> listener) {
+ mRestrictedClickListener = listener;
+ }
+
+ @Nullable
+ @Override
+ public Consumer<Preference> getOnClickWhileRestrictedListener() {
+ return mRestrictedClickListener;
}
}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiTwoActionBasePreference.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiTwoActionBasePreference.java
new file mode 100644
index 0000000..5151642
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiTwoActionBasePreference.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2020 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.ui.preference;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.LayoutRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StyleableRes;
+
+import com.android.car.ui.R;
+
+/**
+ * A base class for several types of preferences, that all have a main click action along
+ * with a secondary action.
+ */
+public abstract class CarUiTwoActionBasePreference extends CarUiPreference {
+
+ protected boolean mSecondaryActionEnabled = true;
+ protected boolean mSecondaryActionVisible = true;
+
+ public CarUiTwoActionBasePreference(Context context,
+ AttributeSet attrs,
+ int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ init(attrs);
+ }
+
+ public CarUiTwoActionBasePreference(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(attrs);
+ }
+
+ public CarUiTwoActionBasePreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(attrs);
+ }
+
+ public CarUiTwoActionBasePreference(Context context) {
+ super(context);
+ init(null);
+ }
+
+ @CallSuper
+ protected void init(@Nullable AttributeSet attrs) {
+ setShowChevron(false);
+
+ TypedArray a = getContext()
+ .obtainStyledAttributes(attrs, R.styleable.CarUiTwoActionBasePreference);
+ try {
+ disallowResourceIds(a,
+ R.styleable.CarUiTwoActionBasePreference_layout,
+ R.styleable.CarUiTwoActionBasePreference_android_layout,
+ R.styleable.CarUiTwoActionBasePreference_widgetLayout,
+ R.styleable.CarUiTwoActionBasePreference_android_widgetLayout);
+ } finally {
+ a.recycle();
+ }
+
+ a = getContext().obtainStyledAttributes(attrs,
+ R.styleable.CarUiTwoActionPreference);
+
+ try {
+ mSecondaryActionVisible = a.getBoolean(
+ R.styleable.CarUiTwoActionPreference_actionShown, true);
+ mSecondaryActionEnabled = a.getBoolean(
+ R.styleable.CarUiTwoActionPreference_actionEnabled, true);
+ } finally {
+ a.recycle();
+ }
+ }
+
+ /**
+ * Returns whether or not the secondary action is enabled.
+ */
+ public boolean isSecondaryActionEnabled() {
+ return mSecondaryActionEnabled && isEnabled();
+ }
+
+ /**
+ * Sets whether or not the secondary action is enabled. This is secondary to the overall
+ * {@link #setEnabled(boolean)} of the preference
+ */
+ public void setSecondaryActionEnabled(boolean enabled) {
+ mSecondaryActionEnabled = enabled;
+ notifyChanged();
+ }
+
+ /**
+ * Returns whether or not the secondary action is visible.
+ */
+ public boolean isSecondaryActionVisible() {
+ return mSecondaryActionVisible;
+ }
+
+ /**
+ * Sets whether or not the secondary action is visible.
+ */
+ public void setSecondaryActionVisible(boolean visible) {
+ mSecondaryActionVisible = visible;
+ notifyChanged();
+ }
+
+ /**
+ * Like {@link #onClick()}, but for the secondary action.
+ */
+ public void performSecondaryActionClick() {
+ if (mSecondaryActionEnabled && mSecondaryActionVisible) {
+ performSecondaryActionClickInternal();
+ }
+ }
+
+ protected abstract void performSecondaryActionClickInternal();
+
+ protected void setLayoutResourceInternal(@LayoutRes int layoutResId) {
+ super.setLayoutResource(layoutResId);
+ }
+
+ @Override
+ public void setLayoutResource(@LayoutRes int layoutResId) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setWidgetLayoutResource(@LayoutRes int widgetLayoutResId) {
+ throw new UnsupportedOperationException();
+ }
+
+ private static void disallowResourceIds(@NonNull TypedArray a, @StyleableRes int ...indices) {
+ for (int index : indices) {
+ if (a.hasValue(index)) {
+ throw new AssertionError("Setting this attribute is not allowed: "
+ + a.getResources().getResourceName(index));
+ }
+ }
+ }
+}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiTwoActionIconPreference.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiTwoActionIconPreference.java
new file mode 100644
index 0000000..57c65df
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiTwoActionIconPreference.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2020 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.ui.preference;
+
+import static com.android.car.ui.utils.CarUiUtils.requireViewByRefId;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import androidx.annotation.DrawableRes;
+import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceViewHolder;
+
+import com.android.car.ui.R;
+
+import java.util.function.Consumer;
+
+/**
+ * A preference that has an icon button that can be pressed independently of pressing the main
+ * body of the preference.
+ */
+public class CarUiTwoActionIconPreference extends CarUiTwoActionBasePreference {
+ @Nullable
+ protected Runnable mSecondaryActionOnClickListener;
+ @Nullable
+ private Drawable mSecondaryActionIcon;
+
+ public CarUiTwoActionIconPreference(Context context,
+ AttributeSet attrs,
+ int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public CarUiTwoActionIconPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public CarUiTwoActionIconPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public CarUiTwoActionIconPreference(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected void init(@Nullable AttributeSet attrs) {
+ super.init(attrs);
+
+ TypedArray a = getContext().obtainStyledAttributes(attrs,
+ R.styleable.CarUiTwoActionIconPreference);
+ try {
+ mSecondaryActionIcon = a.getDrawable(
+ R.styleable.CarUiTwoActionIconPreference_secondaryActionIcon);
+ } finally {
+ a.recycle();
+ }
+
+ setLayoutResourceInternal(R.layout.car_ui_preference_two_action_icon);
+ }
+
+ @Override
+ protected void performSecondaryActionClickInternal() {
+ if (isSecondaryActionEnabled()) {
+ if (isUxRestricted()) {
+ Consumer<Preference> restrictedListener = getOnClickWhileRestrictedListener();
+ if (restrictedListener != null) {
+ restrictedListener.accept(this);
+ }
+ } else if (mSecondaryActionOnClickListener != null) {
+ mSecondaryActionOnClickListener.run();
+ }
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(PreferenceViewHolder holder) {
+ super.onBindViewHolder(holder);
+
+ View firstActionContainer = requireViewByRefId(holder.itemView,
+ R.id.car_ui_first_action_container);
+ View secondActionContainer = requireViewByRefId(holder.itemView,
+ R.id.car_ui_second_action_container);
+ ViewGroup secondaryButton = requireViewByRefId(holder.itemView,
+ R.id.car_ui_secondary_action);
+ ImageView iconView = requireViewByRefId(holder.itemView,
+ R.id.car_ui_secondary_action_concrete);
+
+ holder.itemView.setFocusable(false);
+ holder.itemView.setClickable(false);
+
+ firstActionContainer.setOnClickListener(this::performClick);
+ firstActionContainer.setEnabled(isEnabled());
+ firstActionContainer.setFocusable(isEnabled());
+
+ secondActionContainer.setVisibility(mSecondaryActionVisible ? View.VISIBLE : View.GONE);
+ iconView.setImageDrawable(mSecondaryActionIcon);
+ iconView.setEnabled(isSecondaryActionEnabled());
+ secondaryButton.setEnabled(isSecondaryActionEnabled());
+ secondaryButton.setFocusable(isSecondaryActionEnabled());
+ secondaryButton.setOnClickListener(v -> performSecondaryActionClickInternal());
+ }
+
+ /**
+ * Sets the icon of the secondary action.
+ *
+ * The icon will be tinted to the primary text color, and resized to fit the space.
+ *
+ * @param drawable A {@link Drawable} to set as the icon.
+ */
+ public void setSecondaryActionIcon(@Nullable Drawable drawable) {
+ mSecondaryActionIcon = drawable;
+ notifyChanged();
+ }
+
+ /**
+ * Sets the icon of the secondary action.
+ *
+ * The icon will be tinted to the primary text color, and resized to fit the space.
+ *
+ * @param resid A drawable resource id to set as the icon.
+ */
+ public void setSecondaryActionIcon(@DrawableRes int resid) {
+ setSecondaryActionIcon(ContextCompat.getDrawable(getContext(), resid));
+ }
+
+ /**
+ * Sets the on-click listener of the secondary action button.
+ */
+ public void setOnSecondaryActionClickListener(@Nullable Runnable onClickListener) {
+ mSecondaryActionOnClickListener = onClickListener;
+ notifyChanged();
+ }
+}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiTwoActionPreference.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiTwoActionPreference.java
index 3b9693a..96fb8d5 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiTwoActionPreference.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiTwoActionPreference.java
@@ -30,6 +30,10 @@
/**
* A preference which can perform two actions. The secondary action is shown by default.
* {@link #showAction(boolean)} may be used to manually set the visibility of the action.
+ *
+ * @deprecated This preference relies on the use of custom views. Use
+ * {@link CarUiTwoActionTextPreference}, {@link CarUiTwoActionSwitchPreference}, or
+ * {@link CarUiTwoActionIconPreference} instead.
*/
public class CarUiTwoActionPreference extends CarUiPreference {
@@ -95,7 +99,7 @@
View widgetFrame = CarUiUtils.findViewByRefId(holder.itemView, android.R.id.widget_frame);
holder.itemView.setFocusable(!mIsActionShown);
containerWithoutWidget.setOnClickListener(
- mIsActionShown ? this::performClickUnrestricted : null);
+ mIsActionShown ? this::performClick : null);
containerWithoutWidget.setClickable(mIsActionShown);
containerWithoutWidget.setFocusable(mIsActionShown);
actionContainer.setVisibility(mIsActionShown ? View.VISIBLE : View.GONE);
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiTwoActionSwitchPreference.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiTwoActionSwitchPreference.java
new file mode 100644
index 0000000..22497cc
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiTwoActionSwitchPreference.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2020 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.ui.preference;
+
+import static com.android.car.ui.utils.CarUiUtils.requireViewByRefId;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.Switch;
+
+import androidx.annotation.Nullable;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceViewHolder;
+
+import com.android.car.ui.R;
+
+import java.util.function.Consumer;
+
+/**
+ * A preference that has a switch that can be toggled independently of pressing the main
+ * body of the preference.
+ */
+public class CarUiTwoActionSwitchPreference extends CarUiTwoActionBasePreference {
+ @Nullable
+ protected Consumer<Boolean> mSecondaryActionOnClickListener;
+ private boolean mSecondaryActionChecked;
+
+ public CarUiTwoActionSwitchPreference(Context context,
+ AttributeSet attrs,
+ int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public CarUiTwoActionSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public CarUiTwoActionSwitchPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public CarUiTwoActionSwitchPreference(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected void init(@Nullable AttributeSet attrs) {
+ super.init(attrs);
+
+ setLayoutResourceInternal(R.layout.car_ui_preference_two_action_switch);
+ }
+
+ @Override
+ protected void performSecondaryActionClickInternal() {
+ if (isSecondaryActionEnabled()) {
+ if (isUxRestricted()) {
+ Consumer<Preference> restrictedListener = getOnClickWhileRestrictedListener();
+ if (restrictedListener != null) {
+ restrictedListener.accept(this);
+ }
+ } else {
+ mSecondaryActionChecked = !mSecondaryActionChecked;
+ notifyChanged();
+ if (mSecondaryActionOnClickListener != null) {
+ mSecondaryActionOnClickListener.accept(mSecondaryActionChecked);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(PreferenceViewHolder holder) {
+ super.onBindViewHolder(holder);
+
+ View firstActionContainer = requireViewByRefId(holder.itemView,
+ R.id.car_ui_first_action_container);
+ View secondActionContainer = requireViewByRefId(holder.itemView,
+ R.id.car_ui_second_action_container);
+ View secondaryAction = requireViewByRefId(holder.itemView,
+ R.id.car_ui_secondary_action);
+ Switch s = requireViewByRefId(holder.itemView,
+ R.id.car_ui_secondary_action_concrete);
+
+ holder.itemView.setFocusable(false);
+ holder.itemView.setClickable(false);
+ firstActionContainer.setOnClickListener(this::performClick);
+ firstActionContainer.setEnabled(isEnabled() || isUxRestricted());
+ firstActionContainer.setFocusable(isEnabled() || isUxRestricted());
+
+ secondActionContainer.setVisibility(mSecondaryActionVisible ? View.VISIBLE : View.GONE);
+ s.setChecked(mSecondaryActionChecked);
+ s.setEnabled(isSecondaryActionEnabled());
+
+ secondaryAction.setOnClickListener(v -> performSecondaryActionClickInternal());
+ secondaryAction.setEnabled(isSecondaryActionEnabled() || isUxRestricted());
+ secondaryAction.setFocusable(isSecondaryActionEnabled() || isUxRestricted());
+ }
+
+ /**
+ * Sets the checked state of the switch in the secondary action space.
+ * @param checked Whether the switch should be checked or not.
+ */
+ public void setSecondaryActionChecked(boolean checked) {
+ mSecondaryActionChecked = checked;
+ notifyChanged();
+ }
+
+ /**
+ * Returns the checked state of the switch in the secondary action space.
+ * @return Whether the switch is checked or not.
+ */
+ public boolean isSecondaryActionChecked() {
+ return mSecondaryActionChecked;
+ }
+
+ /**
+ * Sets the on-click listener of the secondary action button.
+ *
+ * The listener is called with the current checked state of the switch.
+ */
+ public void setOnSecondaryActionClickListener(@Nullable Consumer<Boolean> onClickListener) {
+ mSecondaryActionOnClickListener = onClickListener;
+ notifyChanged();
+ }
+}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiTwoActionTextPreference.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiTwoActionTextPreference.java
new file mode 100644
index 0000000..9542d29
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiTwoActionTextPreference.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2020 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.ui.preference;
+
+import static com.android.car.ui.utils.CarUiUtils.requireViewByRefId;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.Button;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceViewHolder;
+
+import com.android.car.ui.R;
+
+import java.util.function.Consumer;
+
+/**
+ * A preference that has a text button that can be pressed independently of pressing the main
+ * body of the preference.
+ */
+public class CarUiTwoActionTextPreference extends CarUiTwoActionBasePreference {
+
+ @Nullable
+ protected Runnable mSecondaryActionOnClickListener;
+ @Nullable
+ private CharSequence mSecondaryActionText;
+
+ public CarUiTwoActionTextPreference(Context context,
+ AttributeSet attrs,
+ int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public CarUiTwoActionTextPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public CarUiTwoActionTextPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public CarUiTwoActionTextPreference(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected void init(@Nullable AttributeSet attrs) {
+ super.init(attrs);
+
+ TypedArray a = getContext().obtainStyledAttributes(attrs,
+ R.styleable.CarUiTwoActionTextPreference);
+ int actionStyle = 0;
+ try {
+ actionStyle = a.getInteger(
+ R.styleable.CarUiTwoActionTextPreference_secondaryActionStyle, 0);
+ mSecondaryActionText = a.getString(
+ R.styleable.CarUiTwoActionTextPreference_secondaryActionText);
+ } finally {
+ a.recycle();
+ }
+
+ setLayoutResourceInternal(actionStyle == 0
+ ? R.layout.car_ui_preference_two_action_text
+ : R.layout.car_ui_preference_two_action_text_borderless);
+ }
+
+ @Override
+ protected void performSecondaryActionClickInternal() {
+ if (isSecondaryActionEnabled()) {
+ if (isUxRestricted()) {
+ Consumer<Preference> restrictedListener = getOnClickWhileRestrictedListener();
+ if (restrictedListener != null) {
+ restrictedListener.accept(this);
+ }
+ } else if (mSecondaryActionOnClickListener != null) {
+ mSecondaryActionOnClickListener.run();
+ }
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(PreferenceViewHolder holder) {
+ super.onBindViewHolder(holder);
+
+ View firstActionContainer = requireViewByRefId(holder.itemView,
+ R.id.car_ui_first_action_container);
+ View secondActionContainer = requireViewByRefId(holder.itemView,
+ R.id.car_ui_second_action_container);
+ Button secondaryButton = requireViewByRefId(holder.itemView,
+ R.id.car_ui_secondary_action);
+
+ holder.itemView.setFocusable(false);
+ holder.itemView.setClickable(false);
+ firstActionContainer.setOnClickListener(this::performClick);
+ firstActionContainer.setEnabled(isEnabled());
+ firstActionContainer.setFocusable(isEnabled());
+
+ secondActionContainer.setVisibility(mSecondaryActionVisible ? View.VISIBLE : View.GONE);
+ secondaryButton.setText(mSecondaryActionText);
+ secondaryButton.setOnClickListener(v -> performSecondaryActionClickInternal());
+ secondaryButton.setEnabled(isSecondaryActionEnabled());
+ secondaryButton.setFocusable(isSecondaryActionEnabled());
+ }
+
+ @Nullable
+ public CharSequence getSecondaryActionText() {
+ return mSecondaryActionText;
+ }
+
+ /**
+ * Sets the title of the secondary action button.
+ *
+ * @param title The text to display on the secondary action.
+ */
+ public void setSecondaryActionText(@Nullable CharSequence title) {
+ mSecondaryActionText = title;
+ notifyChanged();
+ }
+
+ /**
+ * Sets the title of the secondary action button.
+ *
+ * @param resid A string resource of the text to display on the secondary action.
+ */
+ public void setSecondaryActionText(@StringRes int resid) {
+ setSecondaryActionText(getContext().getString(resid));
+ }
+
+ /**
+ * Sets the on-click listener of the secondary action button.
+ */
+ public void setOnSecondaryActionClickListener(@Nullable Runnable onClickListener) {
+ mSecondaryActionOnClickListener = onClickListener;
+ notifyChanged();
+ }
+}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/DisabledPreferenceCallback.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/DisabledPreferenceCallback.java
index ea3585b..f6926be 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/DisabledPreferenceCallback.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/DisabledPreferenceCallback.java
@@ -16,20 +16,26 @@
package com.android.car.ui.preference;
+import android.util.Log;
+
import androidx.annotation.NonNull;
/**
* Interface for preferences to handle clicks when its disabled.
+ *
+ * @deprecated use {@link UxRestrictablePreference} instead
*/
-public interface DisabledPreferenceCallback {
-
- /**
- * Sets if the ripple effect should be shown on disabled preference.
- */
- default void setShouldShowRippleOnDisabledPreference(boolean showRipple) {}
+@Deprecated
+public interface DisabledPreferenceCallback extends UxRestrictablePreference {
/**
* Sets the message to be displayed when the disabled preference is clicked.
+ *
+ * @deprecated Use {@link UxRestrictablePreference#setUxRestricted(boolean)} instead.
*/
- default void setMessageToShowWhenDisabledPreferenceClicked(@NonNull String message) {}
+ @Deprecated
+ default void setMessageToShowWhenDisabledPreferenceClicked(@NonNull String message) {
+ Log.w("carui",
+ "setMessageToShowWhenDisabledPreferenceClicked is deprecated, and does nothing!");
+ }
}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/ListPreferenceFragment.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/ListPreferenceFragment.java
index b9ad789..12ed526 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/ListPreferenceFragment.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/ListPreferenceFragment.java
@@ -58,6 +58,7 @@
private CarUiContentListItem mSelectedItem;
private int mSelectedIndex = -1;
private boolean mFullScreen;
+ private boolean mUseInstantPreferenceChangeCallback;
/**
* Returns a new instance of {@link ListPreferenceFragment} for the {@link ListPreference} with
@@ -90,6 +91,8 @@
super.onViewCreated(view, savedInstanceState);
final CarUiRecyclerView carUiRecyclerView = CarUiUtils.requireViewByRefId(view, R.id.list);
mFullScreen = requireArguments().getBoolean(ARG_FULLSCREEN, true);
+ mUseInstantPreferenceChangeCallback =
+ getResources().getBoolean(R.bool.car_ui_preference_list_instant_change_callback);
ToolbarController toolbar = mFullScreen ? CarUi.getToolbar(getActivity()) : null;
// TODO(b/150230923) remove the code for the old toolbar height change when apps are ready
@@ -153,12 +156,21 @@
}
item.setOnCheckedChangeListener((listItem, isChecked) -> {
+ if (!isChecked) {
+ // Previously selected item is unchecked now, no further processing is needed.
+ return;
+ }
+
if (mSelectedItem != null) {
mSelectedItem.setChecked(false);
adapter.notifyItemChanged(listItems.indexOf(mSelectedItem));
}
mSelectedItem = listItem;
mSelectedIndex = listItems.indexOf(mSelectedItem);
+
+ if (mUseInstantPreferenceChangeCallback) {
+ updatePreference();
+ }
});
listItems.add(item);
@@ -179,7 +191,10 @@
@Override
public void onStop() {
super.onStop();
- updatePreference();
+
+ if (!mUseInstantPreferenceChangeCallback) {
+ updatePreference();
+ }
}
private void updatePreference() {
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/MultiSelectListPreferenceFragment.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/MultiSelectListPreferenceFragment.java
index f9cf8a2..a93bad6 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/MultiSelectListPreferenceFragment.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/MultiSelectListPreferenceFragment.java
@@ -59,6 +59,7 @@
private Set<String> mNewValues;
private ToolbarController mToolbar;
private boolean mFullScreen;
+ private boolean mUseInstantPreferenceChangeCallback;
/**
* Returns a new instance of {@link MultiSelectListPreferenceFragment} for the {@link
@@ -91,6 +92,8 @@
super.onViewCreated(view, savedInstanceState);
final CarUiRecyclerView recyclerView = CarUiUtils.requireViewByRefId(view, R.id.list);
mFullScreen = requireArguments().getBoolean(ARG_FULLSCREEN, true);
+ mUseInstantPreferenceChangeCallback =
+ getResources().getBoolean(R.bool.car_ui_preference_list_instant_change_callback);
mToolbar = mFullScreen ? CarUi.getToolbar(requireActivity()) : null;
// TODO(b/150230923) remove the code for the old toolbar height change when apps are ready
@@ -158,6 +161,10 @@
} else {
mNewValues.remove(entryValue);
}
+
+ if (mUseInstantPreferenceChangeCallback) {
+ updatePreference();
+ }
});
listItems.add(item);
@@ -179,7 +186,9 @@
@Override
public void onStop() {
super.onStop();
- updatePreference();
+ if (!mUseInstantPreferenceChangeCallback) {
+ updatePreference();
+ }
}
private void updatePreference() {
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/UxRestrictablePreference.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/UxRestrictablePreference.java
new file mode 100644
index 0000000..4774c2c
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/UxRestrictablePreference.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.ui.preference;
+
+import androidx.annotation.Nullable;
+import androidx.preference.Preference;
+
+import java.util.function.Consumer;
+
+/**
+ * An interface for preferences that can be ux restricted.
+ *
+ * A ux restricted preference will be displayed differently to indicate such, and will
+ * display a toast message informing the user they cannot click it when they try to.
+ */
+public interface UxRestrictablePreference {
+
+ /** Sets this preference as ux restricted or not */
+ void setUxRestricted(boolean restricted);
+
+ /** Returns if this preference is currently ux restricted */
+ boolean isUxRestricted();
+
+ /** Sets a listener to be called if the preference is clicked while it is ux restricted */
+ void setOnClickWhileRestrictedListener(@Nullable Consumer<Preference> listener);
+
+ /** Gets the listener to be called if the preference is clicked while it is ux restricted */
+ @Nullable
+ Consumer<Preference> getOnClickWhileRestrictedListener();
+}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiListItemAdapter.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiListItemAdapter.java
index a45b1e8..c452ee2 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiListItemAdapter.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiListItemAdapter.java
@@ -170,22 +170,25 @@
ListItemViewHolder(@NonNull View itemView) {
super(itemView);
- mTitle = requireViewByRefId(itemView, R.id.title);
- mBody = requireViewByRefId(itemView, R.id.body);
- mIcon = requireViewByRefId(itemView, R.id.icon);
- mContentIcon = requireViewByRefId(itemView, R.id.content_icon);
- mAvatarIcon = requireViewByRefId(itemView, R.id.avatar_icon);
- mIconContainer = requireViewByRefId(itemView, R.id.icon_container);
- mActionContainer = requireViewByRefId(itemView, R.id.action_container);
- mActionDivider = requireViewByRefId(itemView, R.id.action_divider);
- mSwitch = requireViewByRefId(itemView, R.id.switch_widget);
- mCheckBox = requireViewByRefId(itemView, R.id.checkbox_widget);
- mRadioButton = requireViewByRefId(itemView, R.id.radio_button_widget);
- mSupplementalIcon = requireViewByRefId(itemView, R.id.supplemental_icon);
- mReducedTouchInterceptor = requireViewByRefId(itemView, R.id.reduced_touch_interceptor);
- mTouchInterceptor = requireViewByRefId(itemView, R.id.touch_interceptor);
+ mTitle = requireViewByRefId(itemView, R.id.car_ui_list_item_title);
+ mBody = requireViewByRefId(itemView, R.id.car_ui_list_item_body);
+ mIcon = requireViewByRefId(itemView, R.id.car_ui_list_item_icon);
+ mContentIcon = requireViewByRefId(itemView, R.id.car_ui_list_item_content_icon);
+ mAvatarIcon = requireViewByRefId(itemView, R.id.car_ui_list_item_avatar_icon);
+ mIconContainer = requireViewByRefId(itemView, R.id.car_ui_list_item_icon_container);
+ mActionContainer = requireViewByRefId(itemView, R.id.car_ui_list_item_action_container);
+ mActionDivider = requireViewByRefId(itemView, R.id.car_ui_list_item_action_divider);
+ mSwitch = requireViewByRefId(itemView, R.id.car_ui_list_item_switch_widget);
+ mCheckBox = requireViewByRefId(itemView, R.id.car_ui_list_item_checkbox_widget);
+ mRadioButton = requireViewByRefId(itemView, R.id.car_ui_list_item_radio_button_widget);
+ mSupplementalIcon = requireViewByRefId(itemView,
+ R.id.car_ui_list_item_supplemental_icon);
+ mReducedTouchInterceptor = requireViewByRefId(itemView,
+ R.id.car_ui_list_item_reduced_touch_interceptor);
+ mTouchInterceptor = requireViewByRefId(itemView,
+ R.id.car_ui_list_item_touch_interceptor);
mActionContainerTouchInterceptor = requireViewByRefId(itemView,
- R.id.action_container_touch_interceptor);
+ R.id.car_ui_list_item_action_container_touch_interceptor);
}
void bind(@NonNull CarUiContentListItem item) {
@@ -371,8 +374,8 @@
HeaderViewHolder(@NonNull View itemView) {
super(itemView);
- mTitle = requireViewByRefId(itemView, R.id.title);
- mBody = requireViewByRefId(itemView, R.id.body);
+ mTitle = requireViewByRefId(itemView, R.id.car_ui_list_item_title);
+ mBody = requireViewByRefId(itemView, R.id.car_ui_list_item_body);
}
private void bind(@NonNull CarUiHeaderListItem item) {
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiRecyclerView.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiRecyclerView.java
index d94fbf3..3cf696d 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiRecyclerView.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiRecyclerView.java
@@ -77,7 +77,6 @@
private String mScrollBarClass;
private int mScrollBarPaddingTop;
private int mScrollBarPaddingBottom;
-
@Nullable
private ScrollBar mScrollBar;
@@ -101,7 +100,7 @@
@Nullable
private Rect mContainerPaddingRelative;
@Nullable
- private LinearLayout mContainer;
+ private ViewGroup mContainer;
// Set to true when when styled attributes are read and initialized.
private boolean mIsInitialized;
@@ -249,7 +248,7 @@
return;
}
- mContainer = new LinearLayout(getContext());
+ mContainer = new FrameLayout(getContext());
setVerticalScrollBarEnabled(false);
setHorizontalScrollBarEnabled(false);
@@ -302,7 +301,8 @@
boolean rotaryScrollEnabled = styledAttributes != null && styledAttributes.getBoolean(
R.styleable.CarUiRecyclerView_rotaryScrollEnabled, /* defValue=*/ false);
if (rotaryScrollEnabled) {
- int orientation = styledAttributes.getInt(R.styleable.RecyclerView_android_orientation,
+ int orientation = styledAttributes.getInt(
+ R.styleable.CarUiRecyclerView_android_orientation,
LinearLayout.VERTICAL);
CarUiUtils.setRotaryScrollEnabled(
this, /* isVertical= */ orientation == LinearLayout.VERTICAL);
@@ -408,6 +408,15 @@
* the recycler view was set with the same layout params.
*/
private void installExternalScrollBar() {
+ if (mContainer.getParent() != null) {
+ // We've already installed the parent container.
+ // onAttachToWindow() can be called multiple times, but on the second time
+ // we will crash if we try to add mContainer as a child of a view again while
+ // it already has a parent.
+ return;
+ }
+
+ mContainer.removeAllViews();
LayoutInflater inflater = LayoutInflater.from(getContext());
inflater.inflate(R.layout.car_ui_recycler_view, mContainer, true);
mContainer.setVisibility(mContainerVisibility);
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/DefaultScrollBar.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/DefaultScrollBar.java
index 42554bf..0199d2d 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/DefaultScrollBar.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/DefaultScrollBar.java
@@ -28,6 +28,7 @@
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.recyclerview.widget.OrientationHelper;
import androidx.recyclerview.widget.RecyclerView;
@@ -224,7 +225,7 @@
// Sets the size of the thumb and request a redraw if needed.
ViewGroup.LayoutParams lp = mScrollThumb.getLayoutParams();
- if (lp.height != thumbLength) {
+ if (lp.height != thumbLength || thumbLength < mScrollThumb.getHeight()) {
lp.height = thumbLength;
mScrollThumb.requestLayout();
}
@@ -500,7 +501,33 @@
if ((isAtStart && isAtEnd) || layoutManager == null || layoutManager.getItemCount() == 0) {
mScrollView.setVisibility(View.INVISIBLE);
} else {
- mScrollView.setVisibility(View.VISIBLE);
+ OrientationHelper orientationHelper = getOrientationHelper(layoutManager);
+ int screenSize = orientationHelper.getTotalSpace();
+ int touchTargetSize = (int) getRecyclerView().getContext().getResources()
+ .getDimension(R.dimen.car_ui_touch_target_size);
+ ConstraintLayout.LayoutParams upButtonLayoutParam =
+ (ConstraintLayout.LayoutParams) mUpButton.getLayoutParams();
+ int upButtonMargin = upButtonLayoutParam.topMargin
+ + upButtonLayoutParam.bottomMargin;
+ ConstraintLayout.LayoutParams downButtonLayoutParam =
+ (ConstraintLayout.LayoutParams) mDownButton.getLayoutParams();
+ int downButtonMargin = downButtonLayoutParam.topMargin
+ + downButtonLayoutParam.bottomMargin;
+ int margin = upButtonMargin + downButtonMargin;
+ if (screenSize < 2 * touchTargetSize + margin) {
+ mScrollView.setVisibility(View.INVISIBLE);
+ } else {
+ ConstraintLayout.LayoutParams trackLayoutParam =
+ (ConstraintLayout.LayoutParams) mScrollTrack.getLayoutParams();
+ int trackMargin = trackLayoutParam.topMargin
+ + trackLayoutParam.bottomMargin;
+ margin += trackMargin;
+ if (screenSize < 3 * touchTargetSize + margin) {
+ mScrollTrack.setVisibility(View.INVISIBLE);
+ mScrollThumb.setVisibility(View.INVISIBLE);
+ }
+ mScrollView.setVisibility(View.VISIBLE);
+ }
}
if (layoutManager == null) {
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/CarUiEditText.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/CarUiEditText.java
index 1f1a07c..612a4d8 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/CarUiEditText.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/CarUiEditText.java
@@ -23,6 +23,7 @@
import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.SEARCH_RESULT_ITEM_ID_LIST;
import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.SEARCH_RESULT_SUPPLEMENTAL_ICON_ID_LIST;
import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.WIDE_SCREEN_CLEAR_DATA_ACTION;
+import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.WIDE_SCREEN_POST_LOAD_SEARCH_RESULTS_ACTION;
import android.content.Context;
import android.os.Bundle;
@@ -70,6 +71,12 @@
setText("");
}
+ if (WIDE_SCREEN_POST_LOAD_SEARCH_RESULTS_ACTION.equals(action)) {
+ for (PrivateImeCommandCallback listener : mPrivateImeCommandCallback) {
+ listener.onPostLoadSearchResults();
+ }
+ }
+
if (data == null || mPrivateImeCommandCallback == null) {
return false;
}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/MenuItem.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/MenuItem.java
index 27f8140..5645b0d 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/MenuItem.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/MenuItem.java
@@ -73,6 +73,17 @@
private boolean mIsVisible;
private boolean mIsActivated;
+ @SuppressWarnings("FieldCanBeLocal") // Used with weak references
+ private final CarUxRestrictionsUtil.OnUxRestrictionsChangedListener mUxRestrictionsListener =
+ uxRestrictions -> {
+ boolean wasRestricted = isRestricted();
+ mCurrentRestrictions = uxRestrictions;
+
+ if (isRestricted() != wasRestricted) {
+ update();
+ }
+ };
+
private MenuItem(Builder builder) {
mContext = builder.mContext;
mId = builder.mId;
@@ -92,7 +103,7 @@
mIsPrimary = builder.mIsPrimary;
mUxRestrictions = builder.mUxRestrictions;
- mCurrentRestrictions = CarUxRestrictionsUtil.getInstance(mContext).getCurrentRestrictions();
+ CarUxRestrictionsUtil.getInstance(mContext).register(mUxRestrictionsListener);
}
private void update() {
@@ -239,15 +250,6 @@
update();
}
- /* package */ void setCarUxRestrictions(CarUxRestrictions restrictions) {
- boolean wasRestricted = isRestricted();
- mCurrentRestrictions = restrictions;
-
- if (isRestricted() != wasRestricted) {
- update();
- }
- }
-
/* package */ boolean isRestricted() {
return CarUxRestrictionsUtil.isRestricted(mUxRestrictions, mCurrentRestrictions);
}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/MenuItemRenderer.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/MenuItemRenderer.java
index ca3364b..6d5f572 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/MenuItemRenderer.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/MenuItemRenderer.java
@@ -18,7 +18,6 @@
import static com.android.car.ui.utils.CarUiUtils.requireViewByRefId;
import android.app.Activity;
-import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
@@ -85,10 +84,6 @@
}
}
- void setCarUxRestrictions(CarUxRestrictions restrictions) {
- mMenuItem.setCarUxRestrictions(restrictions);
- }
-
@Override
public void onMenuItemChanged(MenuItem changedItem) {
updateView();
@@ -190,7 +185,7 @@
if (view instanceof ImageView) {
((ImageView) view).setImageState(drawableState, true);
} else if (view instanceof DrawableStateView) {
- ((DrawableStateView) view).setDrawableState(drawableState);
+ ((DrawableStateView) view).setExtraDrawableState(drawableState, null);
}
if (view instanceof ViewGroup) {
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/PrivateImeCommandCallback.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/PrivateImeCommandCallback.java
index 49e7fa4..48583c7 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/PrivateImeCommandCallback.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/PrivateImeCommandCallback.java
@@ -49,4 +49,9 @@
* content area.
*/
void onSurfaceInfo(int displayId, IBinder binder, int height, int width);
+
+ /**
+ * Called when the search results are read and displayed from the content provider by IME.
+ */
+ void onPostLoadSearchResults();
}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/SearchView.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/SearchView.java
index dd6f056..8b0b656 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/SearchView.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/SearchView.java
@@ -86,6 +86,7 @@
private final Handler mHandler = new Handler(Looper.getMainLooper());
private SurfaceControlViewHost mSurfaceControlViewHost;
+ private Uri mContentUri;
private int mSurfaceHeight;
private int mSurfaceWidth;
private List<? extends CarUiImeSearchListItem> mWideScreenSearchItemList;
@@ -278,10 +279,10 @@
private void displaySearchWideScreen() {
String url = CONTENT + getContext().getPackageName() + SEARCH_RESULTS_PROVIDER + "/"
+ SEARCH_RESULTS_TABLE_NAME;
- Uri contentUri = Uri.parse(url);
+ mContentUri = Uri.parse(url);
mIdToListItem.clear();
// clear the table.
- getContext().getContentResolver().delete(contentUri, null, null);
+ getContext().getContentResolver().delete(mContentUri, null, null);
// mWideScreenImeContentAreaView will only be set when running in widescreen mode and
// apps allowed by OEMs are trying to set their own view. In that case we did not want to
@@ -313,7 +314,7 @@
item.getTitle() != null ? item.getTitle().toString() : null);
values.put(SearchResultsProvider.SUBTITLE,
item.getBody() != null ? item.getBody().toString() : null);
- getContext().getContentResolver().insert(contentUri, values);
+ getContext().getContentResolver().insert(mContentUri, values);
mIdToListItem.put(idString, item);
id++;
}
@@ -454,5 +455,12 @@
mInputMethodManager.sendAppPrivateCommand(mSearchText,
WIDE_SCREEN_ACTION, bundle);
}
+
+ @Override
+ public void onPostLoadSearchResults() {
+ if (mContentUri != null) {
+ getContext().getContentResolver().delete(mContentUri, null, null);
+ }
+ }
}
}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/ToolbarControllerImpl.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/ToolbarControllerImpl.java
index 02b3a1a..e30f5f5 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/ToolbarControllerImpl.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/ToolbarControllerImpl.java
@@ -49,7 +49,6 @@
import com.android.car.ui.recyclerview.CarUiListItem;
import com.android.car.ui.recyclerview.CarUiListItemAdapter;
import com.android.car.ui.utils.CarUiUtils;
-import com.android.car.ui.utils.CarUxRestrictionsUtil;
import java.util.ArrayList;
import java.util.Arrays;
@@ -123,15 +122,6 @@
setState(getState());
};
- // Despite the warning, this has to be a field so it's not garbage-collected.
- // The only other reference to it is a weak reference
- private final CarUxRestrictionsUtil.OnUxRestrictionsChangedListener
- mOnUxRestrictionsChangedListener = restrictions -> {
- for (MenuItemRenderer renderer : mMenuItemRenderers) {
- renderer.setCarUxRestrictions(restrictions);
- }
- };
-
public ToolbarControllerImpl(View view) {
mContext = view.getContext();
@@ -197,10 +187,6 @@
setBackgroundShown(true);
mOverflowAdapter = new CarUiListItemAdapter(mUiOverflowItems);
-
- // This holds weak references so we don't need to unregister later
- CarUxRestrictionsUtil.getInstance(getContext())
- .register(mOnUxRestrictionsChangedListener);
}
private Context getContext() {
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/CarUiUtils.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/CarUiUtils.java
index ffa191b..01651fd 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/CarUiUtils.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/CarUiUtils.java
@@ -33,7 +33,6 @@
import android.util.SparseArray;
import android.util.TypedValue;
import android.view.View;
-import android.view.ViewGroup;
import androidx.annotation.DimenRes;
import androidx.annotation.IdRes;
@@ -41,7 +40,6 @@
import androidx.annotation.Nullable;
import androidx.annotation.StyleRes;
import androidx.annotation.UiThread;
-import androidx.core.view.ViewCompat;
import java.lang.reflect.Method;
@@ -106,70 +104,6 @@
}
/**
- * Updates the preference view enabled state. If the view is disabled we just disable the child
- * of preference like TextView, ImageView. The preference itself is always enabled to get the
- * click events. Ripple effect in background is also removed by default. If the ripple is
- * needed see
- * {@link IDisabledPreferenceCallback#setShouldShowRippleOnDisabledPreference(boolean)}
- */
- public static Drawable setPreferenceViewEnabled(boolean viewEnabled, View itemView,
- Drawable background, boolean shouldShowRippleOnDisabledPreference) {
- if (viewEnabled) {
- if (background != null) {
- ViewCompat.setBackground(itemView, background);
- }
- setChildViewsEnabled(itemView, true, false);
- } else {
- itemView.setEnabled(true);
- if (background == null) {
- // store the original background.
- background = itemView.getBackground();
- }
- updateRippleStateOnDisabledPreference(false, shouldShowRippleOnDisabledPreference,
- background, itemView);
- setChildViewsEnabled(itemView, false, true);
- }
- return background;
- }
-
- /**
- * Sets the enabled state on the views of the preference. If the view is being disabled we want
- * only child views of preference to be disabled.
- */
- private static void setChildViewsEnabled(View view, boolean enabled, boolean isRootView) {
- if (!isRootView) {
- view.setEnabled(enabled);
- }
- if (view instanceof ViewGroup) {
- ViewGroup grp = (ViewGroup) view;
- for (int index = 0; index < grp.getChildCount(); index++) {
- setChildViewsEnabled(grp.getChildAt(index), enabled, false);
- }
- }
- }
-
- /**
- * Updates the ripple state on the given preference.
- *
- * @param isEnabled whether the preference is enabled or not
- * @param shouldShowRippleOnDisabledPreference should ripple be displayed when the preference is
- * clicked
- * @param background drawable that represents the ripple
- * @param preference preference on which drawable will be applied
- */
- public static void updateRippleStateOnDisabledPreference(boolean isEnabled,
- boolean shouldShowRippleOnDisabledPreference, Drawable background, View preference) {
- if (isEnabled || preference == null) {
- return;
- }
- if (shouldShowRippleOnDisabledPreference && background != null) {
- ViewCompat.setBackground(preference, background);
- } else {
- ViewCompat.setBackground(preference, null);
- }
- }
-
- /**
* Enables rotary scrolling for {@code view}, either vertically (if {@code isVertical} is true)
* or horizontally (if {@code isVertical} is false). With rotary scrolling enabled, rotating the
* rotary controller will scroll rather than moving the focus when moving the focus would cause
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/DirectManipulationHelper.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/DirectManipulationHelper.java
index 184e522..09d1662 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/DirectManipulationHelper.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/DirectManipulationHelper.java
@@ -26,16 +26,18 @@
import android.view.accessibility.AccessibilityNodeInfo;
import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
/** Helper class to toggle direct manipulation mode. */
public final class DirectManipulationHelper {
/**
- * StateDescription for a {@link View} to support direct manipulation mode. It's also used as
+ * ContentDescription for a {@link View} to support direct manipulation mode. It's also used as
* class name of {@link AccessibilityEvent} to indicate that the AccessibilityEvent represents
* a request to toggle direct manipulation mode.
*/
- private static final String DIRECT_MANIPULATION =
+ @VisibleForTesting
+ public static final String DIRECT_MANIPULATION =
"com.android.car.ui.utils.DIRECT_MANIPULATION";
/** This is a utility class. */
@@ -77,7 +79,7 @@
/** Returns whether the given {@code node} supports rotate directly. */
@TargetApi(R)
public static boolean supportRotateDirectly(@NonNull AccessibilityNodeInfo node) {
- return TextUtils.equals(DIRECT_MANIPULATION, node.getStateDescription());
+ return TextUtils.equals(DIRECT_MANIPULATION, node.getContentDescription());
}
/**
@@ -98,7 +100,7 @@
*/
@TargetApi(R)
public static void setSupportsRotateDirectly(@NonNull View view, boolean enable) {
- view.setStateDescription(enable ? DIRECT_MANIPULATION : null);
+ view.setContentDescription(enable ? DIRECT_MANIPULATION : null);
}
/**
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/ViewUtils.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/ViewUtils.java
index 62c189f..bc5a583 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/ViewUtils.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/ViewUtils.java
@@ -35,6 +35,8 @@
import com.android.car.ui.FocusArea;
import com.android.car.ui.FocusParkingView;
+import com.android.car.ui.R;
+import com.android.car.ui.uxr.DrawableStateView;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -47,31 +49,39 @@
*/
public final class ViewUtils {
+ private static int[] sRestrictedState;
+
/**
* No view is focused, the focused view is not shown, or the focused view is a FocusParkingView.
*/
- public static final int NO_FOCUS = 1;
+ @VisibleForTesting
+ static final int NO_FOCUS = 1;
/** A scrollable container is focused. */
- public static final int SCROLLABLE_CONTAINER_FOCUS = 2;
+ @VisibleForTesting
+ static final int SCROLLABLE_CONTAINER_FOCUS = 2;
/**
* A regular view is focused. A regular View is a View that is neither a FocusParkingView nor a
* scrollable container.
*/
- public static final int REGULAR_FOCUS = 3;
+ @VisibleForTesting
+ static final int REGULAR_FOCUS = 3;
/**
- * An implicit default focus view (i.e., the first focusable item in a scrollable container) is
- * focused.
+ * An implicit default focus view (i.e., the selected item or the first focusable item in a
+ * scrollable container) is focused.
*/
- public static final int IMPLICIT_DEFAULT_FOCUS = 4;
+ @VisibleForTesting
+ static final int IMPLICIT_DEFAULT_FOCUS = 4;
/** The {@code app:defaultFocus} view is focused. */
- public static final int DEFAULT_FOCUS = 5;
+ @VisibleForTesting
+ static final int DEFAULT_FOCUS = 5;
/** The {@code android:focusedByDefault} view is focused. */
- public static final int FOCUSED_BY_DEFAULT = 6;
+ @VisibleForTesting
+ static final int FOCUSED_BY_DEFAULT = 6;
/**
* Focus level of a view. When adjusting the focus, the view with the highest focus level will
@@ -80,7 +90,7 @@
@IntDef(flag = true, value = {NO_FOCUS, SCROLLABLE_CONTAINER_FOCUS, REGULAR_FOCUS,
IMPLICIT_DEFAULT_FOCUS, DEFAULT_FOCUS, FOCUSED_BY_DEFAULT})
@Retention(RetentionPolicy.SOURCE)
- public @interface FocusLevel {
+ private @interface FocusLevel {
}
/** This is a utility class. */
@@ -158,8 +168,24 @@
* @return whether the view is focused
*/
public static boolean adjustFocus(@NonNull View root, @Nullable View currentFocus) {
- @FocusLevel int level = getFocusLevel(currentFocus);
- return adjustFocus(root, level);
+ @FocusLevel int currentLevel = getFocusLevel(currentFocus);
+ return adjustFocus(root, currentLevel, /* cachedFocusedView= */ null,
+ /* defaultFocusOverridesHistory= */ false);
+ }
+
+ /**
+ * If the {@code currentFocus}'s FocusLevel is lower than REGULAR_FOCUS, adjusts focus within
+ * {@code root}. See {@link #adjustFocus(View, int)}. Otherwise no-op.
+ *
+ * @return whether the focus has changed
+ */
+ public static boolean initFocus(@NonNull View root, @Nullable View currentFocus) {
+ @FocusLevel int currentLevel = getFocusLevel(currentFocus);
+ if (currentLevel >= REGULAR_FOCUS) {
+ return false;
+ }
+ return adjustFocus(root, currentLevel, /* cachedFocusedView= */ null,
+ /* defaultFocusOverridesHistory= */ false);
}
/**
@@ -168,7 +194,42 @@
*
* @return whether the view is focused
*/
- public static boolean adjustFocus(@NonNull View root, @FocusLevel int currentLevel) {
+ @VisibleForTesting
+ static boolean adjustFocus(@NonNull View root, @FocusLevel int currentLevel) {
+ return adjustFocus(root, currentLevel, /* cachedFocusedView= */ null,
+ /* defaultFocusOverridesHistory= */ false);
+ }
+
+ /**
+ * Searches the {@code root}'s descendants for a view with the highest {@link FocusLevel} and
+ * focuses on it or the {@code cachedFocusedView}.
+ *
+ * @return whether the view is focused
+ */
+ public static boolean adjustFocus(@NonNull View root,
+ @Nullable View cachedFocusedView,
+ boolean defaultFocusOverridesHistory) {
+ return adjustFocus(root, NO_FOCUS, cachedFocusedView, defaultFocusOverridesHistory);
+ }
+
+ /**
+ * Searches the {@code root}'s descendants for a view with the highest {@link FocusLevel}. If
+ * the view's FocusLevel is higher than {@code currentLevel}, focuses on the view or {@code
+ * cachedFocusedView}.
+ *
+ * @return whether the view is focused
+ */
+ private static boolean adjustFocus(@NonNull View root,
+ @FocusLevel int currentLevel,
+ @Nullable View cachedFocusedView,
+ boolean defaultFocusOverridesHistory) {
+ // If the previously focused view has higher priority than the default focus, try to focus
+ // on the previously focused view.
+ if (!defaultFocusOverridesHistory && requestFocus(cachedFocusedView)) {
+ return true;
+ }
+
+ // Try to focus on the default focus view.
if (currentLevel < FOCUSED_BY_DEFAULT && focusOnFocusedByDefaultView(root)) {
return true;
}
@@ -178,6 +239,14 @@
if (currentLevel < IMPLICIT_DEFAULT_FOCUS && focusOnImplicitDefaultFocusView(root)) {
return true;
}
+
+ // If the previously focused view has lower priority than the default focus, try to focus
+ // on the previously focused view.
+ if (defaultFocusOverridesHistory && requestFocus(cachedFocusedView)) {
+ return true;
+ }
+
+ // Try to focus on other views with low focus levels.
if (currentLevel < REGULAR_FOCUS && focusOnFirstRegularView(root)) {
return true;
}
@@ -215,8 +284,8 @@
}
/**
- * Returns whether the {@code view} is an implicit default focus view, i.e., the first focusable
- * item in a rotary container.
+ * Returns whether the {@code view} is an implicit default focus view, i.e., the selected
+ * item or the first focusable item in a rotary container.
*/
@VisibleForTesting
static boolean isImplicitDefaultFocusView(@NonNull View view) {
@@ -233,7 +302,8 @@
if (rotaryContainer == null) {
return false;
}
- return findFirstFocusableDescendant(rotaryContainer) == view;
+ return findFirstSelectedFocusableDescendant(rotaryContainer) == view
+ || findFirstFocusableDescendant(rotaryContainer) == view;
}
private static boolean isRotaryContainer(@NonNull View view) {
@@ -358,15 +428,21 @@
/**
* Searches the {@code view} and its descendants in depth first order, and returns the first
- * implicit default focus view, i.e., the first focusable item in the first rotary container.
- * Returns null if not found.
+ * implicit default focus view, i.e., the selected item or the first focusable item in the
+ * first rotary container. Returns null if not found.
*/
@VisibleForTesting
@Nullable
static View findImplicitDefaultFocusView(@NonNull View view) {
View rotaryContainer = findRotaryContainer(view);
- return rotaryContainer == null
- ? null
+ if (rotaryContainer == null) {
+ return null;
+ }
+
+ View selectedItem = findFirstSelectedFocusableDescendant(rotaryContainer);
+
+ return selectedItem != null
+ ? selectedItem
: findFirstFocusableDescendant(rotaryContainer);
}
@@ -383,6 +459,18 @@
}
/**
+ * Searches the {@code view}'s descendants in depth first order, and returns the first view
+ * that is selected and can take focus, or null if not found.
+ */
+ @VisibleForTesting
+ @Nullable
+ static View findFirstSelectedFocusableDescendant(@NonNull View view) {
+ return depthFirstSearch(view,
+ /* targetPredicate= */ v -> v != view && v.isSelected() && canTakeFocus(v),
+ /* skipPredicate= */ v -> !v.isShown());
+ }
+
+ /**
* Searches the {@code view} and its descendants in depth first order, and returns the first
* rotary container shown on the screen. Returns null if not found.
*/
@@ -431,4 +519,39 @@
// descendants. We focus on it so that the rotary controller can scroll it.
&& (!isScrollableContainer(view) || findFirstFocusableDescendant(view) == null);
}
+
+ /**
+ * Traverses the view hierarchy, and whenever it sees a {@link DrawableStateView}, adds
+ * state_ux_restricted to it.
+ *
+ * Note that this will remove any other drawable states added by other calls to
+ * {@link DrawableStateView#setExtraDrawableState(int[], int[])}
+ */
+ public static void makeAllViewsUxRestricted(@Nullable View view, boolean restricted) {
+ if (view instanceof DrawableStateView) {
+ if (sRestrictedState == null) {
+ int androidStateUxRestricted = view.getResources()
+ .getIdentifier("state_ux_restricted", "attr", "android");
+
+ if (androidStateUxRestricted == 0) {
+ sRestrictedState = new int[] { R.attr.state_ux_restricted };
+ } else {
+ sRestrictedState = new int[] {
+ R.attr.state_ux_restricted,
+ androidStateUxRestricted
+ };
+ }
+ }
+
+ ((DrawableStateView) view).setExtraDrawableState(
+ restricted ? sRestrictedState : null, null);
+ }
+
+ if (view instanceof ViewGroup) {
+ ViewGroup vg = (ViewGroup) view;
+ for (int i = 0; i < vg.getChildCount(); i++) {
+ makeAllViewsUxRestricted(vg.getChildAt(i), restricted);
+ }
+ }
+ }
}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateButton.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateButton.java
index 9e5e1ce..bf7fcb1 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateButton.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateButton.java
@@ -26,8 +26,7 @@
* such as ux restriction.
*/
public class DrawableStateButton extends Button implements DrawableStateView {
-
- private int[] mState;
+ private DrawableStateUtil mUtil;
public DrawableStateButton(Context context) {
super(context);
@@ -41,24 +40,19 @@
super(context, attrs, defStyleAttr);
}
- public DrawableStateButton(
- Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- }
-
@Override
- public void setDrawableState(int[] state) {
- mState = state;
- refreshDrawableState();
+ public void setExtraDrawableState(@Nullable int[] stateToAdd, @Nullable int[] stateToRemove) {
+ if (mUtil == null) {
+ mUtil = new DrawableStateUtil(this);
+ }
+ mUtil.setExtraDrawableState(stateToAdd, stateToRemove);
}
@Override
public int[] onCreateDrawableState(int extraSpace) {
- if (mState == null) {
- return super.onCreateDrawableState(extraSpace);
- } else {
- return mergeDrawableStates(
- super.onCreateDrawableState(extraSpace + mState.length), mState);
+ if (mUtil == null) {
+ mUtil = new DrawableStateUtil(this);
}
+ return mUtil.onCreateDrawableState(extraSpace, space -> super.onCreateDrawableState(space));
}
}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateConstraintLayout.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateConstraintLayout.java
new file mode 100644
index 0000000..647bfa4
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateConstraintLayout.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.ui.uxr;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import androidx.annotation.Nullable;
+import androidx.constraintlayout.widget.ConstraintLayout;
+
+/**
+ * A {@link ConstraintLayout} that implements {@link DrawableStateView}, for allowing additional
+ * states such as ux restriction.
+ */
+public class DrawableStateConstraintLayout extends ConstraintLayout implements DrawableStateView {
+ private DrawableStateUtil mUtil;
+
+ public DrawableStateConstraintLayout(Context context) {
+ super(context);
+ }
+
+ public DrawableStateConstraintLayout(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public DrawableStateConstraintLayout(Context context,
+ @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ public void setExtraDrawableState(@Nullable int[] stateToAdd, @Nullable int[] stateToRemove) {
+ if (mUtil == null) {
+ mUtil = new DrawableStateUtil(this);
+ }
+ mUtil.setExtraDrawableState(stateToAdd, stateToRemove);
+ }
+
+ @Override
+ public int[] onCreateDrawableState(int extraSpace) {
+ if (mUtil == null) {
+ mUtil = new DrawableStateUtil(this);
+ }
+ return mUtil.onCreateDrawableState(extraSpace, space -> super.onCreateDrawableState(space));
+ }
+}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateFrameLayout.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateFrameLayout.java
new file mode 100644
index 0000000..f743043
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateFrameLayout.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.ui.uxr;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+import androidx.annotation.Nullable;
+
+/**
+ * A {@link FrameLayout} that implements {@link DrawableStateView}, for allowing additional
+ * states such as ux restriction.
+ */
+public class DrawableStateFrameLayout extends FrameLayout implements DrawableStateView {
+ private DrawableStateUtil mUtil;
+
+ public DrawableStateFrameLayout(Context context) {
+ super(context);
+ }
+
+ public DrawableStateFrameLayout(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public DrawableStateFrameLayout(Context context,
+ @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ public void setExtraDrawableState(@Nullable int[] stateToAdd, @Nullable int[] stateToRemove) {
+ if (mUtil == null) {
+ mUtil = new DrawableStateUtil(this);
+ }
+ mUtil.setExtraDrawableState(stateToAdd, stateToRemove);
+ }
+
+ @Override
+ public int[] onCreateDrawableState(int extraSpace) {
+ if (mUtil == null) {
+ mUtil = new DrawableStateUtil(this);
+ }
+ return mUtil.onCreateDrawableState(extraSpace, space -> super.onCreateDrawableState(space));
+ }
+}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateImageView.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateImageView.java
new file mode 100644
index 0000000..511da66
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateImageView.java
@@ -0,0 +1,58 @@
+/*
+ * 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.ui.uxr;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+import androidx.annotation.Nullable;
+
+/**
+ * A {@link ImageView} that implements {@link DrawableStateView}, for allowing additional states
+ * such as ux restriction.
+ */
+public class DrawableStateImageView extends ImageView implements DrawableStateView {
+ private DrawableStateUtil mUtil;
+
+ public DrawableStateImageView(Context context) {
+ super(context);
+ }
+
+ public DrawableStateImageView(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public DrawableStateImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ public void setExtraDrawableState(@Nullable int[] stateToAdd, @Nullable int[] stateToRemove) {
+ if (mUtil == null) {
+ mUtil = new DrawableStateUtil(this);
+ }
+ mUtil.setExtraDrawableState(stateToAdd, stateToRemove);
+ }
+
+ @Override
+ public int[] onCreateDrawableState(int extraSpace) {
+ if (mUtil == null) {
+ mUtil = new DrawableStateUtil(this);
+ }
+ return mUtil.onCreateDrawableState(extraSpace, space -> super.onCreateDrawableState(space));
+ }
+}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateLinearLayout.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateLinearLayout.java
new file mode 100644
index 0000000..689a32f
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateLinearLayout.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.ui.uxr;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.LinearLayout;
+
+import androidx.annotation.Nullable;
+
+/**
+ * A {@link LinearLayout} that implements {@link DrawableStateView}, for allowing additional
+ * states such as ux restriction.
+ */
+public class DrawableStateLinearLayout extends LinearLayout implements DrawableStateView {
+ private DrawableStateUtil mUtil;
+
+ public DrawableStateLinearLayout(Context context) {
+ super(context);
+ }
+
+ public DrawableStateLinearLayout(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public DrawableStateLinearLayout(Context context, @Nullable AttributeSet attrs,
+ int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ public void setExtraDrawableState(@Nullable int[] stateToAdd, @Nullable int[] stateToRemove) {
+ if (mUtil == null) {
+ mUtil = new DrawableStateUtil(this);
+ }
+ mUtil.setExtraDrawableState(stateToAdd, stateToRemove);
+ }
+
+ @Override
+ public int[] onCreateDrawableState(int extraSpace) {
+ if (mUtil == null) {
+ mUtil = new DrawableStateUtil(this);
+ }
+ return mUtil.onCreateDrawableState(extraSpace, space -> super.onCreateDrawableState(space));
+ }
+}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateRelativeLayout.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateRelativeLayout.java
new file mode 100644
index 0000000..1c69300
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateRelativeLayout.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.ui.uxr;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.RelativeLayout;
+
+import androidx.annotation.Nullable;
+
+/**
+ * A {@link RelativeLayout} that implements {@link DrawableStateView}, for allowing additional
+ * states such as ux restriction.
+ */
+public class DrawableStateRelativeLayout extends RelativeLayout implements DrawableStateView {
+ private DrawableStateUtil mUtil;
+
+ public DrawableStateRelativeLayout(Context context) {
+ super(context);
+ }
+
+ public DrawableStateRelativeLayout(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public DrawableStateRelativeLayout(Context context,
+ @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ public void setExtraDrawableState(@Nullable int[] stateToAdd, @Nullable int[] stateToRemove) {
+ if (mUtil == null) {
+ mUtil = new DrawableStateUtil(this);
+ }
+ mUtil.setExtraDrawableState(stateToAdd, stateToRemove);
+ }
+
+ @Override
+ public int[] onCreateDrawableState(int extraSpace) {
+ if (mUtil == null) {
+ mUtil = new DrawableStateUtil(this);
+ }
+ return mUtil.onCreateDrawableState(extraSpace, space -> super.onCreateDrawableState(space));
+ }
+}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateSwitch.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateSwitch.java
index bfa018c..c047592 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateSwitch.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateSwitch.java
@@ -26,7 +26,7 @@
* such as ux restriction.
*/
public class DrawableStateSwitch extends Switch implements DrawableStateView {
- private int[] mState;
+ private DrawableStateUtil mUtil;
public DrawableStateSwitch(Context context) {
super(context);
@@ -46,18 +46,18 @@
}
@Override
- public void setDrawableState(int[] state) {
- mState = state;
- refreshDrawableState();
+ public void setExtraDrawableState(@Nullable int[] stateToAdd, @Nullable int[] stateToRemove) {
+ if (mUtil == null) {
+ mUtil = new DrawableStateUtil(this);
+ }
+ mUtil.setExtraDrawableState(stateToAdd, stateToRemove);
}
@Override
public int[] onCreateDrawableState(int extraSpace) {
- if (mState == null) {
- return super.onCreateDrawableState(extraSpace);
- } else {
- return mergeDrawableStates(
- super.onCreateDrawableState(extraSpace + mState.length), mState);
+ if (mUtil == null) {
+ mUtil = new DrawableStateUtil(this);
}
+ return mUtil.onCreateDrawableState(extraSpace, space -> super.onCreateDrawableState(space));
}
}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateTextView.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateTextView.java
new file mode 100644
index 0000000..42324fe
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateTextView.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.ui.uxr;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+
+/**
+ * A {@link TextView} that implements {@link DrawableStateView}, for allowing additional
+ * states such as ux restriction.
+ */
+public class DrawableStateTextView extends TextView implements DrawableStateView {
+ private DrawableStateUtil mUtil;
+
+ public DrawableStateTextView(Context context) {
+ super(context);
+ }
+
+ public DrawableStateTextView(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public DrawableStateTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ public void setExtraDrawableState(@Nullable int[] stateToAdd, @Nullable int[] stateToRemove) {
+ if (mUtil == null) {
+ mUtil = new DrawableStateUtil(this);
+ }
+ mUtil.setExtraDrawableState(stateToAdd, stateToRemove);
+ }
+
+ @Override
+ public int[] onCreateDrawableState(int extraSpace) {
+ if (mUtil == null) {
+ mUtil = new DrawableStateUtil(this);
+ }
+ return mUtil.onCreateDrawableState(extraSpace, space -> super.onCreateDrawableState(space));
+ }
+}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateUtil.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateUtil.java
new file mode 100644
index 0000000..12f05f1
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateUtil.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.ui.uxr;
+
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+import java.util.Arrays;
+import java.util.function.Function;
+
+/**
+ * This is a utility class designed to make it easier to create a new {@link DrawableStateView}.
+ *
+ * To use, subclass a view and forward it's {@link View#onCreateDrawableState(int)} and
+ * {@link DrawableStateView#setExtraDrawableState(int[], int[])} methods to this object.
+ */
+class DrawableStateUtil implements DrawableStateView {
+
+ private final View mView;
+ @Nullable
+ private int[] mStateToAdd;
+ @Nullable
+ private int[] mStateToRemove;
+
+ DrawableStateUtil(View view) {
+ mView = view;
+ }
+
+ /**
+ * Forward the View's onCreateDrawableState to this method.
+ *
+ * @param extraSpace The extraSpace parameter passed to the View's onCreateDrawableState
+ * @param callSuper A reference to the View's super.onCreateDrawableState()
+ * @return The intended result of the View's onCreateDrawableState()
+ */
+ public int[] onCreateDrawableState(int extraSpace, Function<Integer, int[]> callSuper) {
+ int[] result;
+ if (mStateToAdd == null) {
+ result = callSuper.apply(extraSpace);
+ } else {
+ result = mergeDrawableStates(
+ callSuper.apply(extraSpace + mStateToAdd.length), mStateToAdd);
+ }
+
+ if (mStateToRemove != null && mStateToRemove.length != 0) {
+ result = Arrays.stream(result)
+ .filter(state -> Arrays.stream(mStateToRemove).noneMatch(
+ toRemove -> state == toRemove))
+ .toArray();
+ }
+
+ return result;
+ }
+
+ /**
+ * Forward your View's setExtraDrawableState here.
+ */
+ @Override
+ public void setExtraDrawableState(@Nullable int[] stateToAdd, @Nullable int[] stateToRemove) {
+ mStateToAdd = stateToAdd;
+ mStateToRemove = stateToRemove;
+ mView.refreshDrawableState();
+ }
+
+ /** Copied from {@link View} */
+ private static int[] mergeDrawableStates(int[] baseState, int[] additionalState) {
+ int i = baseState.length - 1;
+ while (i >= 0 && baseState[i] == 0) {
+ i--;
+ }
+ System.arraycopy(additionalState, 0, baseState, i + 1, additionalState.length);
+ return baseState;
+ }
+}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateView.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateView.java
index a9cdb52..87aa8a9 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateView.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateView.java
@@ -15,14 +15,22 @@
*/
package com.android.car.ui.uxr;
+import androidx.annotation.Nullable;
+
/**
- * An Interface to expose a view's drawable state.
+ * An Interface to manipulate a view's drawable state.
*
- * <p>Used by {@link com.android.car.ui.toolbar.Toolbar Toolbar's}
- * {@link com.android.car.ui.toolbar.MenuItem MenuItems} to make the views display if they are ux
- * restricted.
+ * <p>Used by {@link com.android.car.ui.toolbar.MenuItem MenuItems} to make the views display
+ * if they are ux restricted.
*/
public interface DrawableStateView {
- /** Sets the drawable state. This should merge with existing drawable states */
- void setDrawableState(int[] state);
+ /**
+ * Sets the drawable state. This should merge with existing drawable states
+ *
+ * @param stateToAdd An array of drawable states to add to the view's drawable state, along
+ * with any drawable state that would normally be there.
+ * @param stateToRemove An array of drawable states to remove from what would normally be
+ * used to display the view.
+ */
+ void setExtraDrawableState(@Nullable int[] stateToAdd, @Nullable int[] stateToRemove);
}
diff --git a/car-ui-lib/car-ui-lib/src/main/res-overlayable/values/overlayable.xml b/car-ui-lib/car-ui-lib/src/main/res-overlayable/values/overlayable.xml
index b6f00f0..af550e0 100644
--- a/car-ui-lib/car-ui-lib/src/main/res-overlayable/values/overlayable.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res-overlayable/values/overlayable.xml
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='UTF-8'?>
-<!--Copyright (C) 2020 The Android Open Source Project
+<!--Copyright (C) 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -80,6 +80,7 @@
<item type="bool" name="car_ui_ime_wide_screen_aligned_left"/>
<item type="bool" name="car_ui_ime_wide_screen_allow_app_hide_content_area"/>
<item type="bool" name="car_ui_list_item_single_line_title"/>
+ <item type="bool" name="car_ui_preference_list_instant_change_callback"/>
<item type="bool" name="car_ui_preference_list_show_full_screen"/>
<item type="bool" name="car_ui_preference_show_chevron"/>
<item type="bool" name="car_ui_scrollbar_enable"/>
@@ -147,6 +148,7 @@
<item type="dimen" name="car_ui_ime_wide_screen_error_text_size"/>
<item type="dimen" name="car_ui_ime_wide_screen_input_area_height"/>
<item type="dimen" name="car_ui_ime_wide_screen_input_area_margin_top"/>
+ <item type="dimen" name="car_ui_ime_wide_screen_input_area_padding_end"/>
<item type="dimen" name="car_ui_ime_wide_screen_input_edit_text_padding_left"/>
<item type="dimen" name="car_ui_ime_wide_screen_input_edit_text_padding_right"/>
<item type="dimen" name="car_ui_ime_wide_screen_input_edit_text_size"/>
@@ -309,26 +311,39 @@
<item type="drawable" name="car_ui_toolbar_menu_item_icon_ripple"/>
<item type="drawable" name="car_ui_toolbar_search_close_icon"/>
<item type="drawable" name="car_ui_toolbar_search_search_icon"/>
- <item type="id" name="action_container"/>
- <item type="id" name="action_container_touch_interceptor"/>
- <item type="id" name="action_divider"/>
<item type="id" name="action_widget_container"/>
- <item type="id" name="avatar_icon"/>
- <item type="id" name="body"/>
<item type="id" name="car_ui_alert_icon"/>
<item type="id" name="car_ui_alert_subtitle"/>
<item type="id" name="car_ui_alert_title"/>
<item type="id" name="car_ui_base_layout_content_container"/>
<item type="id" name="car_ui_closeKeyboard"/>
<item type="id" name="car_ui_contentAreaAutomotive"/>
+ <item type="id" name="car_ui_divider"/>
+ <item type="id" name="car_ui_first_action_container"/>
<item type="id" name="car_ui_focus_area"/>
<item type="id" name="car_ui_fullscreenArea"/>
<item type="id" name="car_ui_imeWideScreenInputArea"/>
<item type="id" name="car_ui_ime_surface"/>
<item type="id" name="car_ui_inputExtractActionAutomotive"/>
<item type="id" name="car_ui_inputExtractEditTextContainer"/>
+ <item type="id" name="car_ui_list_item_action_container"/>
+ <item type="id" name="car_ui_list_item_action_container_touch_interceptor"/>
+ <item type="id" name="car_ui_list_item_action_divider"/>
+ <item type="id" name="car_ui_list_item_avatar_icon"/>
+ <item type="id" name="car_ui_list_item_body"/>
+ <item type="id" name="car_ui_list_item_checkbox_widget"/>
+ <item type="id" name="car_ui_list_item_content_icon"/>
<item type="id" name="car_ui_list_item_end_guideline"/>
+ <item type="id" name="car_ui_list_item_icon"/>
+ <item type="id" name="car_ui_list_item_icon_container"/>
+ <item type="id" name="car_ui_list_item_radio_button_widget"/>
+ <item type="id" name="car_ui_list_item_reduced_touch_interceptor"/>
<item type="id" name="car_ui_list_item_start_guideline"/>
+ <item type="id" name="car_ui_list_item_supplemental_icon"/>
+ <item type="id" name="car_ui_list_item_switch_widget"/>
+ <item type="id" name="car_ui_list_item_text_container"/>
+ <item type="id" name="car_ui_list_item_title"/>
+ <item type="id" name="car_ui_list_item_touch_interceptor"/>
<item type="id" name="car_ui_list_limiting_message"/>
<item type="id" name="car_ui_preference_container_without_widget"/>
<item type="id" name="car_ui_preference_fragment_container"/>
@@ -338,6 +353,9 @@
<item type="id" name="car_ui_scrollbar_page_up"/>
<item type="id" name="car_ui_scrollbar_thumb"/>
<item type="id" name="car_ui_scrollbar_track"/>
+ <item type="id" name="car_ui_second_action_container"/>
+ <item type="id" name="car_ui_secondary_action"/>
+ <item type="id" name="car_ui_secondary_action_concrete"/>
<item type="id" name="car_ui_toolbar"/>
<item type="id" name="car_ui_toolbar_background"/>
<item type="id" name="car_ui_toolbar_bottom_guideline"/>
@@ -377,17 +395,11 @@
<item type="id" name="car_ui_wideScreenExtractedTextIcon"/>
<item type="id" name="car_ui_wideScreenInputArea"/>
<item type="id" name="car_ui_wideScreenSearchResultList"/>
- <item type="id" name="checkbox_widget"/>
<item type="id" name="container"/>
- <item type="id" name="content_icon"/>
- <item type="id" name="icon"/>
- <item type="id" name="icon_container"/>
<item type="id" name="list"/>
<item type="id" name="nested_recycler_view_layout"/>
<item type="id" name="radio_button"/>
- <item type="id" name="radio_button_widget"/>
<item type="id" name="recycler_view"/>
- <item type="id" name="reduced_touch_interceptor"/>
<item type="id" name="search"/>
<item type="id" name="seek_bar"/>
<item type="id" name="seek_bar_text_left"/>
@@ -396,14 +408,9 @@
<item type="id" name="seekbar"/>
<item type="id" name="seekbar_value"/>
<item type="id" name="spinner"/>
- <item type="id" name="supplemental_icon"/>
- <item type="id" name="switch_widget"/>
- <item type="id" name="text_container"/>
<item type="id" name="textbox"/>
- <item type="id" name="title"/>
<item type="id" name="title_template"/>
<item type="id" name="toolbar"/>
- <item type="id" name="touch_interceptor"/>
<item type="integer" name="car_ui_default_max_string_length"/>
<item type="integer" name="car_ui_focus_area_history_cache_type"/>
<item type="integer" name="car_ui_focus_area_history_expiration_period_ms"/>
@@ -430,6 +437,10 @@
<item type="layout" name="car_ui_preference_dropdown"/>
<item type="layout" name="car_ui_preference_fragment"/>
<item type="layout" name="car_ui_preference_fragment_with_toolbar"/>
+ <item type="layout" name="car_ui_preference_two_action_icon"/>
+ <item type="layout" name="car_ui_preference_two_action_switch"/>
+ <item type="layout" name="car_ui_preference_two_action_text"/>
+ <item type="layout" name="car_ui_preference_two_action_text_borderless"/>
<item type="layout" name="car_ui_preference_widget_checkbox"/>
<item type="layout" name="car_ui_preference_widget_seekbar"/>
<item type="layout" name="car_ui_preference_widget_switch"/>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_header_list_item.xml b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_header_list_item.xml
index 7ac4732..38d1c38 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_header_list_item.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_header_list_item.xml
@@ -32,7 +32,7 @@
<!-- Use nested layout as a workaround for a regression in constraintlayout where chains not
are not rendered correctly when the tail is gone (b/168627311).-->
<LinearLayout
- android:id="@+id/text_container"
+ android:id="@+id/car_ui_list_item_text_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
@@ -42,7 +42,7 @@
android:orientation="vertical">
<TextView
- android:id="@+id/title"
+ android:id="@+id/car_ui_list_item_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/car_ui_header_list_item_text_start_margin"
@@ -50,7 +50,7 @@
android:textAppearance="@style/TextAppearance.CarUi.ListItem.Header" />
<TextView
- android:id="@+id/body"
+ android:id="@+id/car_ui_list_item_body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/car_ui_list_item_text_no_icon_start_margin"
diff --git a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_ims_wide_screen_input_view.xml b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_ims_wide_screen_input_view.xml
index fd7539b..2d68cd8 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_ims_wide_screen_input_view.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_ims_wide_screen_input_view.xml
@@ -62,6 +62,7 @@
android:scrollbars="vertical"
android:gravity="left|center"
android:backgroundTint="@drawable/car_ui_ime_wide_screen_input_area_tint_color"
+ android:paddingEnd="@dimen/car_ui_ime_wide_screen_input_area_padding_end"
android:minLines="1"
android:inputType="text"/>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_list_item.xml b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_list_item.xml
index d0f179e..55877e9 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_list_item.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_list_item.xml
@@ -26,7 +26,7 @@
<!-- The following touch interceptor views are sized to encompass the specific sub-sections of
the list item view to easily control the bounds of a background ripple effects. -->
<View
- android:id="@+id/touch_interceptor"
+ android:id="@+id/car_ui_list_item_touch_interceptor"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/car_ui_list_item_background"
@@ -38,14 +38,14 @@
<!-- This touch interceptor does not include the action container -->
<View
- android:id="@+id/reduced_touch_interceptor"
+ android:id="@+id/car_ui_list_item_reduced_touch_interceptor"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/car_ui_list_item_background"
android:clickable="true"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toStartOf="@id/action_container"
+ app:layout_constraintEnd_toStartOf="@id/car_ui_list_item_action_container"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
@@ -57,7 +57,7 @@
app:layout_constraintGuide_begin="@dimen/car_ui_list_item_start_inset" />
<FrameLayout
- android:id="@+id/icon_container"
+ android:id="@+id/car_ui_list_item_icon_container"
android:layout_width="@dimen/car_ui_list_item_icon_container_width"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
@@ -65,7 +65,7 @@
app:layout_constraintTop_toTopOf="parent">
<ImageView
- android:id="@+id/icon"
+ android:id="@+id/car_ui_list_item_icon"
android:layout_width="@dimen/car_ui_list_item_icon_size"
android:layout_height="@dimen/car_ui_list_item_icon_size"
android:layout_gravity="center"
@@ -73,7 +73,7 @@
android:scaleType="fitXY" />
<ImageView
- android:id="@+id/content_icon"
+ android:id="@+id/car_ui_list_item_content_icon"
android:layout_width="@dimen/car_ui_list_item_content_icon_width"
android:layout_height="@dimen/car_ui_list_item_content_icon_height"
android:layout_gravity="center"
@@ -81,7 +81,7 @@
android:scaleType="fitXY" />
<ImageView
- android:id="@+id/avatar_icon"
+ android:id="@+id/car_ui_list_item_avatar_icon"
android:background="@drawable/car_ui_list_item_avatar_icon_outline"
android:layout_width="@dimen/car_ui_list_item_avatar_icon_width"
android:layout_height="@dimen/car_ui_list_item_avatar_icon_height"
@@ -93,19 +93,19 @@
<!-- Use nested layout as a workaround for a regression in constraintlayout where chains not
are not rendered correctly when the tail is gone (b/168627311).-->
<LinearLayout
- android:id="@+id/text_container"
+ android:id="@+id/car_ui_list_item_text_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toStartOf="@id/action_container"
- app:layout_constraintStart_toEndOf="@id/icon_container"
+ app:layout_constraintEnd_toStartOf="@id/car_ui_list_item_action_container"
+ app:layout_constraintStart_toEndOf="@id/car_ui_list_item_icon_container"
android:layout_marginStart="@dimen/car_ui_list_item_text_start_margin"
app:layout_goneMarginStart="@dimen/car_ui_list_item_text_no_icon_start_margin"
android:orientation="vertical">
<TextView
- android:id="@+id/title"
+ android:id="@+id/car_ui_list_item_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="@bool/car_ui_list_item_single_line_title"
@@ -113,7 +113,7 @@
android:textAppearance="@style/TextAppearance.CarUi.ListItem" />
<TextView
- android:id="@+id/body"
+ android:id="@+id/car_ui_list_item_body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textDirection="locale"
@@ -122,19 +122,19 @@
<!-- This touch interceptor is sized and positioned to encompass the action container -->
<View
- android:id="@+id/action_container_touch_interceptor"
+ android:id="@+id/car_ui_list_item_action_container_touch_interceptor"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/car_ui_list_item_background"
android:clickable="true"
android:visibility="gone"
- app:layout_constraintBottom_toBottomOf="@id/action_container"
- app:layout_constraintEnd_toEndOf="@id/action_container"
- app:layout_constraintStart_toStartOf="@id/action_container"
- app:layout_constraintTop_toTopOf="@id/action_container" />
+ app:layout_constraintBottom_toBottomOf="@id/car_ui_list_item_action_container"
+ app:layout_constraintEnd_toEndOf="@id/car_ui_list_item_action_container"
+ app:layout_constraintStart_toStartOf="@id/car_ui_list_item_action_container"
+ app:layout_constraintTop_toTopOf="@id/car_ui_list_item_action_container" />
<FrameLayout
- android:id="@+id/action_container"
+ android:id="@+id/car_ui_list_item_action_container"
android:layout_width="wrap_content"
android:minWidth="@dimen/car_ui_list_item_icon_container_width"
android:layout_height="0dp"
@@ -143,14 +143,14 @@
app:layout_constraintTop_toTopOf="parent">
<View
- android:id="@+id/action_divider"
+ android:id="@+id/car_ui_list_item_action_divider"
android:layout_width="@dimen/car_ui_list_item_action_divider_width"
android:layout_height="@dimen/car_ui_list_item_action_divider_height"
android:layout_gravity="start|center_vertical"
android:background="@drawable/car_ui_list_item_divider" />
<Switch
- android:id="@+id/switch_widget"
+ android:id="@+id/car_ui_list_item_switch_widget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
@@ -158,7 +158,7 @@
android:focusable="false" />
<CheckBox
- android:id="@+id/checkbox_widget"
+ android:id="@+id/car_ui_list_item_checkbox_widget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
@@ -166,7 +166,7 @@
android:focusable="false" />
<RadioButton
- android:id="@+id/radio_button_widget"
+ android:id="@+id/car_ui_list_item_radio_button_widget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
@@ -174,7 +174,7 @@
android:focusable="false" />
<ImageView
- android:id="@+id/supplemental_icon"
+ android:id="@+id/car_ui_list_item_supplemental_icon"
android:layout_width="@dimen/car_ui_list_item_supplemental_icon_size"
android:layout_height="@dimen/car_ui_list_item_supplemental_icon_size"
android:layout_gravity="center"
diff --git a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_preference.xml b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_preference.xml
index 52dbd68..ef60b83 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_preference.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_preference.xml
@@ -15,7 +15,7 @@
limitations under the License.
-->
-<RelativeLayout
+<com.android.car.ui.uxr.DrawableStateRelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -26,7 +26,7 @@
android:tag="carUiPreference"
android:paddingStart="?android:attr/listPreferredItemPaddingStart">
- <ImageView
+ <com.android.car.ui.uxr.DrawableStateImageView
android:id="@android:id/icon"
android:layout_width="@dimen/car_ui_preference_icon_size"
android:layout_height="@dimen/car_ui_preference_icon_size"
@@ -48,14 +48,14 @@
android:layout_toStartOf="@android:id/widget_frame"
android:orientation="vertical">
- <TextView
+ <com.android.car.ui.uxr.DrawableStateTextView
android:id="@android:id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.CarUi.PreferenceTitle"/>
- <TextView
+ <com.android.car.ui.uxr.DrawableStateTextView
android:id="@android:id/summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -71,4 +71,4 @@
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"/>
-</RelativeLayout>
+</com.android.car.ui.uxr.DrawableStateRelativeLayout>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_preference_two_action_icon.xml b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_preference_two_action_icon.xml
new file mode 100644
index 0000000..0d0f202
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_preference_two_action_icon.xml
@@ -0,0 +1,128 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 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.
+-->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall"
+ android:tag="carUiPreference">
+
+ <com.android.car.ui.uxr.DrawableStateConstraintLayout
+ android:id="@+id/car_ui_first_action_container"
+ android:layout_height="0dp"
+ android:layout_width="0dp"
+ android:background="?android:attr/selectableItemBackground"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/car_ui_second_action_container"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent">
+ <com.android.car.ui.uxr.DrawableStateImageView
+ style="@style/Preference.CarUi.Icon"
+ android:id="@android:id/icon"
+ android:layout_width="@dimen/car_ui_preference_icon_size"
+ android:layout_height="@dimen/car_ui_preference_icon_size"
+ android:scaleType="fitCenter"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"/>
+
+ <com.android.car.ui.uxr.DrawableStateTextView
+ android:id="@android:id/title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/car_ui_preference_icon_margin_end"
+ app:layout_goneMarginStart="0dp"
+ android:textDirection="locale"
+ android:singleLine="true"
+ android:textAppearance="@style/TextAppearance.CarUi.PreferenceTitle"
+ app:layout_constraintStart_toEndOf="@android:id/icon"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@android:id/summary"
+ app:layout_constraintVertical_chainStyle="packed"/>
+
+ <com.android.car.ui.uxr.DrawableStateTextView
+ android:id="@android:id/summary"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/car_ui_preference_icon_margin_end"
+ app:layout_goneMarginStart="0dp"
+ android:textDirection="locale"
+ android:textAppearance="@style/TextAppearance.CarUi.PreferenceSummary"
+ android:maxLines="2"
+ app:layout_constraintStart_toEndOf="@android:id/icon"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@android:id/title"
+ app:layout_constraintBottom_toBottomOf="parent"/>
+ </com.android.car.ui.uxr.DrawableStateConstraintLayout>
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:id="@+id/car_ui_second_action_container"
+ android:layout_height="0dp"
+ android:layout_width="wrap_content"
+ app:layout_constraintStart_toEndOf="@id/car_ui_first_action_container"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent">
+
+ <View
+ android:id="@+id/car_ui_divider"
+ android:layout_width="@dimen/car_ui_divider_width"
+ android:layout_height="0dp"
+ android:layout_marginBottom="@dimen/car_ui_preference_content_margin_bottom"
+ android:layout_marginTop="@dimen/car_ui_preference_content_margin_top"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/car_ui_secondary_action"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ style="@style/Preference.CarUi.Divider"/>
+
+ <com.android.car.ui.uxr.DrawableStateFrameLayout
+ android:id="@+id/car_ui_secondary_action"
+ android:layout_width="?android:attr/listPreferredItemHeightSmall"
+ android:layout_height="match_parent"
+ android:background="?android:attr/selectableItemBackground"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ app:layout_constraintStart_toEndOf="@id/car_ui_divider"
+ app:layout_constraintEnd_toStartOf="@android:id/widget_frame"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent">
+ <com.android.car.ui.uxr.DrawableStateImageView
+ android:id="@+id/car_ui_secondary_action_concrete"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_gravity="center"
+ android:tintMode="src_in"
+ android:tint="@color/car_ui_text_color_primary"/>
+ </com.android.car.ui.uxr.DrawableStateFrameLayout>
+
+ <!-- The widget frame is required for androidx preferences, but we won't use it. -->
+ <FrameLayout
+ android:id="@android:id/widget_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"/>
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_preference_two_action_switch.xml b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_preference_two_action_switch.xml
new file mode 100644
index 0000000..e89b844
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_preference_two_action_switch.xml
@@ -0,0 +1,128 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 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.
+-->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall"
+ android:tag="carUiPreference">
+
+ <com.android.car.ui.uxr.DrawableStateConstraintLayout
+ android:id="@+id/car_ui_first_action_container"
+ android:layout_height="0dp"
+ android:layout_width="0dp"
+ android:background="?android:attr/selectableItemBackground"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/car_ui_second_action_container"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent">
+ <com.android.car.ui.uxr.DrawableStateImageView
+ style="@style/Preference.CarUi.Icon"
+ android:id="@android:id/icon"
+ android:layout_width="@dimen/car_ui_preference_icon_size"
+ android:layout_height="@dimen/car_ui_preference_icon_size"
+ android:scaleType="fitCenter"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"/>
+
+ <com.android.car.ui.uxr.DrawableStateTextView
+ android:id="@android:id/title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/car_ui_preference_icon_margin_end"
+ app:layout_goneMarginStart="0dp"
+ android:textDirection="locale"
+ android:singleLine="true"
+ android:textAppearance="@style/TextAppearance.CarUi.PreferenceTitle"
+ app:layout_constraintStart_toEndOf="@android:id/icon"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@android:id/summary"
+ app:layout_constraintVertical_chainStyle="packed"/>
+
+ <com.android.car.ui.uxr.DrawableStateTextView
+ android:id="@android:id/summary"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/car_ui_preference_icon_margin_end"
+ app:layout_goneMarginStart="0dp"
+ android:textDirection="locale"
+ android:textAppearance="@style/TextAppearance.CarUi.PreferenceSummary"
+ android:maxLines="2"
+ app:layout_constraintStart_toEndOf="@android:id/icon"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@android:id/title"
+ app:layout_constraintBottom_toBottomOf="parent"/>
+ </com.android.car.ui.uxr.DrawableStateConstraintLayout>
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:id="@+id/car_ui_second_action_container"
+ android:layout_height="0dp"
+ android:layout_width="wrap_content"
+ app:layout_constraintStart_toEndOf="@id/car_ui_first_action_container"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent">
+
+ <View
+ android:id="@+id/car_ui_divider"
+ android:layout_width="@dimen/car_ui_divider_width"
+ android:layout_height="0dp"
+ android:layout_marginBottom="@dimen/car_ui_preference_content_margin_bottom"
+ android:layout_marginTop="@dimen/car_ui_preference_content_margin_top"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/car_ui_secondary_action"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ style="@style/Preference.CarUi.Divider"/>
+
+ <com.android.car.ui.uxr.DrawableStateFrameLayout
+ android:id="@+id/car_ui_secondary_action"
+ android:layout_width="?android:attr/listPreferredItemHeightSmall"
+ android:layout_height="match_parent"
+ android:background="?android:attr/selectableItemBackground"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ app:layout_constraintStart_toEndOf="@id/car_ui_divider"
+ app:layout_constraintEnd_toStartOf="@android:id/widget_frame"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent">
+ <com.android.car.ui.uxr.DrawableStateSwitch
+ android:id="@+id/car_ui_secondary_action_concrete"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:clickable="false"
+ android:focusable="false"/>
+ </com.android.car.ui.uxr.DrawableStateFrameLayout>
+
+ <!-- The widget frame is required for androidx preferences, but we won't use it. -->
+ <FrameLayout
+ android:id="@android:id/widget_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"/>
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_preference_two_action_text.xml b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_preference_two_action_text.xml
new file mode 100644
index 0000000..bfdcbba
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_preference_two_action_text.xml
@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 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.
+-->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall"
+ android:tag="carUiPreference">
+
+ <com.android.car.ui.uxr.DrawableStateConstraintLayout
+ android:id="@+id/car_ui_first_action_container"
+ android:layout_height="0dp"
+ android:layout_width="0dp"
+ android:background="?android:attr/selectableItemBackground"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/car_ui_second_action_container"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent">
+ <com.android.car.ui.uxr.DrawableStateImageView
+ style="@style/Preference.CarUi.Icon"
+ android:id="@android:id/icon"
+ android:layout_width="@dimen/car_ui_preference_icon_size"
+ android:layout_height="@dimen/car_ui_preference_icon_size"
+ android:scaleType="fitCenter"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"/>
+
+ <com.android.car.ui.uxr.DrawableStateTextView
+ android:id="@android:id/title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/car_ui_preference_icon_margin_end"
+ app:layout_goneMarginStart="0dp"
+ android:textDirection="locale"
+ android:singleLine="true"
+ android:textAppearance="@style/TextAppearance.CarUi.PreferenceTitle"
+ app:layout_constraintStart_toEndOf="@android:id/icon"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@android:id/summary"
+ app:layout_constraintVertical_chainStyle="packed"/>
+
+ <com.android.car.ui.uxr.DrawableStateTextView
+ android:id="@android:id/summary"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/car_ui_preference_icon_margin_end"
+ app:layout_goneMarginStart="0dp"
+ android:textDirection="locale"
+ android:textAppearance="@style/TextAppearance.CarUi.PreferenceSummary"
+ android:maxLines="2"
+ app:layout_constraintStart_toEndOf="@android:id/icon"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@android:id/title"
+ app:layout_constraintBottom_toBottomOf="parent"/>
+ </com.android.car.ui.uxr.DrawableStateConstraintLayout>
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:id="@+id/car_ui_second_action_container"
+ android:layout_height="0dp"
+ android:layout_width="wrap_content"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ app:layout_constraintStart_toEndOf="@id/car_ui_first_action_container"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent">
+
+ <View
+ android:id="@+id/car_ui_divider"
+ android:layout_width="@dimen/car_ui_divider_width"
+ android:layout_height="0dp"
+ android:layout_marginBottom="@dimen/car_ui_preference_content_margin_bottom"
+ android:layout_marginTop="@dimen/car_ui_preference_content_margin_top"
+ android:layout_marginEnd="?android:attr/listPreferredItemPaddingEnd"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/car_ui_secondary_action"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ style="@style/Preference.CarUi.Divider"/>
+
+ <com.android.car.ui.uxr.DrawableStateButton
+ android:id="@+id/car_ui_secondary_action"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintStart_toEndOf="@id/car_ui_divider"
+ app:layout_constraintEnd_toStartOf="@android:id/widget_frame"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"/>
+
+ <!-- The widget frame is required for androidx preferences, but we won't use it. -->
+ <FrameLayout
+ android:id="@android:id/widget_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"/>
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_preference_two_action_text_borderless.xml b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_preference_two_action_text_borderless.xml
new file mode 100644
index 0000000..4553739
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_preference_two_action_text_borderless.xml
@@ -0,0 +1,120 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 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.
+-->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall"
+ android:tag="carUiPreference">
+
+ <com.android.car.ui.uxr.DrawableStateConstraintLayout
+ android:id="@+id/car_ui_first_action_container"
+ android:layout_height="0dp"
+ android:layout_width="0dp"
+ android:background="?android:attr/selectableItemBackground"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/car_ui_second_action_container"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent">
+ <com.android.car.ui.uxr.DrawableStateImageView
+ style="@style/Preference.CarUi.Icon"
+ android:id="@android:id/icon"
+ android:layout_width="@dimen/car_ui_preference_icon_size"
+ android:layout_height="@dimen/car_ui_preference_icon_size"
+ android:scaleType="fitCenter"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"/>
+
+ <com.android.car.ui.uxr.DrawableStateTextView
+ android:id="@android:id/title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/car_ui_preference_icon_margin_end"
+ app:layout_goneMarginStart="0dp"
+ android:textDirection="locale"
+ android:singleLine="true"
+ android:textAppearance="@style/TextAppearance.CarUi.PreferenceTitle"
+ app:layout_constraintStart_toEndOf="@android:id/icon"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@android:id/summary"
+ app:layout_constraintVertical_chainStyle="packed"/>
+
+ <com.android.car.ui.uxr.DrawableStateTextView
+ android:id="@android:id/summary"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/car_ui_preference_icon_margin_end"
+ app:layout_goneMarginStart="0dp"
+ android:textDirection="locale"
+ android:textAppearance="@style/TextAppearance.CarUi.PreferenceSummary"
+ android:maxLines="2"
+ app:layout_constraintStart_toEndOf="@android:id/icon"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@android:id/title"
+ app:layout_constraintBottom_toBottomOf="parent"/>
+ </com.android.car.ui.uxr.DrawableStateConstraintLayout>
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:id="@+id/car_ui_second_action_container"
+ android:layout_height="0dp"
+ android:layout_width="wrap_content"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ app:layout_constraintStart_toEndOf="@id/car_ui_first_action_container"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent">
+
+ <View
+ android:id="@+id/car_ui_divider"
+ android:layout_width="@dimen/car_ui_divider_width"
+ android:layout_height="0dp"
+ android:layout_marginBottom="@dimen/car_ui_preference_content_margin_bottom"
+ android:layout_marginTop="@dimen/car_ui_preference_content_margin_top"
+ android:layout_marginEnd="?android:attr/listPreferredItemPaddingEnd"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/car_ui_secondary_action"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ style="@style/Preference.CarUi.Divider"/>
+
+ <com.android.car.ui.uxr.DrawableStateButton
+ android:id="@+id/car_ui_secondary_action"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="?android:attr/borderlessButtonStyle"
+ app:layout_constraintStart_toEndOf="@id/car_ui_divider"
+ app:layout_constraintEnd_toStartOf="@android:id/widget_frame"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"/>
+
+ <!-- The widget frame is required for androidx preferences, but we won't use it. -->
+ <FrameLayout
+ android:id="@android:id/widget_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"/>
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_recycler_view.xml b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_recycler_view.xml
index a0c955f..6e12515 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_recycler_view.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_recycler_view.xml
@@ -16,13 +16,18 @@
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
- <include layout="@layout/car_ui_recyclerview_scrollbar"/>
-
<com.android.car.ui.recyclerview.CarUiRecyclerViewContainer
android:id="@+id/car_ui_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:layout_marginEnd="@dimen/car_ui_scrollbar_margin"
- android:tag="carUiRecyclerView"
- android:layout_weight="1"/>
+ android:paddingStart="@dimen/car_ui_scrollbar_margin"
+ android:paddingEnd="@dimen/car_ui_scrollbar_margin"
+ android:tag="carUiRecyclerView" />
+
+ <FrameLayout
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_gravity="left" >
+ <include layout="@layout/car_ui_recyclerview_scrollbar"/>
+ </FrameLayout>
</merge>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_recyclerview_scrollbar.xml b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_recyclerview_scrollbar.xml
index 5896819..d5e626a 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_recyclerview_scrollbar.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_recyclerview_scrollbar.xml
@@ -45,7 +45,8 @@
android:layout_height="0dp"
android:layout_gravity="center_horizontal"
android:background="@drawable/car_ui_recyclerview_scrollbar_thumb"
- app:layout_constraintTop_toBottomOf="@+id/car_ui_scrollbar_page_up"
+ app:layout_constraintTop_toTopOf="@+id/car_ui_scrollbar_track"
+ app:layout_constraintBottom_toBottomOf="@+id/car_ui_scrollbar_track"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"/>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_two_action_preference.xml b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_two_action_preference.xml
index 7c633e0..3f3ffb0 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_two_action_preference.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_two_action_preference.xml
@@ -22,7 +22,7 @@
android:background="@android:color/transparent"
android:gravity="center_vertical"
android:minHeight="?android:attr/listPreferredItemHeightSmall">
- <LinearLayout
+ <com.android.car.ui.uxr.DrawableStateLinearLayout
android:id="@+id/car_ui_preference_container_without_widget"
android:layout_width="0dp"
android:layout_height="match_parent"
@@ -44,20 +44,20 @@
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:orientation="vertical">
- <TextView
+ <com.android.car.ui.uxr.DrawableStateTextView
android:id="@android:id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.CarUi.PreferenceTitle"/>
- <TextView
+ <com.android.car.ui.uxr.DrawableStateTextView
android:id="@android:id/summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.CarUi.PreferenceSummary"/>
</LinearLayout>
- </LinearLayout>
+ </com.android.car.ui.uxr.DrawableStateLinearLayout>
<LinearLayout
android:id="@+id/action_widget_container"
android:layout_width="wrap_content"
@@ -69,7 +69,7 @@
android:layout_marginTop="@dimen/car_ui_preference_content_margin_top"
style="@style/Preference.CarUi.Divider"/>
<!-- Preference should place its actual preference widget here. -->
- <FrameLayout
+ <com.android.car.ui.uxr.DrawableStateFrameLayout
android:id="@android:id/widget_frame"
android:layout_width="wrap_content"
android:layout_height="match_parent"
diff --git a/car-ui-lib/car-ui-lib/src/main/res/values/attrs.xml b/car-ui-lib/car-ui-lib/src/main/res/values/attrs.xml
index 0ca5abe..2a5bd75 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/values/attrs.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/values/attrs.xml
@@ -130,18 +130,43 @@
<!-- grid layout -->
<enum name="grid" value="1" />
</attr>
+
+ <!-- car ui recyclerview orientation -->
+ <attr name="android:orientation" />
</declare-styleable>
<declare-styleable name="CarUiPreference">
<!-- Toggle for showing chevron -->
<attr name="showChevron" format="boolean" />
- <!-- Show ripple when disabled preference is clicked -->
- <attr name="showRippleOnDisabledPreference" format="boolean" />
+ <!-- Display this preference as ux restricted. -->
+ <attr name="car_ui_ux_restricted" format="boolean" />
</declare-styleable>
<declare-styleable name="CarUiTwoActionPreference">
<!-- Determines if the secondary action is initially shown -->
<attr name="actionShown" format="boolean"/>
+ <!-- Determines if the secondary action is initially enabled -->
+ <attr name="actionEnabled" format="boolean"/>
+ </declare-styleable>
+
+ <declare-styleable name="CarUiTwoActionBasePreference">
+ <!-- All of these are disallowed -->
+ <attr name="layout" format="reference"/>
+ <attr name="android:layout" format="reference"/>
+ <attr name="widgetLayout" format="reference"/>
+ <attr name="android:widgetLayout" format="reference"/>
+ </declare-styleable>
+
+ <declare-styleable name="CarUiTwoActionTextPreference">
+ <attr name="secondaryActionStyle" format="enum">
+ <enum name="bordered" value="0"/>
+ <enum name="borderless" value="1"/>
+ </attr>
+ <attr name="secondaryActionText" format="string"/>
+ </declare-styleable>
+
+ <declare-styleable name="CarUiTwoActionIconPreference">
+ <attr name="secondaryActionIcon" format="reference"/>
</declare-styleable>
<!-- Theme attribute to specify a default style for all CarUiPreferences -->
@@ -161,25 +186,30 @@
view. The target view is chosen in the following order:
1. the "android:focusedByDefault" view, if any
2. the "app:defaultFocus" view, if any
- 3. the first focusable item in a scrollable container, if any
- 4. previously focused view, if any and the cache is not stale
- 5. the first focusable view, if any
- Note that 4 will be prioritized over 1&2&3 when
- car_ui_focus_area_default_focus_overrides_history is false.
+ 3. the selected item in a scrollable container, if any
+ 4. the first focusable item in a scrollable container, if any
+ 5. previously focused view, if any and the cache is not stale
+ 6. the first focusable view, if any
+ Note that 5 will be prioritized over 1, 2, 3, and 4 when
+ "app:defaultFocusOverridesHistory" is true.
(2) When it needs to initialize the focus (such as when a window is opened), it will
search for a view in the window and focus on it. The view is chosen in the
following order:
1. the first "android:focusedByDefault" view, if any
2. the first "app:defaultFocus" view, if any
- 3. the first focusable item in a scrollable container, if any
- 4. the first focusable view that is not a FocusParkingView, if any
+ 3. the selected item in a scrollable container, if any
+ 4. the first focusable item in a scrollable container, if any
+ 5. the first focusable view that is not a FocusParkingView, if any
If there is only one FocusArea that needs to set default focus, you can use either
"app:defaultFocus" or "android:focusedByDefault". If there are more than one, you
should use "android:focusedByDefault" in the primary FocusArea, and use
"app:defaultFocus" in other FocusAreas. -->
-
<attr name="defaultFocus" format="reference"/>
+ <!-- Whether to focus on the default focus view when nudging to the FocusArea, even if there
+ was another view in the FocusArea focused before. -->
+ <attr name="defaultFocusOverridesHistory" format="boolean"/>
+
<!-- The paddings of FocusArea highlight. It does't impact the paddings on its child views,
or vice versa. -->
<!-- The start padding of the FocusArea highlight. -->
@@ -242,6 +272,11 @@
<attr name="nudgeUp" format="reference"/>
<!-- The ID of the target FocusArea when nudging down. -->
<attr name="nudgeDown" format="reference"/>
+
+ <!-- Whether rotation wraps around. When true, rotation wraps around, staying within the
+ FocusArea, when it reaches the first or last focusable view in the FocusArea. When
+ false, rotation does nothing in this case. -->
+ <attr name="wrapAround" format="boolean"/>
</declare-styleable>
<!-- Attributes for FocusParkingView. -->
diff --git a/car-ui-lib/car-ui-lib/src/main/res/values/bools.xml b/car-ui-lib/car-ui-lib/src/main/res/values/bools.xml
index ccf9c4d..ab575b2 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/values/bools.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/values/bools.xml
@@ -44,6 +44,8 @@
<bool name="car_ui_preference_show_chevron">false</bool>
<!-- whether list preference should be shown in full screen or as a dialog -->
<bool name="car_ui_preference_list_show_full_screen">true</bool>
+ <!-- Whether list and multi-select preference selection changes should propagate instantly -->
+ <bool name="car_ui_preference_list_instant_change_callback">false</bool>
<!-- List items -->
@@ -57,9 +59,8 @@
<!-- Whether to draw highlight (car_ui_focus_area_background_highlight) on top of the FocusArea
but behind its children when the FocusArea's descendant gets focused. -->
<bool name="car_ui_enable_focus_area_background_highlight">false</bool>
- <!-- Whether to focus on the app:defaultFocus view when nudging to the FocusArea, even if there
- was another view focused in the FocusArea. -->
- <bool name="car_ui_focus_area_default_focus_overrides_history">true</bool>
+ <!-- Deprecated. -->
+ <bool name="car_ui_focus_area_default_focus_overrides_history">false</bool>
<!-- Whether to clear FocusArea history when the user rotates the rotary controller. -->
<bool name="car_ui_clear_focus_area_history_when_rotating">true</bool>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/values/dimens.xml b/car-ui-lib/car-ui-lib/src/main/res/values/dimens.xml
index d355f99..998d58f 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/values/dimens.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/values/dimens.xml
@@ -247,4 +247,10 @@
<dimen name="car_ui_ime_wide_search_item_sub_title_padding_top">22dp</dimen>
<dimen name="car_ui_ime_wide_search_item_sub_title_text_size">24dp</dimen>
<dimen name="car_ui_ime_wide_search_item_secondary_image_padding_left">36dp</dimen>
+
+ <!-- Padding towards the end of the input field. We have an overlay on top
+ of input text field that displays the clear and the error icon. This
+ padding is needed so that the icon does not overlap the input field. The
+ padding is calculated by 24dp + car_ui_primary_icon_size (44dp). -->
+ <dimen name="car_ui_ime_wide_screen_input_area_padding_end">68dp</dimen>
</resources>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/values/styles.xml b/car-ui-lib/car-ui-lib/src/main/res/values/styles.xml
index 00cb56e..bd033ec 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/values/styles.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/values/styles.xml
@@ -246,7 +246,7 @@
<!-- TextAppearance -->
<style name="TextAppearance.CarUi" parent="android:TextAppearance.DeviceDefault">
- <item name="android:textColor">?android:attr/textColorPrimary</item>
+ <item name="android:textColor">@color/car_ui_text_color_primary</item>
<item name="android:textAlignment">viewStart</item>
</style>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/values/themes.xml b/car-ui-lib/car-ui-lib/src/main/res/values/themes.xml
index a599fa0..c2069b1 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/values/themes.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/values/themes.xml
@@ -35,6 +35,7 @@
<item name="selectableItemBackground">?android:attr/selectableItemBackground</item>
<item name="selectableItemBackgroundBorderless">?android:attr/selectableItemBackgroundBorderless</item>
+ <item name="android:borderlessButtonStyle">@style/Widget.CarUi.Button.Borderless.Colored</item>
<item name="borderlessButtonStyle">?android:attr/borderlessButtonStyle</item>
<item name="homeAsUpIndicator">?android:attr/homeAsUpIndicator</item>
@@ -208,6 +209,8 @@
<!-- textAppearance -->
<item name="android:textAppearance">@style/TextAppearance.CarUi</item>
+ <!--@color/transparent does not completely remove highlight -->
+ <item name="android:textColorHighlight">#00FFFFFF</item>
</style>
<!-- TODO(b/150230923) remove this when other apps are ready -->
diff --git a/car-ui-lib/car-ui-lib/src/test/java/com/android/car/ui/recyclerview/CarUiListItemTest.java b/car-ui-lib/car-ui-lib/src/test/java/com/android/car/ui/recyclerview/CarUiListItemTest.java
index cf28632..c54dab4 100644
--- a/car-ui-lib/car-ui-lib/src/test/java/com/android/car/ui/recyclerview/CarUiListItemTest.java
+++ b/car-ui-lib/car-ui-lib/src/test/java/com/android/car/ui/recyclerview/CarUiListItemTest.java
@@ -62,33 +62,38 @@
}
private View getListItemTitleAtPosition(int position) {
- return getListItemViewHolderAtPosition(position).itemView.findViewById(R.id.title);
+ return getListItemViewHolderAtPosition(position).itemView
+ .findViewById(R.id.car_ui_list_item_title);
}
private View getListItemBodyAtPosition(int position) {
- return getListItemViewHolderAtPosition(position).itemView.findViewById(R.id.body);
+ return getListItemViewHolderAtPosition(position).itemView
+ .findViewById(R.id.car_ui_list_item_body);
}
private View getListItemIconContainerAtPosition(int position) {
- return getListItemViewHolderAtPosition(position).itemView.findViewById(R.id.icon_container);
+ return getListItemViewHolderAtPosition(position).itemView
+ .findViewById(R.id.car_ui_list_item_icon_container);
}
private View getListItemActionContainerAtPosition(int position) {
- return getListItemViewHolderAtPosition(position)
- .itemView.findViewById(R.id.action_container);
+ return getListItemViewHolderAtPosition(position).itemView
+ .findViewById(R.id.action_container);
}
private Switch getListItemSwitchAtPosition(int position) {
- return getListItemViewHolderAtPosition(position).itemView.findViewById(R.id.switch_widget);
+ return getListItemViewHolderAtPosition(position).itemView
+ .findViewById(R.id.car_ui_list_item_switch_widget);
}
private CheckBox getListItemCheckBoxAtPosition(int position) {
- return getListItemViewHolderAtPosition(position)
- .itemView.findViewById(R.id.checkbox_widget);
+ return getListItemViewHolderAtPosition(position).itemView
+ .findViewById(R.id.car_ui_list_item_checkbox_widget);
}
private View getListItemIconAtPosition(int position) {
- return getListItemViewHolderAtPosition(position).itemView.findViewById(R.id.icon);
+ return getListItemViewHolderAtPosition(position).itemView
+ .findViewById(R.id.icon);
}
private CarUiListItemAdapter.HeaderViewHolder getHeaderViewHolderAtPosition(int position) {
@@ -97,11 +102,13 @@
}
private TextView getHeaderViewHolderTitleAtPosition(int position) {
- return getHeaderViewHolderAtPosition(position).itemView.findViewById(R.id.title);
+ return getHeaderViewHolderAtPosition(position).itemView
+ .findViewById(R.id.car_ui_list_item_title);
}
private TextView getHeaderViewHolderBodyAtPosition(int position) {
- return getHeaderViewHolderAtPosition(position).itemView.findViewById(R.id.body);
+ return getHeaderViewHolderAtPosition(position).itemView
+ .findViewById(R.id.car_ui_list_item_body);
}
private void updateRecyclerViewAdapter(CarUiListItemAdapter adapter) {
diff --git a/car-ui-lib/paintbooth/AndroidManifest.xml b/car-ui-lib/paintbooth/AndroidManifest.xml
index 9763b47..dd2e3e7 100644
--- a/car-ui-lib/paintbooth/AndroidManifest.xml
+++ b/car-ui-lib/paintbooth/AndroidManifest.xml
@@ -53,6 +53,7 @@
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
+ <meta-data android:name="distractionOptimized" android:value="true"/>
</activity>
<activity
@@ -70,6 +71,12 @@
<activity
android:name=".preferences.PreferenceActivity"
android:exported="false"
+ android:parentActivityName=".MainActivity">
+ <meta-data android:name="distractionOptimized" android:value="true"/>
+ </activity>
+ <activity
+ android:name=".preferences.SplitPreferenceActivity"
+ android:exported="false"
android:parentActivityName=".MainActivity"/>
<activity
android:name=".widescreenime.WideScreenImeActivity"
diff --git a/car-ui-lib/paintbooth/build.gradle b/car-ui-lib/paintbooth/build.gradle
index 94ab49e..739e592 100644
--- a/car-ui-lib/paintbooth/build.gradle
+++ b/car-ui-lib/paintbooth/build.gradle
@@ -42,6 +42,9 @@
}
}
}
+
+ // This is the gradle equivalent of the libs: ["android.car"] in the Android.bp
+ useLibrary 'android.car'
}
dependencies {
diff --git a/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/preferences/PreferenceDemoFragment.java b/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/preferences/PreferenceDemoFragment.java
index e9f990d..b7636ad 100644
--- a/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/preferences/PreferenceDemoFragment.java
+++ b/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/preferences/PreferenceDemoFragment.java
@@ -16,33 +16,111 @@
package com.android.car.ui.paintbooth.preferences;
+import android.car.drivingstate.CarUxRestrictions;
import android.os.Bundle;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceGroup;
import com.android.car.ui.paintbooth.R;
-import com.android.car.ui.preference.CarUiPreference;
+import com.android.car.ui.preference.CarUiTwoActionBasePreference;
+import com.android.car.ui.preference.CarUiTwoActionIconPreference;
+import com.android.car.ui.preference.CarUiTwoActionSwitchPreference;
+import com.android.car.ui.preference.CarUiTwoActionTextPreference;
import com.android.car.ui.preference.PreferenceFragment;
+import com.android.car.ui.preference.UxRestrictablePreference;
+import com.android.car.ui.utils.CarUxRestrictionsUtil;
+
+import java.util.Objects;
/**
* Fragment to load preferences
*/
public class PreferenceDemoFragment extends PreferenceFragment {
+ CarUxRestrictionsUtil.OnUxRestrictionsChangedListener mOnUxRestrictionsChangedListener =
+ restrictions -> {
+ boolean isRestricted =
+ (restrictions.getActiveRestrictions()
+ & CarUxRestrictions.UX_RESTRICTIONS_NO_SETUP)
+ == CarUxRestrictions.UX_RESTRICTIONS_NO_SETUP;
+
+ restrictPreference(getPreferenceScreen(), isRestricted);
+ };
+
+ private void restrictPreference(Preference preference, boolean restrict) {
+ if (preference == null) {
+ return;
+ }
+
+ if (preference instanceof UxRestrictablePreference) {
+ ((UxRestrictablePreference) preference).setUxRestricted(restrict);
+ ((UxRestrictablePreference) preference).setOnClickWhileRestrictedListener(p ->
+ Toast.makeText(getContext(), R.string.car_ui_restricted_while_driving,
+ Toast.LENGTH_LONG).show());
+ }
+
+ if (preference instanceof PreferenceGroup) {
+ PreferenceGroup preferenceGroup = (PreferenceGroup) preference;
+ for (int i = 0; i < preferenceGroup.getPreferenceCount(); i++) {
+ restrictPreference(preferenceGroup.getPreference(i), restrict);
+ }
+ }
+ }
+
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
// Load the preferences from an XML resource
setPreferencesFromResource(R.xml.preference_samples, rootKey);
- CarUiPreference preferenceDisabledWithoutRipple = findPreference(
- "preference_disabled_without_ripple");
- preferenceDisabledWithoutRipple.setEnabled(false);
- preferenceDisabledWithoutRipple.setMessageToShowWhenDisabledPreferenceClicked(
- "I am disabled because...");
- preferenceDisabledWithoutRipple.setShouldShowRippleOnDisabledPreference(false);
- CarUiPreference preferenceDisabledWithRipple = findPreference(
- "preference_disabled_with_ripple");
- preferenceDisabledWithRipple.setEnabled(false);
- preferenceDisabledWithRipple.setMessageToShowWhenDisabledPreferenceClicked(
- "I am disabled because...");
- preferenceDisabledWithRipple.setShouldShowRippleOnDisabledPreference(true);
+ setupTwoActionPreferenceClickListeners(requirePreference("twoactiontext"));
+ setupTwoActionPreferenceClickListeners(requirePreference("twoactiontextborderless"));
+ setupTwoActionPreferenceClickListeners(requirePreference("twoactionicon"));
+ setupTwoActionPreferenceClickListeners(requirePreference("twoactionswitch"));
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ CarUxRestrictionsUtil.getInstance(requireContext())
+ .register(mOnUxRestrictionsChangedListener);
+ }
+
+ @Override
+ public void onStop() {
+ CarUxRestrictionsUtil.getInstance(requireContext())
+ .unregister(mOnUxRestrictionsChangedListener);
+ super.onStop();
+ }
+
+ private void setupTwoActionPreferenceClickListeners(CarUiTwoActionBasePreference preference) {
+ if (preference instanceof CarUiTwoActionSwitchPreference) {
+ ((CarUiTwoActionSwitchPreference) preference).setOnSecondaryActionClickListener(
+ (selected) -> preference.setSecondaryActionEnabled(false));
+ } else if (preference instanceof CarUiTwoActionTextPreference) {
+ ((CarUiTwoActionTextPreference) preference).setOnSecondaryActionClickListener(
+ () -> preference.setSecondaryActionEnabled(false));
+ } else {
+ ((CarUiTwoActionIconPreference) preference).setOnSecondaryActionClickListener(
+ () -> preference.setSecondaryActionEnabled(false));
+ }
+
+ preference.setOnPreferenceClickListener((pref) -> {
+ if (!preference.isSecondaryActionEnabled()) {
+ preference.setSecondaryActionEnabled(true);
+ } else {
+ preference.setSecondaryActionVisible(
+ !preference.isSecondaryActionVisible());
+ }
+ return true;
+ });
+ }
+
+ @NonNull
+ private <T extends Preference> T requirePreference(CharSequence key) {
+ T pref = findPreference(key);
+ return Objects.requireNonNull(pref);
}
}
diff --git a/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/widescreenime/WideScreenImeActivity.java b/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/widescreenime/WideScreenImeActivity.java
index 71eb954..2eb3063 100644
--- a/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/widescreenime/WideScreenImeActivity.java
+++ b/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/widescreenime/WideScreenImeActivity.java
@@ -101,7 +101,7 @@
});
CarUiContentListItem.OnClickListener mainClickListener = i ->
- Toast.makeText(this, "Item clicked!", Toast.LENGTH_SHORT).show();
+ Toast.makeText(this, "Item clicked! " + i.getTitle(), Toast.LENGTH_SHORT).show();
CarUiContentListItem.OnClickListener secondaryClickListener = i ->
Toast.makeText(this, "Item's secondary action clicked!", Toast.LENGTH_SHORT).show();
diff --git a/car-ui-lib/paintbooth/src/main/res/drawable/ic_settings_wifi.xml b/car-ui-lib/paintbooth/src/main/res/drawable/ic_settings_wifi.xml
index 9a09d70..c16424c 100644
--- a/car-ui-lib/paintbooth/src/main/res/drawable/ic_settings_wifi.xml
+++ b/car-ui-lib/paintbooth/src/main/res/drawable/ic_settings_wifi.xml
@@ -16,12 +16,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="64dp"
android:height="64dp"
- android:viewportWidth="64"
- android:viewportHeight="64">
+ android:viewportWidth="24"
+ android:viewportHeight="24">
<path
- android:pathData="M36.87,39.05a7,7 0,1 1,-9.901 9.9,7 7,0 0,1 9.901,-9.9zM14.243,26.323c9.763,-9.764 25.593,-9.764 35.355,0M53.84,22.08C41.735,9.972 22.106,9.972 10,22.08M18.486,30.565c7.42,-7.42 19.449,-7.42 26.869,0M22.728,34.808c5.077,-5.077 13.308,-5.077 18.385,0"
- android:strokeWidth="2"
- android:fillColor="#00000000"
- android:fillType="evenOdd"
- android:strokeColor="#FFF"/>
+ android:pathData="M1 9l2 2c4.97-4.97 13.03-4.97 18 0l2-2C16.93 2.93 7.08 2.93 1 9zm8 8l3 3 3-3c-1.65-1.66-4.34-1.66-6 0zm-4-4l2 2c2.76-2.76 7.24-2.76 10 0l2-2C15.14 9.14 8.87 9.14 5 13z"
+ android:fillColor="@color/car_ui_text_color_primary"/>
</vector>
diff --git a/connected-device-lib/res/values/config.xml b/car-ui-lib/paintbooth/src/main/res/values/overlayable.xml
similarity index 62%
rename from connected-device-lib/res/values/config.xml
rename to car-ui-lib/paintbooth/src/main/res/values/overlayable.xml
index c719462..0577d92 100644
--- a/connected-device-lib/res/values/config.xml
+++ b/car-ui-lib/paintbooth/src/main/res/values/overlayable.xml
@@ -1,6 +1,6 @@
-<?xml version="1.0" encoding="UTF-8"?>
+<?xml version='1.0' encoding='UTF-8'?>
<!--
- ~ Copyright (C) 2019 The Android Open Source Project
+ ~ Copyright (C) 2021 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
@@ -14,7 +14,11 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-
-<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="connected_device_shared_preferences" translatable="false">com.android.car.connecteddevice</string>
+<!-- THIS FILE WAS AUTO GENERATED, DO NOT EDIT MANUALLY. -->
+<resources>
+ <overlayable name="PaintboothResources">
+ <policy type="public">
+ <item type="drawable" name="simulated_screen_shape"/>
+ </policy>
+ </overlayable>
</resources>
diff --git a/car-ui-lib/paintbooth/src/main/res/values/strings.xml b/car-ui-lib/paintbooth/src/main/res/values/strings.xml
index 5965d68..b438835 100644
--- a/car-ui-lib/paintbooth/src/main/res/values/strings.xml
+++ b/car-ui-lib/paintbooth/src/main/res/values/strings.xml
@@ -63,6 +63,8 @@
<string name="title_twoaction_preference">TwoAction preference</string>
<!-- Summary of a two action preference [CHAR_LIMIT=70]-->
<string name="summary_twoaction_preference">A widget should be visible on the right</string>
+ <!-- Summary of the deprecated two action preference [CHAR_LIMIT=70]-->
+ <string name="summary_deprecated_twoaction_preference">The deprecated version of TwoActionPreference</string>
<!-- Title of a checkbox preference [CHAR_LIMIT=31]-->
<string name="title_checkbox_preference">Checkbox preference</string>
diff --git a/car-ui-lib/paintbooth/src/main/res/xml/preference_samples.xml b/car-ui-lib/paintbooth/src/main/res/xml/preference_samples.xml
index d9a891c..4cc4ada 100644
--- a/car-ui-lib/paintbooth/src/main/res/xml/preference_samples.xml
+++ b/car-ui-lib/paintbooth/src/main/res/xml/preference_samples.xml
@@ -28,16 +28,6 @@
android:title="@string/title_basic_preference"/>
<Preference
- android:key="preference_disabled_without_ripple"
- android:summary="Without ripple"
- android:title="@string/title_basic_preference"/>
-
- <Preference
- android:key="preference_disabled_with_ripple"
- android:summary="With ripple"
- android:title="@string/title_basic_preference"/>
-
- <Preference
android:key="stylized"
android:dependency="preference"
android:summary="@string/summary_stylish_preference"
@@ -89,9 +79,33 @@
<com.android.car.ui.preference.CarUiTwoActionPreference
android:key="twoaction"
- android:summary="@string/summary_twoaction_preference"
+ android:summary="@string/summary_deprecated_twoaction_preference"
android:title="@string/title_twoaction_preference"
android:widgetLayout="@layout/details_preference_widget"/>
+
+ <com.android.car.ui.preference.CarUiTwoActionTextPreference
+ android:key="twoactiontext"
+ android:summary="@string/summary_twoaction_preference"
+ android:title="@string/title_twoaction_preference"
+ app:secondaryActionText="Secondary action"/>
+ <com.android.car.ui.preference.CarUiTwoActionTextPreference
+ android:key="twoactiontextborderless"
+ android:icon="@drawable/ic_settings_wifi"
+ android:summary="@string/summary_twoaction_preference"
+ android:title="@string/title_twoaction_preference"
+ app:secondaryActionStyle="borderless"
+ app:secondaryActionText="Secondary action"/>
+ <com.android.car.ui.preference.CarUiTwoActionIconPreference
+ android:key="twoactionicon"
+ android:icon="@drawable/ic_settings_wifi"
+ android:summary="@string/summary_twoaction_preference"
+ android:title="@string/title_twoaction_preference"
+ app:secondaryActionIcon="@drawable/ic_launcher"/>
+ <com.android.car.ui.preference.CarUiTwoActionSwitchPreference
+ android:key="twoactionswitch"
+ android:icon="@drawable/ic_settings_wifi"
+ android:summary="@string/summary_twoaction_preference"
+ android:title="@string/title_twoaction_preference"/>
</PreferenceCategory>
<PreferenceCategory
diff --git a/car-ui-lib/referencedesign/Android.mk b/car-ui-lib/referencedesign/Android.mk
index ef0ad97..974c19a 100644
--- a/car-ui-lib/referencedesign/Android.mk
+++ b/car-ui-lib/referencedesign/Android.mk
@@ -6,6 +6,7 @@
CAR_UI_RESOURCE_DIR := $(LOCAL_PATH)/res
CAR_UI_RRO_TARGETS := \
com.android.car.ui.paintbooth \
+ com.google.android.car.ui.paintbooth \
com.android.car.rotaryplayground \
com.android.car.themeplayground \
com.android.car.carlauncher \
diff --git a/car-ui-lib/referencedesign/product.mk b/car-ui-lib/referencedesign/product.mk
index e834714..0ccf70e 100644
--- a/car-ui-lib/referencedesign/product.mk
+++ b/car-ui-lib/referencedesign/product.mk
@@ -3,6 +3,7 @@
# Include generated RROs
PRODUCT_PACKAGES += \
googlecarui-com-android-car-ui-paintbooth \
+ googlecarui-com-google-android-car-ui-paintbooth \
googlecarui-com-android-car-rotaryplayground \
googlecarui-com-android-car-themeplayground \
googlecarui-com-android-car-carlauncher \
diff --git a/car-ui-lib/referencedesign/res/color/car_ui_text_color_primary.xml b/car-ui-lib/referencedesign/res/color/car_ui_text_color_primary.xml
index 34033e2..d310838 100644
--- a/car-ui-lib/referencedesign/res/color/car_ui_text_color_primary.xml
+++ b/car-ui-lib/referencedesign/res/color/car_ui_text_color_primary.xml
@@ -21,5 +21,8 @@
<item android:state_enabled="false"
android:alpha="?android:attr/disabledAlpha"
android:color="?android:attr/colorForeground"/>
+ <item app:state_ux_restricted="true"
+ android:alpha="?android:attr/disabledAlpha"
+ android:color="?android:attr/colorForeground"/>
<item android:color="?android:attr/colorForeground"/>
</selector>
diff --git a/car-ui-lib/referencedesign/res/layout-ldrtl/car_ui_recycler_view.xml b/car-ui-lib/referencedesign/res/layout-ldrtl/car_ui_recycler_view.xml
index 4847388..f487e35 100644
--- a/car-ui-lib/referencedesign/res/layout-ldrtl/car_ui_recycler_view.xml
+++ b/car-ui-lib/referencedesign/res/layout-ldrtl/car_ui_recycler_view.xml
@@ -18,11 +18,16 @@
<com.android.car.ui.recyclerview.CarUiRecyclerViewContainer
android:id="@+id/car_ui_recycler_view"
- android:layout_width="0dp"
+ android:layout_width="match_parent"
android:layout_height="match_parent"
- android:layout_marginStart="@dimen/car_ui_scrollbar_margin"
- android:tag="carUiRecyclerView"
- android:layout_weight="1"/>
+ android:paddingStart="@dimen/car_ui_scrollbar_margin"
+ android:paddingEnd="@dimen/car_ui_scrollbar_margin"
+ android:tag="carUiRecyclerView" />
- <include layout="@layout/car_ui_recyclerview_scrollbar"/>
+ <FrameLayout
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_gravity="left" >
+ <include layout="@layout/car_ui_recyclerview_scrollbar"/>
+ </FrameLayout>
</merge>
diff --git a/car-ui-lib/referencedesign/res/layout-ldrtl/car_ui_recyclerview_scrollbar.xml b/car-ui-lib/referencedesign/res/layout-ldrtl/car_ui_recyclerview_scrollbar.xml
index 5896819..d5e626a 100644
--- a/car-ui-lib/referencedesign/res/layout-ldrtl/car_ui_recyclerview_scrollbar.xml
+++ b/car-ui-lib/referencedesign/res/layout-ldrtl/car_ui_recyclerview_scrollbar.xml
@@ -45,7 +45,8 @@
android:layout_height="0dp"
android:layout_gravity="center_horizontal"
android:background="@drawable/car_ui_recyclerview_scrollbar_thumb"
- app:layout_constraintTop_toBottomOf="@+id/car_ui_scrollbar_page_up"
+ app:layout_constraintTop_toTopOf="@+id/car_ui_scrollbar_track"
+ app:layout_constraintBottom_toBottomOf="@+id/car_ui_scrollbar_track"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"/>
diff --git a/car-ui-lib/referencedesign/res/values/attrs.xml b/car-ui-lib/referencedesign/res/values/attrs.xml
index 0708c10..74adbf9 100644
--- a/car-ui-lib/referencedesign/res/values/attrs.xml
+++ b/car-ui-lib/referencedesign/res/values/attrs.xml
@@ -56,6 +56,6 @@
<attr name="layout_goneMarginStart" format="dimension"/>
<attr name="layout_goneMarginEnd" format="dimension"/>
- <attr name="state_ux_restricted" format="boolean"/>
+ <attr name="state_ux_restricted"/>
</resources>
diff --git a/car-ui-lib/referencedesign/res/values/styles.xml b/car-ui-lib/referencedesign/res/values/styles.xml
index b33d11a..c8d7e6e 100644
--- a/car-ui-lib/referencedesign/res/values/styles.xml
+++ b/car-ui-lib/referencedesign/res/values/styles.xml
@@ -72,7 +72,7 @@
</style>
<style name="TextAppearance.CarUi" parent="android:TextAppearance.DeviceDefault">
- <item name="android:textColor">?android:attr/textColorPrimary</item>
+ <item name="android:textColor">@color/car_ui_text_color_primary</item>
<item name="android:textAlignment">viewStart</item>
</style>
diff --git a/car-ui-lib/referencedesign/res/xml/overlays.xml b/car-ui-lib/referencedesign/res/xml/overlays.xml
index 6332b67..a72420e 100644
--- a/car-ui-lib/referencedesign/res/xml/overlays.xml
+++ b/car-ui-lib/referencedesign/res/xml/overlays.xml
@@ -1,105 +1,93 @@
<overlay>
- <item target="layout/car_ui_base_layout_toolbar" value="@layout/car_ui_base_layout_toolbar"/>
- <item target="layout/car_ui_toolbar" value="@layout/car_ui_toolbar"/>
- <item target="layout/car_ui_toolbar_two_row" value="@layout/car_ui_toolbar_two_row"/>
- <item target="layout/car_ui_toolbar_menu_item" value="@layout/car_ui_toolbar_menu_item"/>
- <item target="layout/car_ui_toolbar_menu_item_primary" value="@layout/car_ui_toolbar_menu_item_primary"/>
- <item target="layout/car_ui_preference_widget_seekbar" value="@layout/car_ui_preference_widget_seekbar"/>
+ <item target="attr/layout_constraintBottom_toBottomOf" value="@attr/layout_constraintBottom_toBottomOf"/>
+ <item target="attr/layout_constraintBottom_toTopOf" value="@attr/layout_constraintBottom_toTopOf"/>
+ <item target="attr/layout_constraintEnd_toEndOf" value="@attr/layout_constraintEnd_toEndOf"/>
+ <item target="attr/layout_constraintEnd_toStartOf" value="@attr/layout_constraintEnd_toStartOf"/>
+ <item target="attr/layout_constraintGuide_begin" value="@attr/layout_constraintGuide_begin"/>
+ <item target="attr/layout_constraintGuide_end" value="@attr/layout_constraintGuide_end"/>
+ <item target="attr/layout_constraintHorizontal_bias" value="@attr/layout_constraintHorizontal_bias"/>
+ <item target="attr/layout_constraintLeft_toLeftOf" value="@attr/layout_constraintLeft_toLeftOf"/>
+ <item target="attr/layout_constraintLeft_toRightOf" value="@attr/layout_constraintLeft_toRightOf"/>
+ <item target="attr/layout_constraintRight_toLeftOf" value="@attr/layout_constraintRight_toLeftOf"/>
+ <item target="attr/layout_constraintRight_toRightOf" value="@attr/layout_constraintRight_toRightOf"/>
+ <item target="attr/layout_constraintStart_toEndOf" value="@attr/layout_constraintStart_toEndOf"/>
+ <item target="attr/layout_constraintStart_toStartOf" value="@attr/layout_constraintStart_toStartOf"/>
+ <item target="attr/layout_constraintTop_toBottomOf" value="@attr/layout_constraintTop_toBottomOf"/>
+ <item target="attr/layout_constraintTop_toTopOf" value="@attr/layout_constraintTop_toTopOf"/>
+ <item target="attr/layout_goneMarginBottom" value="@attr/layout_goneMarginBottom"/>
+ <item target="attr/layout_goneMarginEnd" value="@attr/layout_goneMarginEnd"/>
+ <item target="attr/layout_goneMarginLeft" value="@attr/layout_goneMarginLeft"/>
+ <item target="attr/layout_goneMarginRight" value="@attr/layout_goneMarginRight"/>
+ <item target="attr/layout_goneMarginStart" value="@attr/layout_goneMarginStart"/>
+ <item target="attr/layout_goneMarginTop" value="@attr/layout_goneMarginTop"/>
+ <item target="attr/state_ux_restricted" value="@attr/state_ux_restricted"/>
- <item target="drawable/car_ui_icon_arrow_back" value="@drawable/car_ui_icon_arrow_back"/>
- <item target="drawable/car_ui_toolbar_menu_item_icon_ripple" value="@drawable/car_ui_toolbar_menu_item_icon_ripple"/>
- <item target="drawable/car_ui_toolbar_menu_item_icon_background" value="@drawable/car_ui_toolbar_menu_item_icon_background"/>
- <item target="drawable/car_ui_toolbar_menu_item_icon_background" value="@drawable/car_ui_toolbar_menu_item_icon_background"/>
-
- <item target="style/Widget.CarUi" value="@style/Widget.CarUi"/>
- <item target="style/Widget.CarUi.Toolbar" value="@style/Widget.CarUi.Toolbar"/>
- <item target="style/Widget.CarUi.Toolbar.NavIconContainer" value="@style/Widget.CarUi.Toolbar.NavIconContainer"/>
- <item target="style/Widget.CarUi.Toolbar.NavIcon" value="@style/Widget.CarUi.Toolbar.NavIcon"/>
- <item target="style/Widget.CarUi.Toolbar.MenuItem" value="@style/Widget.CarUi.Toolbar.MenuItem"/>
- <item target="style/Widget.CarUi.Toolbar.MenuItem.IndividualContainer" value="@style/Widget.CarUi.Toolbar.MenuItem.IndividualContainer"/>
- <item target="style/Widget.CarUi.Button.Borderless.Colored" value="@style/Widget.CarUi.Button.Borderless.Colored"/>
- <item target="style/Widget.CarUi.Toolbar.TextButton" value="@style/Widget.CarUi.Toolbar.TextButton"/>
- <item target="style/Widget.CarUi.Toolbar.TextButton.WithIcon" value="@style/Widget.CarUi.Toolbar.TextButton.WithIcon"/>
- <item target="style/Widget.CarUi.SeekbarPreference" value="@style/Widget.CarUi.SeekbarPreference"/>
- <item target="style/Widget.CarUi.SeekbarPreference.Seekbar" value="@style/Widget.CarUi.SeekbarPreference.Seekbar"/>
-
- <item target="dimen/car_ui_toolbar_margin" value="@dimen/car_ui_toolbar_margin"/>
+ <item target="bool/car_ui_toolbar_logo_fills_nav_icon_space" value="@bool/car_ui_toolbar_logo_fills_nav_icon_space" />
+ <item target="bool/car_ui_toolbar_menuitem_individual_click_listeners" value="@bool/car_ui_toolbar_menuitem_individual_click_listeners" />
+ <item target="bool/car_ui_toolbar_nav_icon_reserve_space" value="@bool/car_ui_toolbar_nav_icon_reserve_space" />
+ <item target="bool/car_ui_toolbar_tab_flexible_layout" value="@bool/car_ui_toolbar_tab_flexible_layout" />
+ <item target="bool/car_ui_toolbar_tabs_on_second_row" value="@bool/car_ui_toolbar_tabs_on_second_row" />
<item target="color/car_ui_text_color_secondary" value="@color/car_ui_text_color_secondary"/>
<item target="color/car_ui_toolbar_menu_item_icon_background_color" value="@color/car_ui_toolbar_menu_item_icon_background_color"/>
<item target="color/car_ui_toolbar_menu_item_icon_color" value="@color/car_ui_toolbar_menu_item_icon_color"/>
+ <item target="dimen/car_ui_toolbar_margin" value="@dimen/car_ui_toolbar_margin"/>
+
<item target="drawable/car_ui_icon_arrow_back" value="@drawable/car_ui_icon_arrow_back"/>
+ <item target="drawable/car_ui_toolbar_menu_item_icon_background" value="@drawable/car_ui_toolbar_menu_item_icon_background"/>
<item target="drawable/car_ui_toolbar_menu_item_icon_ripple" value="@drawable/car_ui_toolbar_menu_item_icon_ripple"/>
- <item target="style/TextAppearance.CarUi" value="@style/TextAppearance.CarUi"/>
- <item target="style/TextAppearance.CarUi.Body1" value="@style/TextAppearance.CarUi.Body1"/>
- <item target="style/TextAppearance.CarUi.PreferenceTitle" value="@style/TextAppearance.CarUi.PreferenceTitle"/>
- <item target="style/TextAppearance.CarUi.Body3" value="@style/TextAppearance.CarUi.Body3"/>
- <item target="style/TextAppearance.CarUi.PreferenceSummary" value="@style/TextAppearance.CarUi.PreferenceSummary"/>
-
- <item target="style/Widget.CarUi" value="@style/Widget.CarUi"/>
- <item target="style/Widget.CarUi.Toolbar" value="@style/Widget.CarUi.Toolbar"/>
- <item target="style/Widget.CarUi.Toolbar.NavIconContainer" value="@style/Widget.CarUi.Toolbar.NavIconContainer"/>
- <item target="style/Widget.CarUi.Toolbar.NavIcon" value="@style/Widget.CarUi.Toolbar.NavIcon"/>
-
- <item target="dimen/car_ui_toolbar_margin" value="@dimen/car_ui_toolbar_margin"/>
- <item target="layout/car_ui_recycler_view" value="@layout/car_ui_recycler_view"/>
-
- <item target="bool/car_ui_toolbar_nav_icon_reserve_space" value="@bool/car_ui_toolbar_nav_icon_reserve_space" />
- <item target="bool/car_ui_toolbar_logo_fills_nav_icon_space" value="@bool/car_ui_toolbar_logo_fills_nav_icon_space" />
- <item target="bool/car_ui_toolbar_tab_flexible_layout" value="@bool/car_ui_toolbar_tab_flexible_layout" />
- <item target="bool/car_ui_toolbar_tabs_on_second_row" value="@bool/car_ui_toolbar_tabs_on_second_row" />
- <item target="bool/car_ui_toolbar_menuitem_individual_click_listeners" value="@bool/car_ui_toolbar_menuitem_individual_click_listeners" />
-
- <item target="id/car_ui_toolbar_background" value="@id/car_ui_toolbar_background" />
- <item target="id/car_ui_toolbar_nav_icon_container" value="@id/car_ui_toolbar_nav_icon_container" />
- <item target="id/car_ui_toolbar_nav_icon" value="@id/car_ui_toolbar_nav_icon" />
- <item target="id/car_ui_toolbar_logo" value="@id/car_ui_toolbar_logo" />
- <item target="id/car_ui_toolbar_title_logo_container" value="@id/car_ui_toolbar_title_logo_container" />
- <item target="id/car_ui_toolbar_title_logo" value="@id/car_ui_toolbar_title_logo" />
- <item target="id/car_ui_toolbar_title" value="@id/car_ui_toolbar_title" />
- <item target="id/car_ui_toolbar_title_container" value="@id/car_ui_toolbar_title_container" />
- <item target="id/car_ui_toolbar_subtitle" value="@id/car_ui_toolbar_subtitle" />
- <item target="id/car_ui_toolbar_tabs" value="@id/car_ui_toolbar_tabs" />
- <item target="id/car_ui_toolbar_menu_items_container" value="@id/car_ui_toolbar_menu_items_container" />
- <item target="id/car_ui_toolbar_search_view_container" value="@id/car_ui_toolbar_search_view_container" />
- <item target="id/car_ui_toolbar_progress_bar" value="@id/car_ui_toolbar_progress_bar" />
<item target="id/car_ui_base_layout_content_container" value="@id/car_ui_base_layout_content_container" />
- <item target="id/car_ui_toolbar_menu_item_icon_container" value="@id/car_ui_toolbar_menu_item_icon_container"/>
- <item target="id/car_ui_toolbar_menu_item_icon" value="@id/car_ui_toolbar_menu_item_icon"/>
- <item target="id/car_ui_toolbar_menu_item_switch" value="@id/car_ui_toolbar_menu_item_switch"/>
- <item target="id/car_ui_toolbar_menu_item_text" value="@id/car_ui_toolbar_menu_item_text"/>
- <item target="id/car_ui_toolbar_menu_item_text_with_icon" value="@id/car_ui_toolbar_menu_item_text_with_icon"/>
- <item target="id/seekbar" value="@id/seekbar"/>
- <item target="id/seekbar_value" value="@id/seekbar_value"/>
<item target="id/car_ui_recycler_view" value="@id/car_ui_recycler_view" />
- <item target="id/car_ui_scroll_bar" value="@id/car_ui_scroll_bar"/>
<item target="id/car_ui_scrollbar_page_down" value="@id/car_ui_scrollbar_page_down"/>
<item target="id/car_ui_scrollbar_page_up" value="@id/car_ui_scrollbar_page_up"/>
<item target="id/car_ui_scrollbar_thumb" value="@id/car_ui_scrollbar_thumb"/>
<item target="id/car_ui_scrollbar_track" value="@id/car_ui_scrollbar_track"/>
+ <item target="id/car_ui_scroll_bar" value="@id/car_ui_scroll_bar"/>
+ <item target="id/car_ui_toolbar_background" value="@id/car_ui_toolbar_background" />
+ <item target="id/car_ui_toolbar_logo" value="@id/car_ui_toolbar_logo" />
+ <item target="id/car_ui_toolbar_menu_item_icon_container" value="@id/car_ui_toolbar_menu_item_icon_container"/>
+ <item target="id/car_ui_toolbar_menu_item_icon" value="@id/car_ui_toolbar_menu_item_icon"/>
+ <item target="id/car_ui_toolbar_menu_items_container" value="@id/car_ui_toolbar_menu_items_container" />
+ <item target="id/car_ui_toolbar_menu_item_switch" value="@id/car_ui_toolbar_menu_item_switch"/>
+ <item target="id/car_ui_toolbar_menu_item_text" value="@id/car_ui_toolbar_menu_item_text"/>
+ <item target="id/car_ui_toolbar_menu_item_text_with_icon" value="@id/car_ui_toolbar_menu_item_text_with_icon"/>
+ <item target="id/car_ui_toolbar_nav_icon_container" value="@id/car_ui_toolbar_nav_icon_container" />
+ <item target="id/car_ui_toolbar_nav_icon" value="@id/car_ui_toolbar_nav_icon" />
+ <item target="id/car_ui_toolbar_progress_bar" value="@id/car_ui_toolbar_progress_bar" />
+ <item target="id/car_ui_toolbar_search_view_container" value="@id/car_ui_toolbar_search_view_container" />
+ <item target="id/car_ui_toolbar_subtitle" value="@id/car_ui_toolbar_subtitle" />
+ <item target="id/car_ui_toolbar_tabs" value="@id/car_ui_toolbar_tabs" />
+ <item target="id/car_ui_toolbar_title_container" value="@id/car_ui_toolbar_title_container" />
+ <item target="id/car_ui_toolbar_title_logo_container" value="@id/car_ui_toolbar_title_logo_container" />
+ <item target="id/car_ui_toolbar_title_logo" value="@id/car_ui_toolbar_title_logo" />
+ <item target="id/car_ui_toolbar_title" value="@id/car_ui_toolbar_title" />
+ <item target="id/seekbar" value="@id/seekbar"/>
+ <item target="id/seekbar_value" value="@id/seekbar_value"/>
- <item target="attr/layout_constraintGuide_begin" value="@attr/layout_constraintGuide_begin"/>
- <item target="attr/layout_constraintGuide_end" value="@attr/layout_constraintGuide_end"/>
- <item target="attr/layout_constraintStart_toStartOf" value="@attr/layout_constraintStart_toStartOf"/>
- <item target="attr/layout_constraintStart_toEndOf" value="@attr/layout_constraintStart_toEndOf"/>
- <item target="attr/layout_constraintEnd_toStartOf" value="@attr/layout_constraintEnd_toStartOf"/>
- <item target="attr/layout_constraintEnd_toEndOf" value="@attr/layout_constraintEnd_toEndOf"/>
- <item target="attr/layout_constraintLeft_toLeftOf" value="@attr/layout_constraintLeft_toLeftOf"/>
- <item target="attr/layout_constraintLeft_toRightOf" value="@attr/layout_constraintLeft_toRightOf"/>
- <item target="attr/layout_constraintRight_toLeftOf" value="@attr/layout_constraintRight_toLeftOf"/>
- <item target="attr/layout_constraintRight_toRightOf" value="@attr/layout_constraintRight_toRightOf"/>
- <item target="attr/layout_constraintTop_toTopOf" value="@attr/layout_constraintTop_toTopOf"/>
- <item target="attr/layout_constraintTop_toBottomOf" value="@attr/layout_constraintTop_toBottomOf"/>
- <item target="attr/layout_constraintBottom_toTopOf" value="@attr/layout_constraintBottom_toTopOf"/>
- <item target="attr/layout_constraintBottom_toBottomOf" value="@attr/layout_constraintBottom_toBottomOf"/>
- <item target="attr/layout_constraintHorizontal_bias" value="@attr/layout_constraintHorizontal_bias"/>
- <item target="attr/layout_goneMarginLeft" value="@attr/layout_goneMarginLeft"/>
- <item target="attr/layout_goneMarginLeft" value="@attr/layout_goneMarginRight"/>
- <item target="attr/layout_goneMarginLeft" value="@attr/layout_goneMarginTop"/>
- <item target="attr/layout_goneMarginLeft" value="@attr/layout_goneMarginBottom"/>
- <item target="attr/layout_goneMarginLeft" value="@attr/layout_goneMarginStart"/>
- <item target="attr/layout_goneMarginLeft" value="@attr/layout_goneMarginEnd"/>
- <item target="attr/state_ux_restricted" value="@attr/state_ux_restricted"/>
+ <item target="layout/car_ui_base_layout_toolbar" value="@layout/car_ui_base_layout_toolbar"/>
+ <item target="layout/car_ui_preference_widget_seekbar" value="@layout/car_ui_preference_widget_seekbar"/>
+ <item target="layout/car_ui_recycler_view" value="@layout/car_ui_recycler_view"/>
+ <item target="layout/car_ui_toolbar_menu_item_primary" value="@layout/car_ui_toolbar_menu_item_primary"/>
+ <item target="layout/car_ui_toolbar_menu_item" value="@layout/car_ui_toolbar_menu_item"/>
+ <item target="layout/car_ui_toolbar_two_row" value="@layout/car_ui_toolbar_two_row"/>
+ <item target="layout/car_ui_toolbar" value="@layout/car_ui_toolbar"/>
+
+ <item target="style/TextAppearance.CarUi.Body1" value="@style/TextAppearance.CarUi.Body1"/>
+ <item target="style/TextAppearance.CarUi.Body3" value="@style/TextAppearance.CarUi.Body3"/>
+ <item target="style/TextAppearance.CarUi.PreferenceSummary" value="@style/TextAppearance.CarUi.PreferenceSummary"/>
+ <item target="style/TextAppearance.CarUi.PreferenceTitle" value="@style/TextAppearance.CarUi.PreferenceTitle"/>
+ <item target="style/TextAppearance.CarUi" value="@style/TextAppearance.CarUi"/>
+ <item target="style/Widget.CarUi.Button.Borderless.Colored" value="@style/Widget.CarUi.Button.Borderless.Colored"/>
+ <item target="style/Widget.CarUi.SeekbarPreference.Seekbar" value="@style/Widget.CarUi.SeekbarPreference.Seekbar"/>
+ <item target="style/Widget.CarUi.SeekbarPreference" value="@style/Widget.CarUi.SeekbarPreference"/>
+ <item target="style/Widget.CarUi.Toolbar.MenuItem.IndividualContainer" value="@style/Widget.CarUi.Toolbar.MenuItem.IndividualContainer"/>
+ <item target="style/Widget.CarUi.Toolbar.MenuItem" value="@style/Widget.CarUi.Toolbar.MenuItem"/>
+ <item target="style/Widget.CarUi.Toolbar.NavIconContainer" value="@style/Widget.CarUi.Toolbar.NavIconContainer"/>
+ <item target="style/Widget.CarUi.Toolbar.NavIcon" value="@style/Widget.CarUi.Toolbar.NavIcon"/>
+ <item target="style/Widget.CarUi.Toolbar.TextButton" value="@style/Widget.CarUi.Toolbar.TextButton"/>
+ <item target="style/Widget.CarUi.Toolbar.TextButton.WithIcon" value="@style/Widget.CarUi.Toolbar.TextButton.WithIcon"/>
+ <item target="style/Widget.CarUi.Toolbar" value="@style/Widget.CarUi.Toolbar"/>
+ <item target="style/Widget.CarUi" value="@style/Widget.CarUi"/>
</overlay>
diff --git a/car-ui-lib/tests/apitest/current.xml b/car-ui-lib/tests/apitest/current.xml
index 769b040..674d8b4 100644
--- a/car-ui-lib/tests/apitest/current.xml
+++ b/car-ui-lib/tests/apitest/current.xml
@@ -15,6 +15,7 @@
<public type="bool" name="car_ui_ime_wide_screen_aligned_left"/>
<public type="bool" name="car_ui_ime_wide_screen_allow_app_hide_content_area"/>
<public type="bool" name="car_ui_list_item_single_line_title"/>
+ <public type="bool" name="car_ui_preference_list_instant_change_callback"/>
<public type="bool" name="car_ui_preference_list_show_full_screen"/>
<public type="bool" name="car_ui_preference_show_chevron"/>
<public type="bool" name="car_ui_scrollbar_enable"/>
@@ -82,6 +83,7 @@
<public type="dimen" name="car_ui_ime_wide_screen_error_text_size"/>
<public type="dimen" name="car_ui_ime_wide_screen_input_area_height"/>
<public type="dimen" name="car_ui_ime_wide_screen_input_area_margin_top"/>
+ <public type="dimen" name="car_ui_ime_wide_screen_input_area_padding_end"/>
<public type="dimen" name="car_ui_ime_wide_screen_input_edit_text_padding_left"/>
<public type="dimen" name="car_ui_ime_wide_screen_input_edit_text_padding_right"/>
<public type="dimen" name="car_ui_ime_wide_screen_input_edit_text_size"/>
@@ -244,26 +246,39 @@
<public type="drawable" name="car_ui_toolbar_menu_item_icon_ripple"/>
<public type="drawable" name="car_ui_toolbar_search_close_icon"/>
<public type="drawable" name="car_ui_toolbar_search_search_icon"/>
- <public type="id" name="action_container"/>
- <public type="id" name="action_container_touch_interceptor"/>
- <public type="id" name="action_divider"/>
<public type="id" name="action_widget_container"/>
- <public type="id" name="avatar_icon"/>
- <public type="id" name="body"/>
<public type="id" name="car_ui_alert_icon"/>
<public type="id" name="car_ui_alert_subtitle"/>
<public type="id" name="car_ui_alert_title"/>
<public type="id" name="car_ui_base_layout_content_container"/>
<public type="id" name="car_ui_closeKeyboard"/>
<public type="id" name="car_ui_contentAreaAutomotive"/>
+ <public type="id" name="car_ui_divider"/>
+ <public type="id" name="car_ui_first_action_container"/>
<public type="id" name="car_ui_focus_area"/>
<public type="id" name="car_ui_fullscreenArea"/>
<public type="id" name="car_ui_imeWideScreenInputArea"/>
<public type="id" name="car_ui_ime_surface"/>
<public type="id" name="car_ui_inputExtractActionAutomotive"/>
<public type="id" name="car_ui_inputExtractEditTextContainer"/>
+ <public type="id" name="car_ui_list_item_action_container"/>
+ <public type="id" name="car_ui_list_item_action_container_touch_interceptor"/>
+ <public type="id" name="car_ui_list_item_action_divider"/>
+ <public type="id" name="car_ui_list_item_avatar_icon"/>
+ <public type="id" name="car_ui_list_item_body"/>
+ <public type="id" name="car_ui_list_item_checkbox_widget"/>
+ <public type="id" name="car_ui_list_item_content_icon"/>
<public type="id" name="car_ui_list_item_end_guideline"/>
+ <public type="id" name="car_ui_list_item_icon"/>
+ <public type="id" name="car_ui_list_item_icon_container"/>
+ <public type="id" name="car_ui_list_item_radio_button_widget"/>
+ <public type="id" name="car_ui_list_item_reduced_touch_interceptor"/>
<public type="id" name="car_ui_list_item_start_guideline"/>
+ <public type="id" name="car_ui_list_item_supplemental_icon"/>
+ <public type="id" name="car_ui_list_item_switch_widget"/>
+ <public type="id" name="car_ui_list_item_text_container"/>
+ <public type="id" name="car_ui_list_item_title"/>
+ <public type="id" name="car_ui_list_item_touch_interceptor"/>
<public type="id" name="car_ui_list_limiting_message"/>
<public type="id" name="car_ui_preference_container_without_widget"/>
<public type="id" name="car_ui_preference_fragment_container"/>
@@ -273,6 +288,9 @@
<public type="id" name="car_ui_scrollbar_page_up"/>
<public type="id" name="car_ui_scrollbar_thumb"/>
<public type="id" name="car_ui_scrollbar_track"/>
+ <public type="id" name="car_ui_second_action_container"/>
+ <public type="id" name="car_ui_secondary_action"/>
+ <public type="id" name="car_ui_secondary_action_concrete"/>
<public type="id" name="car_ui_toolbar"/>
<public type="id" name="car_ui_toolbar_background"/>
<public type="id" name="car_ui_toolbar_bottom_guideline"/>
@@ -312,17 +330,11 @@
<public type="id" name="car_ui_wideScreenExtractedTextIcon"/>
<public type="id" name="car_ui_wideScreenInputArea"/>
<public type="id" name="car_ui_wideScreenSearchResultList"/>
- <public type="id" name="checkbox_widget"/>
<public type="id" name="container"/>
- <public type="id" name="content_icon"/>
- <public type="id" name="icon"/>
- <public type="id" name="icon_container"/>
<public type="id" name="list"/>
<public type="id" name="nested_recycler_view_layout"/>
<public type="id" name="radio_button"/>
- <public type="id" name="radio_button_widget"/>
<public type="id" name="recycler_view"/>
- <public type="id" name="reduced_touch_interceptor"/>
<public type="id" name="search"/>
<public type="id" name="seek_bar"/>
<public type="id" name="seek_bar_text_left"/>
@@ -331,14 +343,9 @@
<public type="id" name="seekbar"/>
<public type="id" name="seekbar_value"/>
<public type="id" name="spinner"/>
- <public type="id" name="supplemental_icon"/>
- <public type="id" name="switch_widget"/>
- <public type="id" name="text_container"/>
<public type="id" name="textbox"/>
- <public type="id" name="title"/>
<public type="id" name="title_template"/>
<public type="id" name="toolbar"/>
- <public type="id" name="touch_interceptor"/>
<public type="integer" name="car_ui_default_max_string_length"/>
<public type="integer" name="car_ui_focus_area_history_cache_type"/>
<public type="integer" name="car_ui_focus_area_history_expiration_period_ms"/>
@@ -365,6 +372,10 @@
<public type="layout" name="car_ui_preference_dropdown"/>
<public type="layout" name="car_ui_preference_fragment"/>
<public type="layout" name="car_ui_preference_fragment_with_toolbar"/>
+ <public type="layout" name="car_ui_preference_two_action_icon"/>
+ <public type="layout" name="car_ui_preference_two_action_switch"/>
+ <public type="layout" name="car_ui_preference_two_action_text"/>
+ <public type="layout" name="car_ui_preference_two_action_text_borderless"/>
<public type="layout" name="car_ui_preference_widget_checkbox"/>
<public type="layout" name="car_ui_preference_widget_seekbar"/>
<public type="layout" name="car_ui_preference_widget_switch"/>
diff --git a/connected-device-lib/Android.bp b/connected-device-lib/Android.bp
deleted file mode 100644
index 4383a20..0000000
--- a/connected-device-lib/Android.bp
+++ /dev/null
@@ -1,43 +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.
-//
-
-android_library {
- name: "connected-device-lib",
-
- srcs: ["src/**/*.java"],
-
- manifest: "AndroidManifest.xml",
-
- optimize: {
- enabled: false,
- },
-
- libs: ["android.car-system-stubs"],
-
- static_libs: [
- "androidx.room_room-runtime",
- "connected-device-protos",
- "encryption-runner",
- "guava",
- ],
-
- plugins: [
- "androidx.room_room-compiler-plugin",
- ],
-
- sdk_version: "system_current",
- min_sdk_version: "28",
-}
diff --git a/connected-device-lib/AndroidManifest.xml b/connected-device-lib/AndroidManifest.xml
deleted file mode 100644
index d02ffce..0000000
--- a/connected-device-lib/AndroidManifest.xml
+++ /dev/null
@@ -1,28 +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.
- -->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.car.connecteddevice">
-
- <!-- Needed for BLE scanning/advertising -->
- <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
- <uses-permission android:name="android.permission.BLUETOOTH"/>
- <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
-
- <!-- Needed for detecting foreground user -->
- <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>
- <uses-permission android:name="android.permission.MANAGE_USERS" />
-</manifest>
diff --git a/connected-device-lib/OWNERS b/connected-device-lib/OWNERS
deleted file mode 100644
index 108da4e..0000000
--- a/connected-device-lib/OWNERS
+++ /dev/null
@@ -1,5 +0,0 @@
-# People who can approve changes for submission.
-nicksauer@google.com
-ramperry@google.com
-ajchen@google.com
-danharms@google.com
diff --git a/connected-device-lib/proto/Android.bp b/connected-device-lib/proto/Android.bp
deleted file mode 100644
index c9dcb73..0000000
--- a/connected-device-lib/proto/Android.bp
+++ /dev/null
@@ -1,26 +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.
-//
-
-java_library_static {
- name: "connected-device-protos",
- host_supported: true,
- proto: {
- type: "lite",
- },
- srcs: ["*.proto"],
- jarjar_rules: "jarjar-rules.txt",
- sdk_version: "28",
-}
diff --git a/connected-device-lib/proto/device_message.proto b/connected-device-lib/proto/device_message.proto
deleted file mode 100644
index 79b98df..0000000
--- a/connected-device-lib/proto/device_message.proto
+++ /dev/null
@@ -1,39 +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.
- */
-
-syntax = "proto3";
-
-package com.android.car.connecteddevice.proto;
-
-import "packages/apps/Car/libs/connected-device-lib/proto/operation_type.proto";
-
-option java_package = "com.android.car.connecteddevice.StreamProtos";
-option java_outer_classname = "DeviceMessageProto";
-
-// A message between devices.
-message Message {
- // The operation that this message represents.
- OperationType operation = 1;
-
- // Whether the payload field is encrypted.
- bool is_payload_encrypted = 2;
-
- // Identifier of the intended recipient.
- bytes recipient = 3;
-
- // The bytes that represent the content for this message.
- bytes payload = 4;
-}
\ No newline at end of file
diff --git a/connected-device-lib/proto/jarjar-rules.txt b/connected-device-lib/proto/jarjar-rules.txt
deleted file mode 100644
index d27aecb..0000000
--- a/connected-device-lib/proto/jarjar-rules.txt
+++ /dev/null
@@ -1 +0,0 @@
-rule com.google.protobuf.** com.android.car.protobuf.@1
diff --git a/connected-device-lib/proto/operation_type.proto b/connected-device-lib/proto/operation_type.proto
deleted file mode 100644
index 3e38e27..0000000
--- a/connected-device-lib/proto/operation_type.proto
+++ /dev/null
@@ -1,44 +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.
- */
-
-syntax = "proto3";
-
-package com.android.car.connecteddevice.proto;
-
-option java_package = "com.android.car.connecteddevice.StreamProtos";
-option java_outer_classname = "OperationProto";
-
-// The different message types that indicate the content of the payload.
-//
-// Ensure that these values are positive to reduce incurring too many bytes
-// to encode.
-enum OperationType {
- // The contents of the payload are unknown.
- //
- // Note, this enum name is prefixed. See
- // go/proto-best-practices-checkers#enum-default-value-name-conflict
- OPERATION_TYPE_UNKNOWN = 0;
-
- // The payload contains handshake messages needed to set up encryption.
- ENCRYPTION_HANDSHAKE = 2;
-
- // The message is an acknowledgment of a previously received message. The
- // payload for this type should be empty.
- ACK = 3;
-
- // The payload contains a client-specific message.
- CLIENT_MESSAGE = 4;
-}
diff --git a/connected-device-lib/proto/packet.proto b/connected-device-lib/proto/packet.proto
deleted file mode 100644
index f761622..0000000
--- a/connected-device-lib/proto/packet.proto
+++ /dev/null
@@ -1,38 +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.
- */
-
-syntax = "proto3";
-
-package com.android.car.connecteddevice.proto;
-
-option java_package = "com.android.car.connecteddevice.StreamProtos";
-option java_outer_classname = "PacketProto";
-
-// A packet across a stream.
-message Packet {
- // A 1-based packet number. The first message will have a value of "1" rather
- // than "0".
- fixed32 packet_number = 1;
-
- // The total number of packets in the message stream.
- int32 total_packets = 2;
-
- // Id of message for reassembly on other side
- int32 message_id = 3;
-
- // The bytes that represent the message content for this packet.
- bytes payload = 4;
-}
diff --git a/connected-device-lib/proto/version_exchange.proto b/connected-device-lib/proto/version_exchange.proto
deleted file mode 100644
index a7c3493..0000000
--- a/connected-device-lib/proto/version_exchange.proto
+++ /dev/null
@@ -1,36 +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.
- */
-
-syntax = "proto3";
-
-package com.android.car.connecteddevice.proto;
-
-option java_package = "com.android.car.connecteddevice.StreamProtos";
-option java_outer_classname = "VersionExchangeProto";
-
-message VersionExchange {
- // Minimum supported protobuf version.
- int32 minSupportedMessagingVersion = 1;
-
- // Maximum supported protobuf version.
- int32 maxSupportedMessagingVersion = 2;
-
- // Minimum supported version of the encryption engine.
- int32 minSupportedSecurityVersion = 3;
-
- // Maximum supported version of the encryption engine.
- int32 maxSupportedSecurityVersion = 4;
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/AssociationCallback.java b/connected-device-lib/src/com/android/car/connecteddevice/AssociationCallback.java
deleted file mode 100644
index 7697131..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/AssociationCallback.java
+++ /dev/null
@@ -1,56 +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.connecteddevice;
-
-import androidx.annotation.NonNull;
-
-/** Callbacks that will be invoked during associating a new client. */
-public interface AssociationCallback {
-
- /**
- * Invoked when IHU starts advertising with its device name for association successfully.
- *
- * @param deviceName The device name to identify the car.
- */
- default void onAssociationStartSuccess(String deviceName) {}
-
- /** Invoked when IHU failed to start advertising for association. */
- default void onAssociationStartFailure() {}
-
- /**
- * Invoked when a {@link ConnectedDeviceManager.DeviceError} has been encountered in attempting
- * to associate a new device.
- *
- * @param error The failure indication.
- */
- default void onAssociationError(@ConnectedDeviceManager.DeviceError int error) {}
-
- /**
- * Invoked when a verification code needs to be displayed. The user needs to confirm, and
- * then call {@link ConnectedDeviceManager#notifyOutOfBandAccepted()}.
- *
- * @param code The verification code.
- */
- default void onVerificationCodeAvailable(@NonNull String code) {}
-
- /**
- * Invoked when the association has completed.
- *
- * @param deviceId The id of the newly associated device.
- */
- default void onAssociationCompleted(@NonNull String deviceId) {}
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/ConnectedDeviceManager.java b/connected-device-lib/src/com/android/car/connecteddevice/ConnectedDeviceManager.java
deleted file mode 100644
index b1397e2..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/ConnectedDeviceManager.java
+++ /dev/null
@@ -1,905 +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.connecteddevice;
-
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-import static com.android.car.connecteddevice.util.SafeLog.logw;
-
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-
-import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-
-import com.android.car.connecteddevice.connection.CarBluetoothManager;
-import com.android.car.connecteddevice.connection.DeviceMessage;
-import com.android.car.connecteddevice.model.AssociatedDevice;
-import com.android.car.connecteddevice.model.ConnectedDevice;
-import com.android.car.connecteddevice.model.OobEligibleDevice;
-import com.android.car.connecteddevice.oob.BluetoothRfcommChannel;
-import com.android.car.connecteddevice.oob.OobChannel;
-import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
-import com.android.car.connecteddevice.storage.ConnectedDeviceStorage.AssociatedDeviceCallback;
-import com.android.car.connecteddevice.util.ByteUtils;
-import com.android.car.connecteddevice.util.EventLog;
-import com.android.car.connecteddevice.util.ThreadSafeCallbacks;
-
-import java.lang.annotation.Retention;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.CopyOnWriteArraySet;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.function.Consumer;
-
-/** Manager of devices connected to the car. */
-public class ConnectedDeviceManager {
-
- private static final String TAG = "ConnectedDeviceManager";
-
- // Device name length is limited by available bytes in BLE advertisement data packet.
- //
- // BLE advertisement limits data packet length to 31
- // Currently we send:
- // - 18 bytes for 16 chars UUID: 16 bytes + 2 bytes for header;
- // - 3 bytes for advertisement being connectable;
- // which leaves 10 bytes.
- // Subtracting 2 bytes used by header, we have 8 bytes for device name.
- private static final int DEVICE_NAME_LENGTH_LIMIT = 8;
-
- private final ConnectedDeviceStorage mStorage;
-
- private final CarBluetoothManager mCarBluetoothManager;
-
- private final ThreadSafeCallbacks<DeviceAssociationCallback> mDeviceAssociationCallbacks =
- new ThreadSafeCallbacks<>();
-
- private final ThreadSafeCallbacks<ConnectionCallback> mActiveUserConnectionCallbacks =
- new ThreadSafeCallbacks<>();
-
- private final ThreadSafeCallbacks<ConnectionCallback> mAllUserConnectionCallbacks =
- new ThreadSafeCallbacks<>();
-
- // deviceId -> (recipientId -> callbacks)
- private final Map<String, Map<UUID, ThreadSafeCallbacks<DeviceCallback>>> mDeviceCallbacks =
- new ConcurrentHashMap<>();
-
- // deviceId -> device
- private final Map<String, ConnectedDevice> mConnectedDevices =
- new ConcurrentHashMap<>();
-
- // recipientId -> (deviceId -> message bytes)
- private final Map<UUID, Map<String, List<byte[]>>> mRecipientMissedMessages =
- new ConcurrentHashMap<>();
-
- // Recipient ids that received multiple callback registrations indicate that the recipient id
- // has been compromised. Another party now has access the messages intended for that recipient.
- // As a safeguard, that recipient id will be added to this list and blocked from further
- // callback notifications.
- private final Set<UUID> mBlacklistedRecipients = new CopyOnWriteArraySet<>();
-
- private final AtomicBoolean mIsConnectingToUserDevice = new AtomicBoolean(false);
-
- private final AtomicBoolean mHasStarted = new AtomicBoolean(false);
-
- private String mNameForAssociation;
-
- private AssociationCallback mAssociationCallback;
-
- private MessageDeliveryDelegate mMessageDeliveryDelegate;
-
- private OobChannel mOobChannel;
-
- @Retention(SOURCE)
- @IntDef({
- DEVICE_ERROR_INVALID_HANDSHAKE,
- DEVICE_ERROR_INVALID_MSG,
- DEVICE_ERROR_INVALID_DEVICE_ID,
- DEVICE_ERROR_INVALID_VERIFICATION,
- DEVICE_ERROR_INVALID_CHANNEL_STATE,
- DEVICE_ERROR_INVALID_ENCRYPTION_KEY,
- DEVICE_ERROR_STORAGE_FAILURE,
- DEVICE_ERROR_INVALID_SECURITY_KEY,
- DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED,
- DEVICE_ERROR_UNEXPECTED_DISCONNECTION
- })
- public @interface DeviceError {}
-
- public static final int DEVICE_ERROR_INVALID_HANDSHAKE = 0;
- public static final int DEVICE_ERROR_INVALID_MSG = 1;
- public static final int DEVICE_ERROR_INVALID_DEVICE_ID = 2;
- public static final int DEVICE_ERROR_INVALID_VERIFICATION = 3;
- public static final int DEVICE_ERROR_INVALID_CHANNEL_STATE = 4;
- public static final int DEVICE_ERROR_INVALID_ENCRYPTION_KEY = 5;
- public static final int DEVICE_ERROR_STORAGE_FAILURE = 6;
- public static final int DEVICE_ERROR_INVALID_SECURITY_KEY = 7;
- public static final int DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED = 8;
- public static final int DEVICE_ERROR_UNEXPECTED_DISCONNECTION = 9;
-
- public ConnectedDeviceManager(@NonNull CarBluetoothManager carBluetoothManager,
- @NonNull ConnectedDeviceStorage storage) {
- Executor callbackExecutor = Executors.newSingleThreadExecutor();
- mStorage = storage;
- mCarBluetoothManager = carBluetoothManager;
- mCarBluetoothManager.registerCallback(generateCarBleCallback(carBluetoothManager),
- callbackExecutor);
- mStorage.setAssociatedDeviceCallback(mAssociatedDeviceCallback);
- logd(TAG, "ConnectedDeviceManager created successfully.");
- }
-
- /**
- * Start internal processes and begin discovering devices. Must be called before any
- * connections can be made using {@link #connectToActiveUserDevice()}.
- */
- public void start() {
- if (mHasStarted.getAndSet(true)) {
- reset();
- } else {
- logd(TAG, "Starting ConnectedDeviceManager.");
- EventLog.onConnectedDeviceManagerStarted();
- }
- mCarBluetoothManager.start();
- connectToActiveUserDevice();
- }
-
- /** Reset internal processes and disconnect any active connections. */
- public void reset() {
- logd(TAG, "Resetting ConnectedDeviceManager.");
- for (ConnectedDevice device : mConnectedDevices.values()) {
- removeConnectedDevice(device.getDeviceId());
- }
- mCarBluetoothManager.stop();
- mIsConnectingToUserDevice.set(false);
- if (mOobChannel != null) {
- mOobChannel.interrupt();
- mOobChannel = null;
- }
- mAssociationCallback = null;
- }
-
- /** Returns {@link List<ConnectedDevice>} of devices currently connected. */
- @NonNull
- public List<ConnectedDevice> getActiveUserConnectedDevices() {
- List<ConnectedDevice> activeUserConnectedDevices = new ArrayList<>();
- for (ConnectedDevice device : mConnectedDevices.values()) {
- if (device.isAssociatedWithActiveUser()) {
- activeUserConnectedDevices.add(device);
- }
- }
- logd(TAG, "Returned " + activeUserConnectedDevices.size() + " active user devices.");
- return activeUserConnectedDevices;
- }
-
- /**
- * Register a callback for triggered associated device related events.
- *
- * @param callback {@link DeviceAssociationCallback} to register.
- * @param executor {@link Executor} to execute triggers on.
- */
- public void registerDeviceAssociationCallback(@NonNull DeviceAssociationCallback callback,
- @NonNull Executor executor) {
- mDeviceAssociationCallbacks.add(callback, executor);
- }
-
- /**
- * Unregister a device association callback.
- *
- * @param callback {@link DeviceAssociationCallback} to unregister.
- */
- public void unregisterDeviceAssociationCallback(@NonNull DeviceAssociationCallback callback) {
- mDeviceAssociationCallbacks.remove(callback);
- }
-
- /**
- * Register a callback for manager triggered connection events for only the currently active
- * user's devices.
- *
- * @param callback {@link ConnectionCallback} to register.
- * @param executor {@link Executor} to execute triggers on.
- */
- public void registerActiveUserConnectionCallback(@NonNull ConnectionCallback callback,
- @NonNull Executor executor) {
- mActiveUserConnectionCallbacks.add(callback, executor);
- }
-
- /**
- * Unregister a connection callback from manager.
- *
- * @param callback {@link ConnectionCallback} to unregister.
- */
- public void unregisterConnectionCallback(ConnectionCallback callback) {
- mActiveUserConnectionCallbacks.remove(callback);
- mAllUserConnectionCallbacks.remove(callback);
- }
-
- /** Connect to a device for the active user if available. */
- @VisibleForTesting
- void connectToActiveUserDevice() {
- Executors.defaultThreadFactory().newThread(() -> {
- logd(TAG, "Received request to connect to active user's device.");
- connectToActiveUserDeviceInternal();
- }).start();
- }
-
- private void connectToActiveUserDeviceInternal() {
- try {
- boolean isLockAcquired = mIsConnectingToUserDevice.compareAndSet(false, true);
- if (!isLockAcquired) {
- logd(TAG, "A request has already been made to connect to this user's device. "
- + "Ignoring redundant request.");
- return;
- }
- List<AssociatedDevice> userDevices = mStorage.getActiveUserAssociatedDevices();
- if (userDevices.isEmpty()) {
- logw(TAG, "No devices associated with active user. Ignoring.");
- mIsConnectingToUserDevice.set(false);
- return;
- }
-
- // Only currently support one device per user for fast association, so take the
- // first one.
- AssociatedDevice userDevice = userDevices.get(0);
- if (!userDevice.isConnectionEnabled()) {
- logd(TAG, "Connection is disabled on device " + userDevice + ".");
- mIsConnectingToUserDevice.set(false);
- return;
- }
- if (mConnectedDevices.containsKey(userDevice.getDeviceId())) {
- logd(TAG, "Device has already been connected. No need to attempt connection "
- + "again.");
- mIsConnectingToUserDevice.set(false);
- return;
- }
- EventLog.onStartDeviceSearchStarted();
- mCarBluetoothManager.connectToDevice(UUID.fromString(userDevice.getDeviceId()));
- } catch (Exception e) {
- loge(TAG, "Exception while attempting connection with active user's device.", e);
- }
- }
-
- /**
- * Start the association with a new device.
- *
- * @param callback Callback for association events.
- */
- public void startAssociation(@NonNull AssociationCallback callback) {
- mAssociationCallback = callback;
- Executors.defaultThreadFactory().newThread(() -> {
- logd(TAG, "Received request to start association.");
- mCarBluetoothManager.startAssociation(getNameForAssociation(),
- mInternalAssociationCallback);
- }).start();
- }
-
- /**
- * Start association with an out of band device.
- *
- * @param device The out of band eligible device.
- * @param callback Callback for association events.
- */
- public void startOutOfBandAssociation(@NonNull OobEligibleDevice device,
- @NonNull AssociationCallback callback) {
- logd(TAG, "Received request to start out of band association.");
- mAssociationCallback = callback;
- mOobChannel = new BluetoothRfcommChannel();
- mOobChannel.completeOobDataExchange(device, new OobChannel.Callback() {
- @Override
- public void onOobExchangeSuccess() {
- logd(TAG, "Out of band exchange succeeded. Proceeding to association with device.");
- Executors.defaultThreadFactory().newThread(() -> {
- mCarBluetoothManager.startOutOfBandAssociation(
- getNameForAssociation(),
- mOobChannel, mInternalAssociationCallback);
- }).start();
- }
-
- @Override
- public void onOobExchangeFailure() {
- loge(TAG, "Out of band exchange failed.");
- mInternalAssociationCallback.onAssociationError(
- DEVICE_ERROR_INVALID_ENCRYPTION_KEY);
- mOobChannel = null;
- mAssociationCallback = null;
- }
- });
- }
-
- /** Stop the association with any device. */
- public void stopAssociation(@NonNull AssociationCallback callback) {
- if (mAssociationCallback != callback) {
- logd(TAG, "Stop association called with unrecognized callback. Ignoring.");
- return;
- }
- logd(TAG, "Stopping association.");
- mAssociationCallback = null;
- mCarBluetoothManager.stopAssociation();
- if (mOobChannel != null) {
- mOobChannel.interrupt();
- }
- mOobChannel = null;
- }
-
- /**
- * Get a list of associated devices for the given user.
- *
- * @return Associated device list.
- */
- @NonNull
- public List<AssociatedDevice> getActiveUserAssociatedDevices() {
- return mStorage.getActiveUserAssociatedDevices();
- }
-
- /** Notify that the user has accepted a pairing code or any out-of-band confirmation. */
- public void notifyOutOfBandAccepted() {
- mCarBluetoothManager.notifyOutOfBandAccepted();
- }
-
- /**
- * Remove the associated device with the given device identifier for the current user.
- *
- * @param deviceId Device identifier.
- */
- public void removeActiveUserAssociatedDevice(@NonNull String deviceId) {
- mStorage.removeAssociatedDeviceForActiveUser(deviceId);
- disconnectDevice(deviceId);
- }
-
- /**
- * Enable connection on an associated device.
- *
- * @param deviceId Device identifier.
- */
- public void enableAssociatedDeviceConnection(@NonNull String deviceId) {
- logd(TAG, "enableAssociatedDeviceConnection() called on " + deviceId);
- mStorage.updateAssociatedDeviceConnectionEnabled(deviceId,
- /* isConnectionEnabled= */ true);
-
- connectToActiveUserDevice();
- }
-
- /**
- * Disable connection on an associated device.
- *
- * @param deviceId Device identifier.
- */
- public void disableAssociatedDeviceConnection(@NonNull String deviceId) {
- logd(TAG, "disableAssociatedDeviceConnection() called on " + deviceId);
- mStorage.updateAssociatedDeviceConnectionEnabled(deviceId,
- /* isConnectionEnabled= */ false);
- disconnectDevice(deviceId);
- }
-
- private void disconnectDevice(String deviceId) {
- ConnectedDevice device = mConnectedDevices.get(deviceId);
- if (device != null) {
- mCarBluetoothManager.disconnectDevice(deviceId);
- removeConnectedDevice(deviceId);
- }
- }
-
- /**
- * Register a callback for a specific device and recipient.
- *
- * @param device {@link ConnectedDevice} to register triggers on.
- * @param recipientId {@link UUID} to register as recipient of.
- * @param callback {@link DeviceCallback} to register.
- * @param executor {@link Executor} on which to execute callback.
- */
- public void registerDeviceCallback(@NonNull ConnectedDevice device, @NonNull UUID recipientId,
- @NonNull DeviceCallback callback, @NonNull Executor executor) {
- if (isRecipientBlacklisted(recipientId)) {
- notifyOfBlacklisting(device, recipientId, callback, executor);
- return;
- }
- logd(TAG, "New callback registered on device " + device.getDeviceId() + " for recipient "
- + recipientId);
- String deviceId = device.getDeviceId();
- Map<UUID, ThreadSafeCallbacks<DeviceCallback>> recipientCallbacks =
- mDeviceCallbacks.computeIfAbsent(deviceId, key -> new ConcurrentHashMap<>());
-
- // Device already has a callback registered with this recipient UUID. For the
- // protection of the user, this UUID is now blacklisted from future subscriptions
- // and the original subscription is notified and removed.
- if (recipientCallbacks.containsKey(recipientId)) {
- blacklistRecipient(deviceId, recipientId);
- notifyOfBlacklisting(device, recipientId, callback, executor);
- return;
- }
-
- ThreadSafeCallbacks<DeviceCallback> newCallbacks = new ThreadSafeCallbacks<>();
- newCallbacks.add(callback, executor);
- recipientCallbacks.put(recipientId, newCallbacks);
-
- List<byte[]> messages = popMissedMessages(recipientId, device.getDeviceId());
- if (messages != null) {
- for (byte[] message : messages) {
- newCallbacks.invoke(deviceCallback ->
- deviceCallback.onMessageReceived(device, message));
- }
- }
- }
-
- /**
- * Set the delegate for message delivery operations.
- *
- * @param delegate The {@link MessageDeliveryDelegate} to set. {@code null} to unset.
- */
- public void setMessageDeliveryDelegate(@Nullable MessageDeliveryDelegate delegate) {
- mMessageDeliveryDelegate = delegate;
- }
-
- private void notifyOfBlacklisting(@NonNull ConnectedDevice device, @NonNull UUID recipientId,
- @NonNull DeviceCallback callback, @NonNull Executor executor) {
- loge(TAG, "Multiple callbacks registered for recipient " + recipientId + "! Your "
- + "recipient id is no longer secure and has been blocked from future use.");
- executor.execute(() ->
- callback.onDeviceError(device, DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED));
- }
-
- private void saveMissedMessage(@NonNull String deviceId, @NonNull UUID recipientId,
- @NonNull byte[] message) {
- // Store last message in case recipient registers callbacks in the future.
- logd(TAG, "No recipient registered for device " + deviceId + " and recipient "
- + recipientId + " combination. Saving message.");
- mRecipientMissedMessages.computeIfAbsent(recipientId, __ -> new HashMap<>())
- .computeIfAbsent(deviceId, __ -> new ArrayList<>()).add(message);
- }
-
- /**
- * Remove the last message sent for this device prior to a {@link DeviceCallback} being
- * registered.
- *
- * @param recipientId Recipient's id
- * @param deviceId Device id
- * @return The missed {@code byte[]} messages, or {@code null} if no messages were
- * missed.
- */
- @Nullable
- private List<byte[]> popMissedMessages(@NonNull UUID recipientId, @NonNull String deviceId) {
- Map<String, List<byte[]>> missedMessages = mRecipientMissedMessages.get(recipientId);
- if (missedMessages == null) {
- return null;
- }
-
- return missedMessages.remove(deviceId);
- }
-
- /**
- * Unregister callback from device events.
- *
- * @param device {@link ConnectedDevice} callback was registered on.
- * @param recipientId {@link UUID} callback was registered under.
- * @param callback {@link DeviceCallback} to unregister.
- */
- public void unregisterDeviceCallback(@NonNull ConnectedDevice device,
- @NonNull UUID recipientId, @NonNull DeviceCallback callback) {
- logd(TAG, "Device callback unregistered on device " + device.getDeviceId() + " for "
- + "recipient " + recipientId + ".");
-
- Map<UUID, ThreadSafeCallbacks<DeviceCallback>> recipientCallbacks =
- mDeviceCallbacks.get(device.getDeviceId());
- if (recipientCallbacks == null) {
- return;
- }
- ThreadSafeCallbacks<DeviceCallback> callbacks = recipientCallbacks.get(recipientId);
- if (callbacks == null) {
- return;
- }
-
- callbacks.remove(callback);
- if (callbacks.size() == 0) {
- recipientCallbacks.remove(recipientId);
- }
- }
-
- /**
- * Securely send message to a device.
- *
- * @param device {@link ConnectedDevice} to send the message to.
- * @param recipientId Recipient {@link UUID}.
- * @param message Message to send.
- * @throws IllegalStateException Secure channel has not been established.
- */
- public void sendMessageSecurely(@NonNull ConnectedDevice device, @NonNull UUID recipientId,
- @NonNull byte[] message) throws IllegalStateException {
- sendMessage(device, recipientId, message, /* isEncrypted= */ true);
- }
-
- /**
- * Send an unencrypted message to a device.
- *
- * @param device {@link ConnectedDevice} to send the message to.
- * @param recipientId Recipient {@link UUID}.
- * @param message Message to send.
- */
- public void sendMessageUnsecurely(@NonNull ConnectedDevice device, @NonNull UUID recipientId,
- @NonNull byte[] message) {
- sendMessage(device, recipientId, message, /* isEncrypted= */ false);
- }
-
- private void sendMessage(@NonNull ConnectedDevice device, @NonNull UUID recipientId,
- @NonNull byte[] message, boolean isEncrypted) throws IllegalStateException {
- String deviceId = device.getDeviceId();
- logd(TAG, "Sending new message to device " + deviceId + " for " + recipientId
- + " containing " + message.length + ". Message will be sent securely: "
- + isEncrypted + ".");
-
- ConnectedDevice connectedDevice = mConnectedDevices.get(deviceId);
- if (connectedDevice == null) {
- loge(TAG, "Attempted to send message to unknown device " + deviceId + ". Ignoring.");
- return;
- }
-
- if (isEncrypted && !connectedDevice.hasSecureChannel()) {
- throw new IllegalStateException("Cannot send a message securely to device that has not "
- + "established a secure channel.");
- }
-
- mCarBluetoothManager.sendMessage(deviceId, new DeviceMessage(recipientId, isEncrypted,
- message));
- }
-
- private boolean isRecipientBlacklisted(UUID recipientId) {
- return mBlacklistedRecipients.contains(recipientId);
- }
-
- private void blacklistRecipient(@NonNull String deviceId, @NonNull UUID recipientId) {
- Map<UUID, ThreadSafeCallbacks<DeviceCallback>> recipientCallbacks =
- mDeviceCallbacks.get(deviceId);
- if (recipientCallbacks == null) {
- // Should never happen, but null-safety check.
- return;
- }
-
- ThreadSafeCallbacks<DeviceCallback> existingCallback = recipientCallbacks.get(recipientId);
- if (existingCallback == null) {
- // Should never happen, but null-safety check.
- return;
- }
-
- ConnectedDevice connectedDevice = mConnectedDevices.get(deviceId);
- if (connectedDevice != null) {
- recipientCallbacks.get(recipientId).invoke(
- callback ->
- callback.onDeviceError(connectedDevice,
- DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED)
- );
- }
-
- recipientCallbacks.remove(recipientId);
- mBlacklistedRecipients.add(recipientId);
- }
-
- @VisibleForTesting
- void addConnectedDevice(@NonNull String deviceId) {
- if (mConnectedDevices.containsKey(deviceId)) {
- // Device already connected. No-op until secure channel established.
- return;
- }
- logd(TAG, "New device with id " + deviceId + " connected.");
- ConnectedDevice connectedDevice = new ConnectedDevice(
- deviceId,
- /* deviceName= */ null,
- mStorage.getActiveUserAssociatedDeviceIds().contains(deviceId),
- /* hasSecureChannel= */ false
- );
-
- mConnectedDevices.put(deviceId, connectedDevice);
- invokeConnectionCallbacks(connectedDevice.isAssociatedWithActiveUser(),
- callback -> callback.onDeviceConnected(connectedDevice));
- }
-
- @VisibleForTesting
- void removeConnectedDevice(@NonNull String deviceId) {
- logd(TAG, "Device " + deviceId + " disconnected.");
- ConnectedDevice connectedDevice = mConnectedDevices.get(deviceId);
- mIsConnectingToUserDevice.set(false);
- if (connectedDevice == null) {
- return;
- }
-
- mConnectedDevices.remove(deviceId);
- boolean isAssociated = connectedDevice.isAssociatedWithActiveUser();
- invokeConnectionCallbacks(isAssociated,
- callback -> callback.onDeviceDisconnected(connectedDevice));
-
- if (isAssociated || mConnectedDevices.isEmpty()) {
- // Try to regain connection to active user's device.
- connectToActiveUserDevice();
- }
- }
-
- @VisibleForTesting
- void onSecureChannelEstablished(@NonNull String deviceId) {
- if (mConnectedDevices.get(deviceId) == null) {
- loge(TAG, "Secure channel established on unknown device " + deviceId + ".");
- return;
- }
- ConnectedDevice connectedDevice = mConnectedDevices.get(deviceId);
- ConnectedDevice updatedConnectedDevice = new ConnectedDevice(connectedDevice.getDeviceId(),
- connectedDevice.getDeviceName(), connectedDevice.isAssociatedWithActiveUser(),
- /* hasSecureChannel= */ true);
-
- boolean notifyCallbacks = mConnectedDevices.get(deviceId) != null;
-
- // TODO (b/143088482) Implement interrupt
- // Ignore if central already holds the active device connection and interrupt the
- // connection.
-
- mConnectedDevices.put(deviceId, updatedConnectedDevice);
- logd(TAG, "Secure channel established to " + deviceId + " . Notifying callbacks: "
- + notifyCallbacks + ".");
- if (notifyCallbacks) {
- notifyAllDeviceCallbacks(deviceId,
- callback -> callback.onSecureChannelEstablished(updatedConnectedDevice));
- }
- }
-
- @VisibleForTesting
- void onMessageReceived(@NonNull String deviceId, @NonNull DeviceMessage message) {
- logd(TAG, "New message received from device " + deviceId + " intended for "
- + message.getRecipient() + " containing " + message.getMessage().length
- + " bytes.");
-
- ConnectedDevice connectedDevice = mConnectedDevices.get(deviceId);
- if (connectedDevice == null) {
- logw(TAG, "Received message from unknown device " + deviceId + "or to unknown "
- + "recipient " + message.getRecipient() + ".");
- return;
- }
-
- if (mMessageDeliveryDelegate != null
- && !mMessageDeliveryDelegate.shouldDeliverMessageForDevice(connectedDevice)) {
- logw(TAG, "The message delegate has rejected this message. It will not be "
- + "delivered to the intended recipient.");
- return;
- }
-
- UUID recipientId = message.getRecipient();
- Map<UUID, ThreadSafeCallbacks<DeviceCallback>> deviceCallbacks =
- mDeviceCallbacks.get(deviceId);
- if (deviceCallbacks == null) {
- saveMissedMessage(deviceId, recipientId, message.getMessage());
- return;
- }
- ThreadSafeCallbacks<DeviceCallback> recipientCallbacks =
- deviceCallbacks.get(recipientId);
- if (recipientCallbacks == null) {
- saveMissedMessage(deviceId, recipientId, message.getMessage());
- return;
- }
-
- recipientCallbacks.invoke(
- callback -> callback.onMessageReceived(connectedDevice, message.getMessage()));
- }
-
- @VisibleForTesting
- void deviceErrorOccurred(@NonNull String deviceId) {
- ConnectedDevice connectedDevice = mConnectedDevices.get(deviceId);
- if (connectedDevice == null) {
- logw(TAG, "Failed to establish secure channel on unknown device " + deviceId + ".");
- return;
- }
-
- notifyAllDeviceCallbacks(deviceId, callback -> callback.onDeviceError(connectedDevice,
- DEVICE_ERROR_INVALID_SECURITY_KEY));
- }
-
- @VisibleForTesting
- void onAssociationCompleted(@NonNull String deviceId) {
- ConnectedDevice connectedDevice = mConnectedDevices.get(deviceId);
- if (connectedDevice == null) {
- return;
- }
-
- // The previous device is now obsolete and should be replaced with a new one properly
- // reflecting the state of belonging to the active user and notify features.
- if (connectedDevice.isAssociatedWithActiveUser()) {
- // Device was already marked as belonging to active user. No need to reissue callbacks.
- return;
- }
- removeConnectedDevice(deviceId);
- addConnectedDevice(deviceId);
- }
-
- @NonNull
- private List<String> getActiveUserDeviceIds() {
- return mStorage.getActiveUserAssociatedDeviceIds();
- }
-
- private void invokeConnectionCallbacks(boolean belongsToActiveUser,
- @NonNull Consumer<ConnectionCallback> notification) {
- logd(TAG, "Notifying connection callbacks for device belonging to active user "
- + belongsToActiveUser + ".");
- if (belongsToActiveUser) {
- mActiveUserConnectionCallbacks.invoke(notification);
- }
- mAllUserConnectionCallbacks.invoke(notification);
- }
-
- private void notifyAllDeviceCallbacks(@NonNull String deviceId,
- @NonNull Consumer<DeviceCallback> notification) {
- logd(TAG, "Notifying all device callbacks for device " + deviceId + ".");
- Map<UUID, ThreadSafeCallbacks<DeviceCallback>> deviceCallbacks =
- mDeviceCallbacks.get(deviceId);
- if (deviceCallbacks == null) {
- return;
- }
-
- for (ThreadSafeCallbacks<DeviceCallback> callbacks : deviceCallbacks.values()) {
- callbacks.invoke(notification);
- }
- }
-
- /**
- * Returns the name that should be used for the device during enrollment of a trusted device.
- *
- * <p>The returned name will be a combination of a prefix sysprop and randomized digits.
- */
- @NonNull
- private String getNameForAssociation() {
- if (mNameForAssociation == null) {
- mNameForAssociation = ByteUtils.generateRandomNumberString(DEVICE_NAME_LENGTH_LIMIT);
- }
- return mNameForAssociation;
- }
-
- @NonNull
- private CarBluetoothManager.Callback generateCarBleCallback(
- @NonNull CarBluetoothManager carBluetoothManager) {
- return new CarBluetoothManager.Callback() {
- @Override
- public void onDeviceConnected(String deviceId) {
- EventLog.onDeviceIdReceived();
- addConnectedDevice(deviceId);
- }
-
- @Override
- public void onDeviceDisconnected(String deviceId) {
- removeConnectedDevice(deviceId);
- }
-
- @Override
- public void onSecureChannelEstablished(String deviceId) {
- EventLog.onSecureChannelEstablished();
- ConnectedDeviceManager.this.onSecureChannelEstablished(deviceId);
- }
-
- @Override
- public void onMessageReceived(String deviceId, DeviceMessage message) {
- ConnectedDeviceManager.this.onMessageReceived(deviceId, message);
- }
-
- @Override
- public void onSecureChannelError(String deviceId) {
- deviceErrorOccurred(deviceId);
- }
- };
- }
-
- private final AssociationCallback mInternalAssociationCallback = new AssociationCallback() {
- @Override
- public void onAssociationStartSuccess(String deviceName) {
- if (mAssociationCallback != null) {
- mAssociationCallback.onAssociationStartSuccess(deviceName);
- }
- }
-
- @Override
- public void onAssociationStartFailure() {
- if (mAssociationCallback != null) {
- mAssociationCallback.onAssociationStartFailure();
- }
- }
-
- @Override
- public void onAssociationError(int error) {
- if (mAssociationCallback != null) {
- mAssociationCallback.onAssociationError(error);
- }
- }
-
- @Override
- public void onVerificationCodeAvailable(String code) {
- if (mAssociationCallback != null) {
- mAssociationCallback.onVerificationCodeAvailable(code);
- }
- }
-
- @Override
- public void onAssociationCompleted(String deviceId) {
- if (mAssociationCallback != null) {
- mAssociationCallback.onAssociationCompleted(deviceId);
- }
- ConnectedDeviceManager.this.onAssociationCompleted(deviceId);
- }
- };
-
- private final AssociatedDeviceCallback mAssociatedDeviceCallback =
- new AssociatedDeviceCallback() {
- @Override
- public void onAssociatedDeviceAdded(AssociatedDevice device) {
- mDeviceAssociationCallbacks.invoke(callback ->
- callback.onAssociatedDeviceAdded(device));
- }
-
- @Override
- public void onAssociatedDeviceRemoved(AssociatedDevice device) {
- mDeviceAssociationCallbacks.invoke(callback ->
- callback.onAssociatedDeviceRemoved(device));
- logd(TAG, "Successfully removed associated device " + device + ".");
- }
-
- @Override
- public void onAssociatedDeviceUpdated(AssociatedDevice device) {
- mDeviceAssociationCallbacks.invoke(callback ->
- callback.onAssociatedDeviceUpdated(device));
- }
- };
-
- /** Callback for triggered connection events from {@link ConnectedDeviceManager}. */
- public interface ConnectionCallback {
- /** Triggered when a new device has connected. */
- void onDeviceConnected(@NonNull ConnectedDevice device);
-
- /** Triggered when a device has disconnected. */
- void onDeviceDisconnected(@NonNull ConnectedDevice device);
- }
-
- /** Triggered device events for a connected device from {@link ConnectedDeviceManager}. */
- public interface DeviceCallback {
- /**
- * Triggered when secure channel has been established on a device. Encrypted messaging now
- * available.
- */
- void onSecureChannelEstablished(@NonNull ConnectedDevice device);
-
- /** Triggered when a new message is received from a device. */
- void onMessageReceived(@NonNull ConnectedDevice device, @NonNull byte[] message);
-
- /** Triggered when an error has occurred for a device. */
- void onDeviceError(@NonNull ConnectedDevice device, @DeviceError int error);
- }
-
- /** Callback for association device related events. */
- public interface DeviceAssociationCallback {
-
- /** Triggered when an associated device has been added. */
- void onAssociatedDeviceAdded(@NonNull AssociatedDevice device);
-
- /** Triggered when an associated device has been removed. */
- void onAssociatedDeviceRemoved(@NonNull AssociatedDevice device);
-
- /** Triggered when the name of an associated device has been updated. */
- void onAssociatedDeviceUpdated(@NonNull AssociatedDevice device);
- }
-
- /** Delegate for message delivery operations. */
- public interface MessageDeliveryDelegate {
-
- /** Indicate whether a message should be delivered for the specified device. */
- boolean shouldDeliverMessageForDevice(@NonNull ConnectedDevice device);
- }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/AssociationSecureChannel.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/AssociationSecureChannel.java
deleted file mode 100644
index 65a05da..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/AssociationSecureChannel.java
+++ /dev/null
@@ -1,226 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.connection;
-
-import static android.car.encryptionrunner.EncryptionRunnerFactory.EncryptionRunnerType;
-import static android.car.encryptionrunner.EncryptionRunnerFactory.newRunner;
-import static android.car.encryptionrunner.HandshakeMessage.HandshakeState;
-
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-
-import android.car.encryptionrunner.EncryptionRunner;
-import android.car.encryptionrunner.HandshakeException;
-import android.car.encryptionrunner.HandshakeMessage;
-import android.car.encryptionrunner.Key;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-
-import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
-import com.android.car.connecteddevice.util.ByteUtils;
-
-import java.security.InvalidParameterException;
-import java.util.Arrays;
-import java.util.UUID;
-
-/**
- * A secure channel established with the association flow.
- */
-public class AssociationSecureChannel extends SecureChannel {
-
- private static final String TAG = "AssociationSecureChannel";
-
- private static final int DEVICE_ID_BYTES = 16;
-
- private final ConnectedDeviceStorage mStorage;
-
- private ShowVerificationCodeListener mShowVerificationCodeListener;
-
- @HandshakeState
- private int mState = HandshakeState.UNKNOWN;
-
- private Key mPendingKey;
-
- private String mDeviceId;
-
- public AssociationSecureChannel(DeviceMessageStream stream, ConnectedDeviceStorage storage) {
- this(stream, storage, newRunner(EncryptionRunnerType.UKEY2));
- }
-
- AssociationSecureChannel(DeviceMessageStream stream, ConnectedDeviceStorage storage,
- EncryptionRunner encryptionRunner) {
- super(stream, encryptionRunner);
- encryptionRunner.setIsReconnect(false);
- mStorage = storage;
- }
-
- @Override
- void processHandshake(@NonNull byte[] message) throws HandshakeException {
- switch (mState) {
- case HandshakeState.UNKNOWN:
- processHandshakeUnknown(message);
- break;
- case HandshakeState.IN_PROGRESS:
- processHandshakeInProgress(message);
- break;
- case HandshakeState.FINISHED:
- processHandshakeDeviceIdAndSecret(message);
- break;
- default:
- loge(TAG, "Encountered unexpected handshake state: " + mState + ".");
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
- }
- }
-
- private void processHandshakeUnknown(@NonNull byte[] message) throws HandshakeException {
- logd(TAG, "Responding to handshake init request.");
- HandshakeMessage handshakeMessage = getEncryptionRunner().respondToInitRequest(message);
- mState = handshakeMessage.getHandshakeState();
- sendHandshakeMessage(handshakeMessage.getNextMessage(), /* isEncrypted= */ false);
- }
-
- private void processHandshakeInProgress(@NonNull byte[] message) throws HandshakeException {
- logd(TAG, "Continuing handshake.");
- HandshakeMessage handshakeMessage = getEncryptionRunner().continueHandshake(message);
- mState = handshakeMessage.getHandshakeState();
- if (mState != HandshakeState.VERIFICATION_NEEDED) {
- loge(TAG, "processHandshakeInProgress: Encountered unexpected handshake state: "
- + mState + ".");
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
- return;
- }
-
- String code = handshakeMessage.getVerificationCode();
- if (code == null) {
- loge(TAG, "Unable to get verification code.");
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_VERIFICATION);
- return;
- }
- processVerificationCode(code);
- }
-
- private void processVerificationCode(@NonNull String code) {
- if (mShowVerificationCodeListener == null) {
- loge(TAG,
- "No verification code listener has been set. Unable to display "
- + "verification "
- + "code to user.");
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
- return;
- }
-
- logd(TAG, "Showing pairing code: " + code);
- mShowVerificationCodeListener.showVerificationCode(code);
- }
-
- private void processHandshakeDeviceIdAndSecret(@NonNull byte[] message) {
- UUID deviceId = ByteUtils.bytesToUUID(Arrays.copyOf(message, DEVICE_ID_BYTES));
- if (deviceId == null) {
- loge(TAG, "Received invalid device id. Aborting.");
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_DEVICE_ID);
- return;
- }
- mDeviceId = deviceId.toString();
- notifyCallback(callback -> callback.onDeviceIdReceived(mDeviceId));
-
- mStorage.saveEncryptionKey(mDeviceId, mPendingKey.asBytes());
- mPendingKey = null;
- try {
- mStorage.saveChallengeSecret(mDeviceId,
- Arrays.copyOfRange(message, DEVICE_ID_BYTES, message.length));
- } catch (InvalidParameterException e) {
- loge(TAG, "Error saving challenge secret.", e);
- notifySecureChannelFailure(CHANNEL_ERROR_STORAGE_ERROR);
- return;
- }
-
- notifyCallback(Callback::onSecureChannelEstablished);
- }
-
- /** Set the listener that notifies to show verification code. {@code null} to clear. */
- public void setShowVerificationCodeListener(@Nullable ShowVerificationCodeListener listener) {
- mShowVerificationCodeListener = listener;
- }
-
- @VisibleForTesting
- @Nullable
- public ShowVerificationCodeListener getShowVerificationCodeListener() {
- return mShowVerificationCodeListener;
- }
-
- /**
- * Called by the client to notify that the user has accepted a pairing code or any out-of-band
- * confirmation, and send confirmation signals to remote bluetooth device.
- */
- public void notifyOutOfBandAccepted() {
- HandshakeMessage message;
- try {
- message = getEncryptionRunner().verifyPin();
- } catch (HandshakeException e) {
- loge(TAG, "Error during PIN verification", e);
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_VERIFICATION);
- return;
- }
- if (message.getHandshakeState() != HandshakeState.FINISHED) {
- loge(TAG, "Handshake not finished after calling verify PIN. Instead got "
- + "state: " + message.getHandshakeState() + ".");
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
- return;
- }
-
- Key localKey = message.getKey();
- if (localKey == null) {
- loge(TAG, "Unable to finish association, generated key is null.");
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_ENCRYPTION_KEY);
- return;
- }
- mState = message.getHandshakeState();
- setEncryptionKey(localKey);
- mPendingKey = localKey;
- logd(TAG, "Pairing code successfully verified.");
- sendUniqueIdToClient();
- }
-
- private void sendUniqueIdToClient() {
- UUID uniqueId = mStorage.getUniqueId();
- DeviceMessage deviceMessage = new DeviceMessage(/* recipient= */ null,
- /* isMessageEncrypted= */ true, ByteUtils.uuidToBytes(uniqueId));
- logd(TAG, "Sending car's device id of " + uniqueId + " to device.");
- sendHandshakeMessage(ByteUtils.uuidToBytes(uniqueId), /* isEncrypted= */ true);
- }
-
- @HandshakeState
- int getState() {
- return mState;
- }
-
- void setState(@HandshakeState int state) {
- mState = state;
- }
-
- /** Listener that will be invoked to display verification code. */
- public interface ShowVerificationCodeListener {
- /**
- * Invoke when a verification need to be displayed during device association.
- *
- * @param code The verification code to show.
- */
- void showVerificationCode(@NonNull String code);
- }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/CarBluetoothManager.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/CarBluetoothManager.java
deleted file mode 100644
index 9ca699b..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/CarBluetoothManager.java
+++ /dev/null
@@ -1,485 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.connection;
-
-import static com.android.car.connecteddevice.ConnectedDeviceManager.DEVICE_ERROR_INVALID_HANDSHAKE;
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-import static com.android.car.connecteddevice.util.SafeLog.logw;
-
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothGatt;
-
-import androidx.annotation.CallSuper;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.car.connecteddevice.AssociationCallback;
-import com.android.car.connecteddevice.model.AssociatedDevice;
-import com.android.car.connecteddevice.oob.OobChannel;
-import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
-import com.android.car.connecteddevice.util.ThreadSafeCallbacks;
-
-import java.util.UUID;
-import java.util.concurrent.CopyOnWriteArraySet;
-import java.util.concurrent.Executor;
-
-/**
- * Generic BLE manager for a car that keeps track of connected devices and their associated
- * callbacks.
- */
-public abstract class CarBluetoothManager {
-
- private static final String TAG = "CarBluetoothManager";
-
- protected final ConnectedDeviceStorage mStorage;
-
- protected final CopyOnWriteArraySet<ConnectedRemoteDevice> mConnectedDevices =
- new CopyOnWriteArraySet<>();
-
- protected final ThreadSafeCallbacks<Callback> mCallbacks = new ThreadSafeCallbacks<>();
-
- private String mClientDeviceName;
-
- private String mClientDeviceAddress;
-
- protected CarBluetoothManager(@NonNull ConnectedDeviceStorage connectedDeviceStorage) {
- mStorage = connectedDeviceStorage;
- }
-
- /** Attempt to connect to device with provided id. */
- public void connectToDevice(@NonNull UUID deviceId) {
- for (ConnectedRemoteDevice device : mConnectedDevices) {
- if (UUID.fromString(device.mDeviceId).equals(deviceId)) {
- logd(TAG, "Already connected to device " + deviceId + ".");
- // Already connected to this device. Ignore requests to connect again.
- return;
- }
- }
- // Clear any previous session before starting a new one.
- reset();
- initiateConnectionToDevice(deviceId);
- }
-
- /** Start to connect to associated devices */
- public abstract void initiateConnectionToDevice(@NonNull UUID deviceId);
-
- /** Start the association with a new device */
- public abstract void startAssociation(@NonNull String nameForAssociation,
- @NonNull AssociationCallback callback);
-
- /** Start the association with a new device using out of band verification code exchange */
- public abstract void startOutOfBandAssociation(
- @NonNull String nameForAssociation,
- @NonNull OobChannel oobChannel,
- @NonNull AssociationCallback callback);
-
- /** Disconnect the provided device from this manager. */
- public abstract void disconnectDevice(@NonNull String deviceId);
-
- /** Get current {@link AssociationCallback}. */
- @Nullable
- public abstract AssociationCallback getAssociationCallback();
-
- /** Set current {@link AssociationCallback}. */
- public abstract void setAssociationCallback(@Nullable AssociationCallback callback);
-
- /** Set the value of the client device name */
- public void setClientDeviceName(String deviceName) {
- mClientDeviceName = deviceName;
- }
-
- /** Set the value of client device's mac address */
- public void setClientDeviceAddress(String macAddress) {
- mClientDeviceAddress = macAddress;
- }
-
- /**
- * Initialize and start the manager.
- */
- @CallSuper
- public void start() {}
-
- /**
- * Stop the manager and clean up.
- */
- public void stop() {
- for (ConnectedRemoteDevice device : mConnectedDevices) {
- if (device.mGatt != null) {
- device.mGatt.close();
- }
- }
- mConnectedDevices.clear();
- }
-
- /**
- * Stop the association process with any device.
- */
- public void stopAssociation() {
- if (!isAssociating()) {
- return;
- }
- reset();
- }
-
- /**
- * Register a {@link Callback} to be notified on the {@link Executor}.
- */
- public void registerCallback(@NonNull Callback callback, @NonNull Executor executor) {
- mCallbacks.add(callback, executor);
- }
-
- /**
- * Unregister a callback.
- *
- * @param callback The {@link Callback} to unregister.
- */
- public void unregisterCallback(@NonNull Callback callback) {
- mCallbacks.remove(callback);
- }
-
- /**
- * Send a message to a connected device.
- *
- * @param deviceId Id of connected device.
- * @param message {@link DeviceMessage} to send.
- */
- public void sendMessage(@NonNull String deviceId, @NonNull DeviceMessage message) {
- ConnectedRemoteDevice device = getConnectedDevice(deviceId);
- if (device == null) {
- logw(TAG, "Attempted to send message to unknown device $deviceId. Ignored.");
- return;
- }
-
- sendMessage(device, message);
- }
-
- /**
- * Send a message to a connected device.
- *
- * @param device The connected {@link ConnectedRemoteDevice}.
- * @param message {@link DeviceMessage} to send.
- */
- public void sendMessage(@NonNull ConnectedRemoteDevice device, @NonNull DeviceMessage message) {
- String deviceId = device.mDeviceId;
- if (deviceId == null) {
- deviceId = "Unidentified device";
- }
-
- logd(TAG, "Writing " + message.getMessage().length + " bytes to " + deviceId + ".");
- device.mSecureChannel.sendClientMessage(message);
- }
-
- /** Clean manager status and callbacks. */
- @CallSuper
- public void reset() {
- mClientDeviceAddress = null;
- mClientDeviceName = null;
- mConnectedDevices.clear();
- }
-
- /** Notify that the user has accepted a pairing code or other out-of-band confirmation. */
- public void notifyOutOfBandAccepted() {
- if (getConnectedDevice() == null) {
- disconnectWithError("Null connected device found when out-of-band confirmation "
- + "received.");
- return;
- }
-
- AssociationSecureChannel secureChannel =
- (AssociationSecureChannel) getConnectedDevice().mSecureChannel;
- if (secureChannel == null) {
- disconnectWithError("Null SecureBleChannel found for the current connected device "
- + "when out-of-band confirmation received.");
- return;
- }
-
- secureChannel.notifyOutOfBandAccepted();
- }
-
- /** Returns the secure channel of current connected device. */
- @Nullable
- public SecureChannel getConnectedDeviceChannel() {
- ConnectedRemoteDevice connectedDevice = getConnectedDevice();
- if (connectedDevice == null) {
- return null;
- }
-
- return connectedDevice.mSecureChannel;
- }
-
- /**
- * Get the {@link ConnectedRemoteDevice} with matching {@link BluetoothGatt} if available.
- * Returns
- * {@code null} if no matches are found.
- */
- @Nullable
- protected final ConnectedRemoteDevice getConnectedDevice(@NonNull BluetoothGatt gatt) {
- for (ConnectedRemoteDevice device : mConnectedDevices) {
- if (device.mGatt == gatt) {
- return device;
- }
- }
-
- return null;
- }
-
- /**
- * Get the {@link ConnectedRemoteDevice} with matching {@link BluetoothDevice} if available.
- * Returns
- * {@code null} if no matches are found.
- */
- @Nullable
- protected final ConnectedRemoteDevice getConnectedDevice(@NonNull BluetoothDevice device) {
- for (ConnectedRemoteDevice connectedDevice : mConnectedDevices) {
- if (device.equals(connectedDevice.mDevice)) {
- return connectedDevice;
- }
- }
-
- return null;
- }
-
- /**
- * Get the {@link ConnectedRemoteDevice} with matching device id if available. Returns {@code
- * null} if no matches are found.
- */
- @Nullable
- protected final ConnectedRemoteDevice getConnectedDevice(@NonNull String deviceId) {
- for (ConnectedRemoteDevice device : mConnectedDevices) {
- if (deviceId.equals(device.mDeviceId)) {
- return device;
- }
- }
-
- return null;
- }
-
- /** Add the {@link ConnectedRemoteDevice} that has connected. */
- protected final void addConnectedDevice(@NonNull ConnectedRemoteDevice device) {
- mConnectedDevices.add(device);
- }
-
- /** Return the number of devices currently connected. */
- protected final int getConnectedDevicesCount() {
- return mConnectedDevices.size();
- }
-
- /** Remove [@link BleDevice} that has been disconnected. */
- protected final void removeConnectedDevice(@NonNull ConnectedRemoteDevice device) {
- mConnectedDevices.remove(device);
- }
-
- /** Return [@code true} if the manager is currently in an association process. */
- protected final boolean isAssociating() {
- return getAssociationCallback() != null;
- }
-
- /**
- * Set the device id of {@link ConnectedRemoteDevice} and then notify device connected callback.
- *
- * @param deviceId The device id received from remote device.
- */
- protected final void setDeviceIdAndNotifyCallbacks(@NonNull String deviceId) {
- logd(TAG, "Setting device id: " + deviceId);
- ConnectedRemoteDevice connectedDevice = getConnectedDevice();
- if (connectedDevice == null) {
- disconnectWithError("Null connected device found when device id received.");
- return;
- }
-
- connectedDevice.mDeviceId = deviceId;
- mCallbacks.invoke(callback -> callback.onDeviceConnected(deviceId));
- }
-
- /** Log error which causes the disconnect with {@link Exception} and notify callbacks. */
- protected final void disconnectWithError(@NonNull String errorMessage, @Nullable Exception e) {
- loge(TAG, errorMessage, e);
- if (isAssociating()) {
- getAssociationCallback().onAssociationError(DEVICE_ERROR_INVALID_HANDSHAKE);
- }
- reset();
- }
-
- /** Log error which cause the disconnection and notify callbacks. */
- protected final void disconnectWithError(@NonNull String errorMessage) {
- disconnectWithError(errorMessage, null);
- }
-
- /** Return the current connected device. */
- @Nullable
- protected final ConnectedRemoteDevice getConnectedDevice() {
- if (mConnectedDevices.isEmpty()) {
- return null;
- }
- // Directly return the next because there will only be one device connected at one time.
- return mConnectedDevices.iterator().next();
- }
-
- protected final SecureChannel.Callback mSecureChannelCallback =
- new SecureChannel.Callback() {
- @Override
- public void onSecureChannelEstablished() {
- ConnectedRemoteDevice connectedDevice = getConnectedDevice();
- if (connectedDevice == null || connectedDevice.mDeviceId == null) {
- disconnectWithError("Null device id found when secure channel "
- + "established.");
- return;
- }
- String deviceId = connectedDevice.mDeviceId;
- if (mClientDeviceAddress == null) {
- disconnectWithError("Null device address found when secure channel "
- + "established.");
- return;
- }
-
- if (isAssociating()) {
- logd(TAG, "Secure channel established for un-associated device. Saving "
- + "association of that device for current user.");
- mStorage.addAssociatedDeviceForActiveUser(
- new AssociatedDevice(deviceId, mClientDeviceAddress,
- mClientDeviceName, /* isConnectionEnabled= */ true));
- AssociationCallback callback = getAssociationCallback();
- if (callback != null) {
- callback.onAssociationCompleted(deviceId);
- setAssociationCallback(null);
- }
- }
- mCallbacks.invoke(callback -> callback.onSecureChannelEstablished(deviceId));
- }
-
- @Override
- public void onEstablishSecureChannelFailure(int error) {
- ConnectedRemoteDevice connectedDevice = getConnectedDevice();
- if (connectedDevice == null || connectedDevice.mDeviceId == null) {
- disconnectWithError("Null device id found when secure channel "
- + "failed to establish.");
- return;
- }
- String deviceId = connectedDevice.mDeviceId;
- mCallbacks.invoke(callback -> callback.onSecureChannelError(deviceId));
-
- if (isAssociating()) {
- getAssociationCallback().onAssociationError(error);
- }
-
- disconnectWithError("Error while establishing secure connection.");
- }
-
- @Override
- public void onMessageReceived(DeviceMessage deviceMessage) {
- ConnectedRemoteDevice connectedDevice = getConnectedDevice();
- if (connectedDevice == null || connectedDevice.mDeviceId == null) {
- disconnectWithError("Null device id found when message received.");
- return;
- }
-
- logd(TAG, "Received new message from " + connectedDevice.mDeviceId
- + " with " + deviceMessage.getMessage().length + " bytes in its "
- + "payload. Notifying " + mCallbacks.size() + " callbacks.");
- mCallbacks.invoke(
- callback -> callback.onMessageReceived(connectedDevice.mDeviceId,
- deviceMessage));
- }
-
- @Override
- public void onMessageReceivedError(Exception exception) {
- // TODO(b/143879960) Extend the message error from here to continue up the
- // chain.
- disconnectWithError("Error while receiving message.");
- }
-
- @Override
- public void onDeviceIdReceived(String deviceId) {
- setDeviceIdAndNotifyCallbacks(deviceId);
- }
- };
-
-
- /** State for a connected device. */
- public enum ConnectedDeviceState {
- CONNECTING,
- PENDING_VERIFICATION,
- CONNECTED,
- UNKNOWN
- }
-
- /**
- * Container class to hold information about a connected device.
- */
- public static class ConnectedRemoteDevice {
- @NonNull
- public BluetoothDevice mDevice;
- @Nullable
- public BluetoothGatt mGatt;
- @NonNull
- public ConnectedDeviceState mState;
- @Nullable
- public String mDeviceId;
- @Nullable
- public SecureChannel mSecureChannel;
-
- public ConnectedRemoteDevice(@NonNull BluetoothDevice device,
- @Nullable BluetoothGatt gatt) {
- mDevice = device;
- mGatt = gatt;
- mState = ConnectedDeviceState.UNKNOWN;
- }
- }
-
- /**
- * Callback for triggered events from {@link CarBluetoothManager}.
- */
- public interface Callback {
- /**
- * Triggered when device is connected and device id retrieved. Device is now ready to
- * receive messages.
- *
- * @param deviceId Id of device that has connected.
- */
- void onDeviceConnected(@NonNull String deviceId);
-
- /**
- * Triggered when device is disconnected.
- *
- * @param deviceId Id of device that has disconnected.
- */
- void onDeviceDisconnected(@NonNull String deviceId);
-
- /**
- * Triggered when device has established encryption for secure communication.
- *
- * @param deviceId Id of device that has established encryption.
- */
- void onSecureChannelEstablished(@NonNull String deviceId);
-
- /**
- * Triggered when a new message is received.
- *
- * @param deviceId Id of the device that sent the message.
- * @param message {@link DeviceMessage} received.
- */
- void onMessageReceived(@NonNull String deviceId, @NonNull DeviceMessage message);
-
- /**
- * Triggered when an error when establishing the secure channel.
- *
- * @param deviceId Id of the device that experienced the error.
- */
- void onSecureChannelError(@NonNull String deviceId);
- }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/DeviceMessage.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/DeviceMessage.java
deleted file mode 100644
index 128decb..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/DeviceMessage.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.connection;
-
-import static com.android.car.connecteddevice.StreamProtos.DeviceMessageProto.Message;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.util.Arrays;
-import java.util.Objects;
-import java.util.UUID;
-
-/** Holds the needed data from a {@link Message}. */
-public class DeviceMessage {
-
- private static final String TAG = "DeviceMessage";
-
- private final UUID mRecipient;
-
- private final boolean mIsMessageEncrypted;
-
- private byte[] mMessage;
-
- public DeviceMessage(@Nullable UUID recipient, boolean isMessageEncrypted,
- @NonNull byte[] message) {
- mRecipient = recipient;
- mIsMessageEncrypted = isMessageEncrypted;
- mMessage = message;
- }
-
- /** Returns the recipient for this message. {@code null} if no recipient set. */
- @Nullable
- public UUID getRecipient() {
- return mRecipient;
- }
-
- /** Returns whether this message is encrypted. */
- public boolean isMessageEncrypted() {
- return mIsMessageEncrypted;
- }
-
- /** Returns the message payload. */
- @Nullable
- public byte[] getMessage() {
- return mMessage;
- }
-
- /** Set the message payload. */
- public void setMessage(@NonNull byte[] message) {
- mMessage = message;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (obj == this) {
- return true;
- }
- if (!(obj instanceof DeviceMessage)) {
- return false;
- }
- DeviceMessage deviceMessage = (DeviceMessage) obj;
- return Objects.equals(mRecipient, deviceMessage.mRecipient)
- && mIsMessageEncrypted == deviceMessage.mIsMessageEncrypted
- && Arrays.equals(mMessage, deviceMessage.mMessage);
- }
-
- @Override
- public int hashCode() {
- return 31 * Objects.hash(mRecipient, mIsMessageEncrypted)
- + Arrays.hashCode(mMessage);
- }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/DeviceMessageStream.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/DeviceMessageStream.java
deleted file mode 100644
index c23618a..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/DeviceMessageStream.java
+++ /dev/null
@@ -1,362 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.connection;
-
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-import static com.android.car.connecteddevice.util.SafeLog.logw;
-
-import androidx.annotation.IntRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-
-import com.android.car.connecteddevice.StreamProtos.DeviceMessageProto.Message;
-import com.android.car.connecteddevice.StreamProtos.OperationProto.OperationType;
-import com.android.car.connecteddevice.StreamProtos.PacketProto.Packet;
-import com.android.car.connecteddevice.StreamProtos.VersionExchangeProto.VersionExchange;
-import com.android.car.connecteddevice.util.ByteUtils;
-import com.android.car.protobuf.ByteString;
-import com.android.car.protobuf.InvalidProtocolBufferException;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.util.ArrayDeque;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.UUID;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-
-/**
- * Abstract class which includes common logic of different types of {@link DeviceMessageStream}.
- */
-public abstract class DeviceMessageStream {
-
- private static final String TAG = "DeviceMessageStream";
-
- // Only version 2 of the messaging and version 2 of the security supported.
- @VisibleForTesting
- public static final int MESSAGING_VERSION = 2;
- @VisibleForTesting
- public static final int SECURITY_VERSION = 2;
-
- private final ArrayDeque<Packet> mPacketQueue = new ArrayDeque<>();
-
- private final Map<Integer, ByteArrayOutputStream> mPendingData = new HashMap<>();
-
- // messageId -> nextExpectedPacketNumber
- private final Map<Integer, Integer> mPendingPacketNumber = new HashMap<>();
-
- private final MessageIdGenerator mMessageIdGenerator = new MessageIdGenerator();
-
- private final AtomicBoolean mIsVersionExchanged = new AtomicBoolean(false);
-
- private final AtomicBoolean mIsSendingInProgress = new AtomicBoolean(false);
-
- private int mMaxWriteSize;
-
- /** Listener which will be notified when there is new {@link DeviceMessage} received. */
- private MessageReceivedListener mMessageReceivedListener;
-
- /** Listener which will be notified when there is error parsing the received message. */
- private MessageReceivedErrorListener mMessageReceivedErrorListener;
-
- public DeviceMessageStream(int defaultMaxWriteSize) {
- mMaxWriteSize = defaultMaxWriteSize;
- }
-
- /**
- * Send data to the connected device. Note: {@link #sendCompleted()} must be called when the
- * bytes have successfully been sent to indicate the stream is ready to send more data.
- */
- protected abstract void send(byte[] data);
-
- /**
- * Set the given listener to be notified when a new message was received from the client. If
- * listener is {@code null}, clear.
- */
- public final void setMessageReceivedListener(@Nullable MessageReceivedListener listener) {
- mMessageReceivedListener = listener;
- }
-
- /**
- * Set the given listener to be notified when there was an error during receiving message from
- * the client. If listener is {@code null}, clear.
- */
- public final void setMessageReceivedErrorListener(
- @Nullable MessageReceivedErrorListener listener) {
- mMessageReceivedErrorListener = listener;
- }
-
- /**
- * Notify the {@code mMessageReceivedListener} about the message received if it is not {@code
- * null}.
- *
- * @param deviceMessage The message received.
- * @param operationType The operation type of the message.
- */
- protected final void notifyMessageReceivedListener(@NonNull DeviceMessage deviceMessage,
- OperationType operationType) {
- if (mMessageReceivedListener != null) {
- mMessageReceivedListener.onMessageReceived(deviceMessage, operationType);
- }
- }
-
- /**
- * Notify the {@code mMessageReceivedErrorListener} about the message received if it is not
- * {@code null}.
- *
- * @param e The exception happened when parsing the received message.
- */
- protected final void notifyMessageReceivedErrorListener(Exception e) {
- if (mMessageReceivedErrorListener != null) {
- mMessageReceivedErrorListener.onMessageReceivedError(e);
- }
- }
-
- /**
- * Writes the given message to the write characteristic of this stream with operation type
- * {@code CLIENT_MESSAGE}.
- *
- * This method will handle the chunking of messages based on the max write size.
- *
- * @param deviceMessage The data object contains recipient, isPayloadEncrypted and message.
- */
- public final void writeMessage(@NonNull DeviceMessage deviceMessage) {
- writeMessage(deviceMessage, OperationType.CLIENT_MESSAGE);
- }
-
- /**
- * Send {@link DeviceMessage} to remote connected devices.
- *
- * @param deviceMessage The message which need to be sent
- * @param operationType The operation type of current message
- */
- public final void writeMessage(@NonNull DeviceMessage deviceMessage,
- OperationType operationType) {
- Message.Builder builder = Message.newBuilder()
- .setOperation(operationType)
- .setIsPayloadEncrypted(deviceMessage.isMessageEncrypted())
- .setPayload(ByteString.copyFrom(deviceMessage.getMessage()));
-
- UUID recipient = deviceMessage.getRecipient();
- if (recipient != null) {
- builder.setRecipient(ByteString.copyFrom(ByteUtils.uuidToBytes(recipient)));
- }
-
- Message message = builder.build();
- byte[] rawBytes = message.toByteArray();
- List<Packet> packets;
- try {
- packets = PacketFactory.makePackets(rawBytes, mMessageIdGenerator.next(),
- mMaxWriteSize);
- } catch (PacketFactoryException e) {
- loge(TAG, "Error while creating message packets.", e);
- return;
- }
- mPacketQueue.addAll(packets);
- writeNextMessageInQueue();
- }
-
- private void writeNextMessageInQueue() {
- if (mPacketQueue.isEmpty()) {
- logd(TAG, "No more packets to send.");
- return;
- }
- boolean isLockAcquired = mIsSendingInProgress.compareAndSet(false, true);
- if (!isLockAcquired) {
- logd(TAG, "Unable to send packet at this time.");
- return;
- }
-
- Packet packet = mPacketQueue.remove();
- logd(TAG, "Writing packet " + packet.getPacketNumber() + " of "
- + packet.getTotalPackets() + " for " + packet.getMessageId() + ".");
- send(packet.toByteArray());
- }
-
- /** Process incoming data from stream. */
- protected final void onDataReceived(byte[] data) {
- if (!hasVersionBeenExchanged()) {
- processVersionExchange(data);
- return;
- }
-
- Packet packet;
- try {
- packet = Packet.parseFrom(data);
- } catch (InvalidProtocolBufferException e) {
- loge(TAG, "Can not parse packet from client.", e);
- notifyMessageReceivedErrorListener(e);
- return;
- }
- processPacket(packet);
- }
-
- protected final void processPacket(@NonNull Packet packet) {
- int messageId = packet.getMessageId();
- int packetNumber = packet.getPacketNumber();
- int expectedPacket = mPendingPacketNumber.getOrDefault(messageId, 1);
- if (packetNumber == expectedPacket - 1) {
- logw(TAG, "Received duplicate packet " + packet.getPacketNumber() + " for message "
- + messageId + ". Ignoring.");
- return;
- }
- if (packetNumber != expectedPacket) {
- loge(TAG, "Received unexpected packet " + packetNumber + " for message "
- + messageId + ".");
- notifyMessageReceivedErrorListener(
- new IllegalStateException("Packet received out of order."));
- return;
- }
- mPendingPacketNumber.put(messageId, packetNumber + 1);
-
- ByteArrayOutputStream currentPayloadStream =
- mPendingData.getOrDefault(messageId, new ByteArrayOutputStream());
- mPendingData.putIfAbsent(messageId, currentPayloadStream);
-
- byte[] payload = packet.getPayload().toByteArray();
- try {
- currentPayloadStream.write(payload);
- } catch (IOException e) {
- loge(TAG, "Error writing packet to stream.", e);
- notifyMessageReceivedErrorListener(e);
- return;
- }
- logd(TAG, "Parsed packet " + packet.getPacketNumber() + " of "
- + packet.getTotalPackets() + " for message " + messageId + ". Writing "
- + payload.length + ".");
-
- if (packet.getPacketNumber() != packet.getTotalPackets()) {
- return;
- }
-
- byte[] messageBytes = currentPayloadStream.toByteArray();
- mPendingData.remove(messageId);
-
- logd(TAG, "Received complete device message " + messageId + " of " + messageBytes.length
- + " bytes.");
- Message message;
- try {
- message = Message.parseFrom(messageBytes);
- } catch (InvalidProtocolBufferException e) {
- loge(TAG, "Cannot parse device message from client.", e);
- notifyMessageReceivedErrorListener(e);
- return;
- }
-
- DeviceMessage deviceMessage = new DeviceMessage(
- ByteUtils.bytesToUUID(message.getRecipient().toByteArray()),
- message.getIsPayloadEncrypted(), message.getPayload().toByteArray());
- notifyMessageReceivedListener(deviceMessage, message.getOperation());
- }
-
- /** The maximum amount of bytes that can be written in a single packet. */
- public final void setMaxWriteSize(@IntRange(from = 1) int maxWriteSize) {
- if (maxWriteSize <= 0) {
- return;
- }
- mMaxWriteSize = maxWriteSize;
- }
-
- private boolean hasVersionBeenExchanged() {
- return mIsVersionExchanged.get();
- }
-
- /** Indicate current send operation has completed. */
- @VisibleForTesting
- public final void sendCompleted() {
- mIsSendingInProgress.set(false);
- writeNextMessageInQueue();
- }
-
- private void processVersionExchange(@NonNull byte[] value) {
- VersionExchange versionExchange;
- try {
- versionExchange = VersionExchange.parseFrom(value);
- } catch (InvalidProtocolBufferException e) {
- loge(TAG, "Could not parse version exchange message", e);
- notifyMessageReceivedErrorListener(e);
-
- return;
- }
- int minMessagingVersion = versionExchange.getMinSupportedMessagingVersion();
- int maxMessagingVersion = versionExchange.getMaxSupportedMessagingVersion();
- int minSecurityVersion = versionExchange.getMinSupportedSecurityVersion();
- int maxSecurityVersion = versionExchange.getMaxSupportedSecurityVersion();
- if (minMessagingVersion > MESSAGING_VERSION || maxMessagingVersion < MESSAGING_VERSION
- || minSecurityVersion > SECURITY_VERSION || maxSecurityVersion < SECURITY_VERSION) {
- loge(TAG, "Unsupported message version for min " + minMessagingVersion + " and max "
- + maxMessagingVersion + " or security version for " + minSecurityVersion
- + " and max " + maxSecurityVersion + ".");
- notifyMessageReceivedErrorListener(new IllegalStateException("Unsupported version."));
- return;
- }
-
- VersionExchange headunitVersion = VersionExchange.newBuilder()
- .setMinSupportedMessagingVersion(MESSAGING_VERSION)
- .setMaxSupportedMessagingVersion(MESSAGING_VERSION)
- .setMinSupportedSecurityVersion(SECURITY_VERSION)
- .setMaxSupportedSecurityVersion(SECURITY_VERSION)
- .build();
-
- send(headunitVersion.toByteArray());
- mIsVersionExchanged.set(true);
- logd(TAG, "Sent supported version to the phone.");
- }
-
-
- /** A generator of unique IDs for messages. */
- private static class MessageIdGenerator {
- private final AtomicInteger mMessageId = new AtomicInteger(0);
-
- int next() {
- int current = mMessageId.getAndIncrement();
- mMessageId.compareAndSet(Integer.MAX_VALUE, 0);
- return current;
- }
- }
-
- /**
- * Listener to be invoked when a complete message is received from the client.
- */
- public interface MessageReceivedListener {
-
- /**
- * Called when a complete message is received from the client.
- *
- * @param deviceMessage The message received from the client.
- * @param operationType The {@link OperationType} of the received message.
- */
- void onMessageReceived(@NonNull DeviceMessage deviceMessage,
- OperationType operationType);
- }
-
- /**
- * Listener to be invoked when there was an error during receiving message from the client.
- */
- public interface MessageReceivedErrorListener {
- /**
- * Called when there was an error during receiving message from the client.
- *
- * @param exception The error.
- */
- void onMessageReceivedError(@NonNull Exception exception);
- }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/OobAssociationSecureChannel.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/OobAssociationSecureChannel.java
deleted file mode 100644
index c6b0a7d..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/OobAssociationSecureChannel.java
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.connection;
-
-import static android.car.encryptionrunner.HandshakeMessage.HandshakeState;
-
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-
-import android.car.encryptionrunner.EncryptionRunner;
-import android.car.encryptionrunner.EncryptionRunnerFactory;
-import android.car.encryptionrunner.HandshakeException;
-import android.car.encryptionrunner.HandshakeMessage;
-
-import androidx.annotation.NonNull;
-
-import com.android.car.connecteddevice.oob.OobConnectionManager;
-import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
-
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.util.Arrays;
-
-import javax.crypto.BadPaddingException;
-import javax.crypto.IllegalBlockSizeException;
-
-/**
- * A secure channel established with the association flow with an out-of-band verification.
- */
-public class OobAssociationSecureChannel extends AssociationSecureChannel {
-
- private static final String TAG = "OobAssociationSecureChannel";
-
- private final OobConnectionManager mOobConnectionManager;
-
- private byte[] mOobCode;
-
- public OobAssociationSecureChannel(
- DeviceMessageStream stream,
- ConnectedDeviceStorage storage,
- OobConnectionManager oobConnectionManager) {
- this(stream, storage, oobConnectionManager, EncryptionRunnerFactory.newRunner(
- EncryptionRunnerFactory.EncryptionRunnerType.OOB_UKEY2));
- }
-
- OobAssociationSecureChannel(
- DeviceMessageStream stream,
- ConnectedDeviceStorage storage,
- OobConnectionManager oobConnectionManager,
- EncryptionRunner encryptionRunner) {
- super(stream, storage, encryptionRunner);
- mOobConnectionManager = oobConnectionManager;
- }
-
- @Override
- void processHandshake(@NonNull byte[] message) throws HandshakeException {
- switch (getState()) {
- case HandshakeState.IN_PROGRESS:
- processHandshakeInProgress(message);
- break;
- case HandshakeState.OOB_VERIFICATION_NEEDED:
- processHandshakeOobVerificationNeeded(message);
- break;
- default:
- super.processHandshake(message);
- }
- }
-
- private void processHandshakeInProgress(@NonNull byte[] message) throws HandshakeException {
- HandshakeMessage handshakeMessage = getEncryptionRunner().continueHandshake(message);
- setState(handshakeMessage.getHandshakeState());
- int state = getState();
- if (state != HandshakeState.OOB_VERIFICATION_NEEDED) {
- loge(TAG, "processHandshakeInProgress: Encountered unexpected handshake state: "
- + state + ".");
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
- return;
- }
-
- mOobCode = handshakeMessage.getOobVerificationCode();
- if (mOobCode == null) {
- loge(TAG, "Unable to get out of band verification code.");
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_VERIFICATION);
- return;
- }
-
- byte[] encryptedCode;
- try {
- encryptedCode = mOobConnectionManager.encryptVerificationCode(mOobCode);
- } catch (InvalidKeyException | InvalidAlgorithmParameterException
- | IllegalBlockSizeException | BadPaddingException e) {
- loge(TAG, "Encryption failed for verification code exchange.", e);
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_HANDSHAKE);
- return;
- }
-
- sendHandshakeMessage(encryptedCode, /* isEncrypted= */ false);
- }
-
- private void processHandshakeOobVerificationNeeded(@NonNull byte[] message) {
- byte[] decryptedCode;
- try {
- decryptedCode = mOobConnectionManager.decryptVerificationCode(message);
- } catch (InvalidKeyException | InvalidAlgorithmParameterException
- | IllegalBlockSizeException | BadPaddingException e) {
- loge(TAG, "Decryption failed for verification code exchange", e);
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_HANDSHAKE);
- return;
- }
-
- if (!Arrays.equals(mOobCode, decryptedCode)) {
- loge(TAG, "Exchanged verification codes do not match. Aborting secure channel.");
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_VERIFICATION);
- return;
- }
-
- notifyOutOfBandAccepted();
- }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/PacketFactory.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/PacketFactory.java
deleted file mode 100644
index 5036a18..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/PacketFactory.java
+++ /dev/null
@@ -1,156 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.connection;
-
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-
-import androidx.annotation.VisibleForTesting;
-
-import com.android.car.connecteddevice.StreamProtos.PacketProto.Packet;
-import com.android.car.protobuf.ByteString;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-/**
- * Factory for creating {@link Packet} protos.
- */
-class PacketFactory {
- private static final String TAG = "PacketFactory";
-
- /**
- * The size in bytes of a {@code fixed32} field in the proto.
- */
- private static final int FIXED_32_SIZE = 4;
-
- /**
- * The bytes needed to encode the field number in the proto.
- *
- * <p>Since the {@link Packet} only has 4 fields, it will only take 1 additional byte to
- * encode.
- */
- private static final int FIELD_NUMBER_ENCODING_SIZE = 1;
-
- /**
- * The size in bytes of field {@code packet_number}. The proto field is a {@code fixed32}.
- */
- private static final int PACKET_NUMBER_ENCODING_SIZE =
- FIXED_32_SIZE + FIELD_NUMBER_ENCODING_SIZE;
-
- /**
- * Split given data if necessary to fit within the given {@code maxSize}.
- *
- * @param payload The payload to potentially split across multiple {@link Packet}s.
- * @param messageId The unique id for identifying message.
- * @param maxSize The maximum size of each chunk.
- * @return A list of {@link Packet}s.
- * @throws PacketFactoryException if an error occurred during the splitting of data.
- */
- static List<Packet> makePackets(byte[] payload, int messageId, int maxSize)
- throws PacketFactoryException {
- List<Packet> blePackets = new ArrayList<>();
- int payloadSize = payload.length;
- int totalPackets = getTotalPacketNumber(messageId, payloadSize, maxSize);
- int maxPayloadSize = maxSize
- - getPacketHeaderSize(totalPackets, messageId, Math.min(payloadSize, maxSize));
-
- int start = 0;
- int end = Math.min(payloadSize, maxPayloadSize);
- for (int packetNum = 1; packetNum <= totalPackets; packetNum++) {
- blePackets.add(Packet.newBuilder()
- .setPacketNumber(packetNum)
- .setTotalPackets(totalPackets)
- .setMessageId(messageId)
- .setPayload(ByteString.copyFrom(Arrays.copyOfRange(payload, start, end)))
- .build());
- start = end;
- end = Math.min(start + maxPayloadSize, payloadSize);
- }
- return blePackets;
- }
-
- /**
- * Compute the header size for the {@link Packet} proto in bytes. This method assumes that
- * the proto contains a payload.
- */
- @VisibleForTesting
- static int getPacketHeaderSize(int totalPackets, int messageId, int payloadSize) {
- return FIXED_32_SIZE + FIELD_NUMBER_ENCODING_SIZE
- + getEncodedSize(totalPackets) + FIELD_NUMBER_ENCODING_SIZE
- + getEncodedSize(messageId) + FIELD_NUMBER_ENCODING_SIZE
- + getEncodedSize(payloadSize) + FIELD_NUMBER_ENCODING_SIZE;
- }
-
- /**
- * Compute the total packets required to encode a payload of the given size.
- */
- @VisibleForTesting
- static int getTotalPacketNumber(int messageId, int payloadSize, int maxSize)
- throws PacketFactoryException {
- int headerSizeWithoutTotalPackets = FIXED_32_SIZE + FIELD_NUMBER_ENCODING_SIZE
- + getEncodedSize(messageId) + FIELD_NUMBER_ENCODING_SIZE
- + getEncodedSize(Math.min(payloadSize, maxSize)) + FIELD_NUMBER_ENCODING_SIZE;
-
- for (int value = 1; value <= PACKET_NUMBER_ENCODING_SIZE; value++) {
- int packetHeaderSize = headerSizeWithoutTotalPackets + value
- + FIELD_NUMBER_ENCODING_SIZE;
- int maxPayloadSize = maxSize - packetHeaderSize;
- if (maxPayloadSize < 0) {
- throw new PacketFactoryException("Packet header size too large.");
- }
- int totalPackets = (int) Math.ceil(payloadSize / (double) maxPayloadSize);
- if (getEncodedSize(totalPackets) == value) {
- return totalPackets;
- }
- }
-
- loge(TAG, "Cannot get valid total packet number for message: messageId: "
- + messageId + ", payloadSize: " + payloadSize + ", maxSize: " + maxSize);
- throw new PacketFactoryException("No valid total packet number.");
- }
-
- /**
- * This method implements Protocol Buffers encoding algorithm.
- *
- * <p>Computes the number of bytes that would be needed to store a 32-bit variant.
- *
- * @param value the data that need to be encoded
- * @return the size of the encoded data
- * @see <a href="https://developers.google.com/protocol-buffers/docs/encoding#varints">
- * Protocol Buffers Encoding</a>
- */
- private static int getEncodedSize(int value) {
- if (value < 0) {
- 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;
- }
-
- private PacketFactory() {}
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/PacketFactoryException.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/PacketFactoryException.java
deleted file mode 100644
index 41c11fc..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/PacketFactoryException.java
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.connection;
-
-/**
- * Exception for signaling {@link PacketFactory} errors.
- */
-class PacketFactoryException extends Exception {
- PacketFactoryException(String message) {
- super(message);
- }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/ReconnectSecureChannel.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/ReconnectSecureChannel.java
deleted file mode 100644
index 71c879a..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/ReconnectSecureChannel.java
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.connection;
-
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-
-import android.car.encryptionrunner.EncryptionRunner;
-import android.car.encryptionrunner.EncryptionRunnerFactory;
-import android.car.encryptionrunner.HandshakeException;
-import android.car.encryptionrunner.HandshakeMessage;
-import android.car.encryptionrunner.HandshakeMessage.HandshakeState;
-import android.car.encryptionrunner.Key;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
-import com.android.car.connecteddevice.util.ByteUtils;
-
-import java.util.Arrays;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * A secure channel established with the reconnection flow.
- */
-public class ReconnectSecureChannel extends SecureChannel {
-
- private static final String TAG = "ReconnectSecureChannel";
-
- private final ConnectedDeviceStorage mStorage;
-
- private final String mDeviceId;
-
- private final byte[] mExpectedChallengeResponse;
-
- @HandshakeState
- private int mState = HandshakeState.UNKNOWN;
-
- private AtomicBoolean mHasVerifiedDevice = new AtomicBoolean(false);
-
- /**
- * Create a new secure reconnection channel.
- *
- * @param stream The {@link DeviceMessageStream} for communication with the device.
- * @param storage {@link ConnectedDeviceStorage} for secure storage.
- * @param deviceId Id of the device being reconnected.
- * @param expectedChallengeResponse Expected response to challenge issued in reconnect. Should
- * pass {@code null} when device verification is not needed
- * during the reconnection process.
- */
- public ReconnectSecureChannel(@NonNull DeviceMessageStream stream,
- @NonNull ConnectedDeviceStorage storage, @NonNull String deviceId,
- @Nullable byte[] expectedChallengeResponse) {
- super(stream, newReconnectRunner());
- mStorage = storage;
- mDeviceId = deviceId;
- if (expectedChallengeResponse == null) {
- // Skip the device verification step for spp reconnection
- mHasVerifiedDevice.set(true);
- }
- mExpectedChallengeResponse = expectedChallengeResponse;
- }
-
- private static EncryptionRunner newReconnectRunner() {
- EncryptionRunner encryptionRunner = EncryptionRunnerFactory.newRunner();
- encryptionRunner.setIsReconnect(true);
- return encryptionRunner;
- }
-
- @Override
- void processHandshake(byte[] message) throws HandshakeException {
- switch (mState) {
- case HandshakeState.UNKNOWN:
- if (!mHasVerifiedDevice.get()) {
- processHandshakeDeviceVerification(message);
- } else {
- processHandshakeInitialization(message);
- }
- break;
- case HandshakeState.IN_PROGRESS:
- processHandshakeInProgress(message);
- break;
- case HandshakeState.RESUMING_SESSION:
- processHandshakeResumingSession(message);
- break;
- default:
- loge(TAG, "Encountered unexpected handshake state: " + mState + ".");
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
- }
- }
-
- private void processHandshakeDeviceVerification(byte[] message) {
- byte[] challengeResponse = Arrays.copyOf(message,
- mExpectedChallengeResponse.length);
- byte[] deviceChallenge = Arrays.copyOfRange(message,
- mExpectedChallengeResponse.length, message.length);
- if (!Arrays.equals(mExpectedChallengeResponse, challengeResponse)) {
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_ENCRYPTION_KEY);
- return;
- }
- logd(TAG, "Responding to challenge " + ByteUtils.byteArrayToHexString(deviceChallenge)
- + ".");
- byte[] deviceChallengeResponse = mStorage.hashWithChallengeSecret(mDeviceId,
- deviceChallenge);
- if (deviceChallengeResponse == null) {
- notifySecureChannelFailure(CHANNEL_ERROR_STORAGE_ERROR);
- }
- sendHandshakeMessage(deviceChallengeResponse, /* isEncrypted= */ false);
- mHasVerifiedDevice.set(true);
- }
-
- private void processHandshakeInitialization(byte[] message) throws HandshakeException {
- logd(TAG, "Responding to handshake init request.");
- HandshakeMessage handshakeMessage = getEncryptionRunner().respondToInitRequest(message);
- mState = handshakeMessage.getHandshakeState();
- sendHandshakeMessage(handshakeMessage.getNextMessage(), /* isEncrypted= */ false);
- }
-
- private void processHandshakeInProgress(@NonNull byte[] message) throws HandshakeException {
- logd(TAG, "Continuing handshake.");
- HandshakeMessage handshakeMessage = getEncryptionRunner().continueHandshake(message);
- mState = handshakeMessage.getHandshakeState();
- }
-
- private void processHandshakeResumingSession(@NonNull byte[] message)
- throws HandshakeException {
- logd(TAG, "Start reconnection authentication.");
-
- byte[] previousKey = mStorage.getEncryptionKey(mDeviceId);
- if (previousKey == null) {
- loge(TAG, "Unable to resume session, previous key is null.");
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_ENCRYPTION_KEY);
- return;
- }
-
- HandshakeMessage handshakeMessage = getEncryptionRunner().authenticateReconnection(message,
- previousKey);
- mState = handshakeMessage.getHandshakeState();
- if (mState != HandshakeState.FINISHED) {
- loge(TAG, "Unable to resume session, unexpected next handshake state: " + mState + ".");
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
- return;
- }
-
- Key newKey = handshakeMessage.getKey();
- if (newKey == null) {
- loge(TAG, "Unable to resume session, new key is null.");
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_ENCRYPTION_KEY);
- return;
- }
-
- logd(TAG, "Saved new key for reconnection.");
- mStorage.saveEncryptionKey(mDeviceId, newKey.asBytes());
- setEncryptionKey(newKey);
- sendServerAuthToClient(handshakeMessage.getNextMessage());
- notifyCallback(Callback::onSecureChannelEstablished);
- }
-
- private void sendServerAuthToClient(@Nullable byte[] message) {
- if (message == null) {
- loge(TAG, "Unable to send server authentication message to client, message is null.");
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_MSG);
- return;
- }
-
- sendHandshakeMessage(message, /* isEncrypted= */ false);
- }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/SecureChannel.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/SecureChannel.java
deleted file mode 100644
index e6ad290..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/SecureChannel.java
+++ /dev/null
@@ -1,289 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.connection;
-
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-
-import android.car.encryptionrunner.EncryptionRunner;
-import android.car.encryptionrunner.HandshakeException;
-import android.car.encryptionrunner.Key;
-
-import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-
-import com.android.car.connecteddevice.StreamProtos.OperationProto.OperationType;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.security.SignatureException;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Consumer;
-
-/**
- * Establishes a secure channel with {@link EncryptionRunner} over {@link DeviceMessageStream} as
- * server side, sends and receives messages securely after the secure channel has been established.
- */
-public abstract class SecureChannel {
-
- private static final String TAG = "SecureChannel";
-
- @Retention(RetentionPolicy.SOURCE)
- @IntDef({
- CHANNEL_ERROR_INVALID_HANDSHAKE,
- CHANNEL_ERROR_INVALID_MSG,
- CHANNEL_ERROR_INVALID_DEVICE_ID,
- CHANNEL_ERROR_INVALID_VERIFICATION,
- CHANNEL_ERROR_INVALID_STATE,
- CHANNEL_ERROR_INVALID_ENCRYPTION_KEY,
- CHANNEL_ERROR_STORAGE_ERROR
- })
- @interface ChannelError { }
-
- /** Indicates an error during a Handshake of EncryptionRunner. */
- static final int CHANNEL_ERROR_INVALID_HANDSHAKE = 0;
- /** Received an invalid handshake message or has an invalid handshake message to send. */
- static final int CHANNEL_ERROR_INVALID_MSG = 1;
- /** Unable to retrieve a valid id. */
- static final int CHANNEL_ERROR_INVALID_DEVICE_ID = 2;
- /** Unable to get verification code or there's a error during pin verification. */
- static final int CHANNEL_ERROR_INVALID_VERIFICATION = 3;
- /** Encountered an unexpected handshake state. */
- static final int CHANNEL_ERROR_INVALID_STATE = 4;
- /** Failed to get a valid previous/new encryption key. */
- static final int CHANNEL_ERROR_INVALID_ENCRYPTION_KEY = 5;
- /** Failed to save or retrieve security keys. */
- static final int CHANNEL_ERROR_STORAGE_ERROR = 6;
-
-
- private final DeviceMessageStream mStream;
-
- private final EncryptionRunner mEncryptionRunner;
-
- private final AtomicReference<Key> mEncryptionKey = new AtomicReference<>();
-
- private Callback mCallback;
-
- SecureChannel(@NonNull DeviceMessageStream stream,
- @NonNull EncryptionRunner encryptionRunner) {
- mStream = stream;
- mEncryptionRunner = encryptionRunner;
- mStream.setMessageReceivedListener(this::onMessageReceived);
- }
-
- /** Logic for processing a handshake message from device. */
- abstract void processHandshake(byte[] message) throws HandshakeException;
-
- void sendHandshakeMessage(@Nullable byte[] message, boolean isEncrypted) {
- if (message == null) {
- loge(TAG, "Unable to send next handshake message, message is null.");
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_MSG);
- return;
- }
-
- logd(TAG, "Sending handshake message.");
- DeviceMessage deviceMessage = new DeviceMessage(/* recipient= */ null,
- isEncrypted, message);
- if (deviceMessage.isMessageEncrypted()) {
- encryptMessage(deviceMessage);
- }
- mStream.writeMessage(deviceMessage, OperationType.ENCRYPTION_HANDSHAKE);
- }
-
- /** Set the encryption key that secures this channel. */
- void setEncryptionKey(@Nullable Key encryptionKey) {
- mEncryptionKey.set(encryptionKey);
- }
-
- /**
- * Send a client message.
- * <p>Note: This should be called with an encrypted message only after the secure channel has
- * been established.</p>
- *
- * @param deviceMessage The {@link DeviceMessage} to send.
- */
- public void sendClientMessage(@NonNull DeviceMessage deviceMessage)
- throws IllegalStateException {
- if (deviceMessage.isMessageEncrypted()) {
- encryptMessage(deviceMessage);
- }
- mStream.writeMessage(deviceMessage, OperationType.CLIENT_MESSAGE);
- }
-
- private void encryptMessage(@NonNull DeviceMessage deviceMessage) {
- Key key = mEncryptionKey.get();
- if (key == null) {
- throw new IllegalStateException("Secure channel has not been established.");
- }
-
- byte[] encryptedMessage = key.encryptData(deviceMessage.getMessage());
- deviceMessage.setMessage(encryptedMessage);
- }
-
- /** Get the BLE stream backing this channel. */
- @NonNull
- public DeviceMessageStream getStream() {
- return mStream;
- }
-
- /** Register a callback that notifies secure channel events. */
- public void registerCallback(Callback callback) {
- mCallback = callback;
- }
-
- /** Unregister a callback. */
- void unregisterCallback(Callback callback) {
- if (callback == mCallback) {
- mCallback = null;
- }
- }
-
- @VisibleForTesting
- @Nullable
- public Callback getCallback() {
- return mCallback;
- }
-
- void notifyCallback(@NonNull Consumer<Callback> notification) {
- if (mCallback != null) {
- notification.accept(mCallback);
- }
- }
-
- /** Notify callbacks that an error has occurred. */
- void notifySecureChannelFailure(@ChannelError int error) {
- loge(TAG, "Secure channel error: " + error);
- notifyCallback(callback -> callback.onEstablishSecureChannelFailure(error));
- }
-
- /** Return the {@link EncryptionRunner} for this channel. */
- @NonNull
- EncryptionRunner getEncryptionRunner() {
- return mEncryptionRunner;
- }
-
- /**
- * Process the inner message and replace with decrypted value if necessary. If an error occurs
- * the inner message will be replaced with {@code null} and call
- * {@link Callback#onMessageReceivedError(Exception)} on the registered callback.
- *
- * @param deviceMessage The message to process.
- * @return {@code true} if message was successfully processed. {@code false} if an error
- * occurred.
- */
- @VisibleForTesting
- boolean processMessage(@NonNull DeviceMessage deviceMessage) {
- if (!deviceMessage.isMessageEncrypted()) {
- logd(TAG, "Message was not decrypted. No further action necessary.");
- return true;
- }
- Key key = mEncryptionKey.get();
- if (key == null) {
- loge(TAG, "Received encrypted message before secure channel has "
- + "been established.");
- notifyCallback(callback -> callback.onMessageReceivedError(null));
- deviceMessage.setMessage(null);
- return false;
- }
- try {
- byte[] decryptedMessage = key.decryptData(deviceMessage.getMessage());
- deviceMessage.setMessage(decryptedMessage);
- logd(TAG, "Decrypted secure message.");
- return true;
- } catch (SignatureException e) {
- loge(TAG, "Could not decrypt client credentials.", e);
- notifyCallback(callback -> callback.onMessageReceivedError(e));
- deviceMessage.setMessage(null);
-
- return false;
- }
- }
-
- @VisibleForTesting
- void onMessageReceived(@NonNull DeviceMessage deviceMessage, OperationType operationType) {
- boolean success = processMessage(deviceMessage);
- switch(operationType) {
- case ENCRYPTION_HANDSHAKE:
- if (!success) {
- notifyCallback(callback -> callback.onEstablishSecureChannelFailure(
- CHANNEL_ERROR_INVALID_HANDSHAKE));
- break;
- }
- logd(TAG, "Received handshake message.");
- try {
- processHandshake(deviceMessage.getMessage());
- } catch (HandshakeException e) {
- loge(TAG, "Handshake failed.", e);
- notifyCallback(callback -> callback.onEstablishSecureChannelFailure(
- CHANNEL_ERROR_INVALID_HANDSHAKE));
- }
- break;
- case CLIENT_MESSAGE:
- if (!success || deviceMessage.getMessage() == null) {
- break;
- }
- logd(TAG, "Received client message.");
- notifyCallback(
- callback -> callback.onMessageReceived(deviceMessage));
- break;
- default:
- loge(TAG, "Received unexpected operation type: " + operationType + ".");
- }
- }
-
- /**
- * Callbacks that will be invoked during establishing secure channel, sending and receiving
- * messages securely.
- */
- public interface Callback {
- /**
- * Invoked when secure channel has been established successfully.
- */
- default void onSecureChannelEstablished() { }
-
- /**
- * Invoked when a {@link ChannelError} has been encountered in attempting to establish
- * a secure channel.
- *
- * @param error The failure indication.
- */
- default void onEstablishSecureChannelFailure(@SecureChannel.ChannelError int error) { }
-
- /**
- * Invoked when a complete message is received securely from the client and decrypted.
- *
- * @param deviceMessage The {@link DeviceMessage} with decrypted message.
- */
- default void onMessageReceived(@NonNull DeviceMessage deviceMessage) { }
-
- /**
- * Invoked when there was an error during a processing or decrypting of a client message.
- *
- * @param exception The error.
- */
- default void onMessageReceivedError(@Nullable Exception exception) { }
-
- /**
- * Invoked when the device id was received from the client.
- *
- * @param deviceId The unique device id of client.
- */
- default void onDeviceIdReceived(@NonNull String deviceId) { }
- }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/ble/BleCentralManager.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/ble/BleCentralManager.java
deleted file mode 100644
index 7a663f7..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/ble/BleCentralManager.java
+++ /dev/null
@@ -1,195 +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.connecteddevice.connection.ble;
-
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-import static com.android.car.connecteddevice.util.SafeLog.logw;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.le.BluetoothLeScanner;
-import android.bluetooth.le.ScanCallback;
-import android.bluetooth.le.ScanFilter;
-import android.bluetooth.le.ScanResult;
-import android.bluetooth.le.ScanSettings;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.os.Handler;
-
-import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.List;
-import java.util.concurrent.atomic.AtomicInteger;
-
-/**
- * Class that manages BLE scanning operations.
- */
-public class BleCentralManager {
-
- private static final String TAG = "BleCentralManager";
-
- private static final int RETRY_LIMIT = 5;
-
- private static final int RETRY_INTERVAL_MS = 1000;
-
- private final Context mContext;
-
- private final Handler mHandler;
-
- private List<ScanFilter> mScanFilters;
-
- private ScanSettings mScanSettings;
-
- private ScanCallback mScanCallback;
-
- private BluetoothLeScanner mScanner;
-
- private int mScannerStartCount = 0;
-
- private AtomicInteger mScannerState = new AtomicInteger(STOPPED);
- @Retention(RetentionPolicy.SOURCE)
- @IntDef({
- STOPPED,
- STARTED,
- SCANNING
- })
- private @interface ScannerState {}
- private static final int STOPPED = 0;
- private static final int STARTED = 1;
- private static final int SCANNING = 2;
-
- public BleCentralManager(@NonNull Context context) {
- mContext = context;
- mHandler = new Handler(context.getMainLooper());
- }
-
- /**
- * Start the BLE scanning process.
- *
- * @param filters Optional list of {@link ScanFilter}s to apply to scan results.
- * @param settings {@link ScanSettings} to apply to scanner.
- * @param callback {@link ScanCallback} for scan events.
- */
- public void startScanning(@Nullable List<ScanFilter> filters, @NonNull ScanSettings settings,
- @NonNull ScanCallback callback) {
- if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
- loge(TAG, "Attempted start scanning, but system does not support BLE. Ignoring");
- return;
- }
- logd(TAG, "Request received to start scanning.");
- mScannerStartCount = 0;
- mScanFilters = filters;
- mScanSettings = settings;
- mScanCallback = callback;
- updateScannerState(STARTED);
- startScanningInternally();
- }
-
- /** Stop the scanner */
- public void stopScanning() {
- logd(TAG, "Attempting to stop scanning");
- if (mScanner != null) {
- mScanner.stopScan(mInternalScanCallback);
- }
- mScanCallback = null;
- updateScannerState(STOPPED);
- }
-
- /** Returns {@code true} if currently scanning, {@code false} otherwise. */
- public boolean isScanning() {
- return mScannerState.get() == SCANNING;
- }
-
- /** Clean up the scanning process. */
- public void cleanup() {
- if (isScanning()) {
- stopScanning();
- }
- }
-
- private void startScanningInternally() {
- logd(TAG, "Attempting to start scanning");
- if (mScanner == null && BluetoothAdapter.getDefaultAdapter() != null) {
- mScanner = BluetoothAdapter.getDefaultAdapter().getBluetoothLeScanner();
- }
- if (mScanner != null) {
- mScanner.startScan(mScanFilters, mScanSettings, mInternalScanCallback);
- updateScannerState(SCANNING);
- } else {
- mHandler.postDelayed(() -> {
- // Keep trying
- logd(TAG, "Scanner unavailable. Trying again.");
- startScanningInternally();
- }, RETRY_INTERVAL_MS);
- }
- }
-
- private void updateScannerState(@ScannerState int newState) {
- mScannerState.set(newState);
- }
-
- private final ScanCallback mInternalScanCallback = new ScanCallback() {
- @Override
- public void onScanResult(int callbackType, ScanResult result) {
- if (mScanCallback != null) {
- mScanCallback.onScanResult(callbackType, result);
- }
- }
-
- @Override
- public void onBatchScanResults(List<ScanResult> results) {
- logd(TAG, "Batch scan found " + results.size() + " results.");
- if (mScanCallback != null) {
- mScanCallback.onBatchScanResults(results);
- }
- }
-
- @Override
- public void onScanFailed(int errorCode) {
- if (mScannerStartCount >= RETRY_LIMIT) {
- loge(TAG, "Cannot start BLE Scanner. Scanning Retry count: "
- + mScannerStartCount);
- if (mScanCallback != null) {
- mScanCallback.onScanFailed(errorCode);
- }
- return;
- }
-
- mScannerStartCount++;
- logw(TAG, "BLE Scanner failed to start. Error: "
- + errorCode
- + " Retry: "
- + mScannerStartCount);
- switch(errorCode) {
- case SCAN_FAILED_ALREADY_STARTED:
- // Scanner already started. Do nothing.
- break;
- case SCAN_FAILED_APPLICATION_REGISTRATION_FAILED:
- case SCAN_FAILED_INTERNAL_ERROR:
- mHandler.postDelayed(BleCentralManager.this::startScanningInternally,
- RETRY_INTERVAL_MS);
- break;
- default:
- // Ignore other codes.
- }
- }
- };
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/ble/BleDeviceMessageStream.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/ble/BleDeviceMessageStream.java
deleted file mode 100644
index 864475f..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/ble/BleDeviceMessageStream.java
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.connection.ble;
-
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-import static com.android.car.connecteddevice.util.SafeLog.logw;
-
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.os.Handler;
-import android.os.Looper;
-
-import androidx.annotation.NonNull;
-
-import com.android.car.connecteddevice.connection.DeviceMessageStream;
-
-import java.util.concurrent.atomic.AtomicLong;
-
-/** BLE message stream to a device. */
-public class BleDeviceMessageStream extends DeviceMessageStream {
-
- private static final String TAG = "BleDeviceMessageStream";
-
- /*
- * During bandwidth testing, it was discovered that allowing the stream to send as fast as it
- * can blocked outgoing notifications from being received by the connected device. Adding a
- * throttle to the outgoing messages alleviated this block and allowed both sides to
- * send/receive in parallel successfully.
- */
- private static final long THROTTLE_DEFAULT_MS = 10L;
- private static final long THROTTLE_WAIT_MS = 75L;
-
- private final Handler mHandler = new Handler(Looper.getMainLooper());
-
- private final AtomicLong mThrottleDelay = new AtomicLong(THROTTLE_DEFAULT_MS);
-
- private final BlePeripheralManager mBlePeripheralManager;
-
- private final BluetoothDevice mDevice;
-
- private final BluetoothGattCharacteristic mWriteCharacteristic;
-
- private final BluetoothGattCharacteristic mReadCharacteristic;
-
- BleDeviceMessageStream(@NonNull BlePeripheralManager blePeripheralManager,
- @NonNull BluetoothDevice device,
- @NonNull BluetoothGattCharacteristic writeCharacteristic,
- @NonNull BluetoothGattCharacteristic readCharacteristic,
- int defaultMaxWriteSize) {
- super(defaultMaxWriteSize);
- mBlePeripheralManager = blePeripheralManager;
- mDevice = device;
- mWriteCharacteristic = writeCharacteristic;
- mReadCharacteristic = readCharacteristic;
- mBlePeripheralManager.addOnCharacteristicWriteListener(this::onCharacteristicWrite);
- mBlePeripheralManager.addOnCharacteristicReadListener(this::onCharacteristicRead);
- }
-
- @Override
- protected void send(byte[] data) {
- mWriteCharacteristic.setValue(data);
- mBlePeripheralManager.notifyCharacteristicChanged(mDevice, mWriteCharacteristic,
- /* confirm= */ false);
- }
-
- private void onCharacteristicRead(@NonNull BluetoothDevice device) {
- if (!mDevice.equals(device)) {
- logw(TAG, "Received a read notification from a device (" + device.getAddress()
- + ") that is not the expected device (" + mDevice.getAddress() + ") registered "
- + "to this stream. Ignoring.");
- return;
- }
-
- logd(TAG, "Releasing lock on characteristic.");
- sendCompleted();
- }
-
- private void onCharacteristicWrite(@NonNull BluetoothDevice device,
- @NonNull BluetoothGattCharacteristic characteristic, @NonNull byte[] value) {
- logd(TAG, "Received a message from a device (" + device.getAddress() + ").");
- if (!mDevice.equals(device)) {
- logw(TAG, "Received a message from a device (" + device.getAddress() + ") that is not "
- + "the expected device (" + mDevice.getAddress() + ") registered to this "
- + "stream. Ignoring.");
- return;
- }
-
- if (!characteristic.getUuid().equals(mReadCharacteristic.getUuid())) {
- logw(TAG, "Received a write to a characteristic (" + characteristic.getUuid() + ") that"
- + " is not the expected UUID (" + mReadCharacteristic.getUuid() + "). "
- + "Ignoring.");
- return;
- }
- onDataReceived(value);
- }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/ble/BlePeripheralManager.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/ble/BlePeripheralManager.java
deleted file mode 100644
index 645cecb..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/ble/BlePeripheralManager.java
+++ /dev/null
@@ -1,534 +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.connecteddevice.connection.ble;
-
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-import static com.android.car.connecteddevice.util.SafeLog.logw;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothGatt;
-import android.bluetooth.BluetoothGattCallback;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattDescriptor;
-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 androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.car.connecteddevice.util.ByteUtils;
-
-import java.util.HashSet;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.CopyOnWriteArraySet;
-import java.util.concurrent.atomic.AtomicReference;
-
-/**
- * A generic class that manages BLE peripheral operations like start/stop advertising, notifying
- * connects/disconnects and reading/writing values to GATT characteristics.
- */
-// TODO(b/123248433) This could move to a separate comms library.
-public class BlePeripheralManager {
- private static final String TAG = "BlePeripheralManager";
-
- private static final int BLE_RETRY_LIMIT = 5;
- private static final int BLE_RETRY_INTERVAL_MS = 1000;
-
- private static final int GATT_SERVER_RETRY_LIMIT = 20;
- private static final int GATT_SERVER_RETRY_DELAY_MS = 200;
-
- // 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
- private static final UUID DEVICE_NAME_UUID =
- UUID.fromString("00002a00-0000-1000-8000-00805f9b34fb");
-
- private final Handler mHandler;
-
- private final Context mContext;
- private final Set<Callback> mCallbacks = new CopyOnWriteArraySet<>();
- private final Set<OnCharacteristicWriteListener> mWriteListeners = new HashSet<>();
- private final Set<OnCharacteristicReadListener> mReadListeners = new HashSet<>();
- private final AtomicReference<BluetoothGattServer> mGattServer = new AtomicReference<>();
- private final AtomicReference<BluetoothGatt> mBluetoothGatt = new AtomicReference<>();
-
- private int mMtuSize = 20;
-
- private BluetoothManager mBluetoothManager;
- private AtomicReference<BluetoothLeAdvertiser> mAdvertiser = new AtomicReference<>();
- private int mAdvertiserStartCount;
- private int mGattServerRetryStartCount;
- private BluetoothGattService mBluetoothGattService;
- private AdvertiseCallback mAdvertiseCallback;
- private AdvertiseData mAdvertiseData;
- private AdvertiseData mScanResponse;
-
- public BlePeripheralManager(Context context) {
- mContext = context;
- mHandler = new Handler(mContext.getMainLooper());
- }
-
- /**
- * Registers the given callback to be notified of various events within the {@link
- * BlePeripheralManager}.
- *
- * @param callback The callback to be notified.
- */
- void registerCallback(@NonNull Callback callback) {
- mCallbacks.add(callback);
- }
-
- /**
- * Unregisters a previously registered callback.
- *
- * @param callback The callback to unregister.
- */
- void unregisterCallback(@NonNull Callback callback) {
- mCallbacks.remove(callback);
- }
-
- /**
- * Adds a listener to be notified of a write to characteristics.
- *
- * @param listener The listener to invoke.
- */
- void addOnCharacteristicWriteListener(@NonNull OnCharacteristicWriteListener listener) {
- mWriteListeners.add(listener);
- }
-
- /**
- * Removes the given listener from being notified of characteristic writes.
- *
- * @param listener The listener to remove.
- */
- void removeOnCharacteristicWriteListener(@NonNull OnCharacteristicWriteListener listener) {
- mWriteListeners.remove(listener);
- }
-
- /**
- * Adds a listener to be notified of reads to characteristics.
- *
- * @param listener The listener to invoke.
- */
- void addOnCharacteristicReadListener(@NonNull OnCharacteristicReadListener listener) {
- mReadListeners.add(listener);
- }
-
- /**
- * Removes the given listener from being notified of characteristic reads.
- *
- * @param listener The listener to remove.
- */
- void removeOnCharacteristicReadistener(@NonNull OnCharacteristicReadListener listener) {
- mReadListeners.remove(listener);
- }
-
- /**
- * Returns the current MTU size.
- *
- * @return The size of the MTU in bytes.
- */
- int getMtuSize() {
- return mMtuSize;
- }
-
- /**
- * 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
- * @param data {@link AdvertiseData} data to advertise
- * @param scanResponse {@link AdvertiseData} scan response
- * @param advertiseCallback {@link AdvertiseCallback} callback for advertiser
- */
- void startAdvertising(
- BluetoothGattService service, AdvertiseData data,
- AdvertiseData scanResponse, AdvertiseCallback advertiseCallback) {
- logd(TAG, "Request to start advertising with service " + service.getUuid() + ".");
- if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
- loge(TAG, "Attempted start advertising, but system does not support BLE. Ignoring.");
- return;
- }
- // Clears previous session before starting advertising.
- cleanup();
- mBluetoothGattService = service;
- mAdvertiseCallback = advertiseCallback;
- mAdvertiseData = data;
- mScanResponse = scanResponse;
- mGattServerRetryStartCount = 0;
- mBluetoothManager = (BluetoothManager) mContext.getSystemService(Context.BLUETOOTH_SERVICE);
- mGattServer.set(mBluetoothManager.openGattServer(mContext, mGattServerCallback));
- openGattServer();
- }
-
- /**
- * Stops the GATT server from advertising.
- *
- * @param advertiseCallback The callback that is associated with the advertisement.
- */
- void stopAdvertising(AdvertiseCallback advertiseCallback) {
- BluetoothLeAdvertiser advertiser = mAdvertiser.getAndSet(null);
- if (advertiser != null) {
- advertiser.stopAdvertising(advertiseCallback);
- logd(TAG, "Advertising stopped.");
- }
- }
-
- /**
- * Notifies the characteristic change via {@link BluetoothGattServer}
- */
- void notifyCharacteristicChanged(
- @NonNull BluetoothDevice device,
- @NonNull BluetoothGattCharacteristic characteristic,
- boolean confirm) {
- BluetoothGattServer gattServer = mGattServer.get();
- if (gattServer == null) {
- return;
- }
-
- if (!gattServer.notifyCharacteristicChanged(device, characteristic, confirm)) {
- loge(TAG, "notifyCharacteristicChanged failed");
- }
- }
-
- /**
- * Connect the Gatt server of the remote device to retrieve device name.
- */
- final void retrieveDeviceName(BluetoothDevice device) {
- mBluetoothGatt.compareAndSet(null, device.connectGatt(mContext, false, mGattCallback));
- }
-
- /**
- * Cleans up the BLE GATT server state.
- */
- void cleanup() {
- logd(TAG, "Cleaning up manager.");
- // Stops the advertiser, scanner and GATT server. This needs to be done to avoid leaks.
- stopAdvertising(mAdvertiseCallback);
- // Clears all registered listeners. IHU only supports single connection in peripheral role.
- mReadListeners.clear();
- mWriteListeners.clear();
-
- BluetoothGattServer gattServer = mGattServer.getAndSet(null);
- if (gattServer == null) {
- return;
- }
-
- logd(TAG, "Stopping gatt server.");
- BluetoothGatt bluetoothGatt = mBluetoothGatt.getAndSet(null);
- if (bluetoothGatt != null) {
- gattServer.cancelConnection(bluetoothGatt.getDevice());
- logd(TAG, "Disconnecting gatt.");
- bluetoothGatt.disconnect();
- bluetoothGatt.close();
- }
- gattServer.clearServices();
- gattServer.close();
- }
-
- private void openGattServer() {
- // Only open one Gatt server.
- BluetoothGattServer gattServer = mGattServer.get();
- if (gattServer != null) {
- logd(TAG, "Gatt Server created, retry count: " + mGattServerRetryStartCount);
- gattServer.clearServices();
- gattServer.addService(mBluetoothGattService);
- AdvertiseSettings settings =
- new AdvertiseSettings.Builder()
- .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
- .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
- .setConnectable(true)
- .build();
- mAdvertiserStartCount = 0;
- startAdvertisingInternally(settings, mAdvertiseData, mScanResponse, mAdvertiseCallback);
- mGattServerRetryStartCount = 0;
- } else if (mGattServerRetryStartCount < GATT_SERVER_RETRY_LIMIT) {
- mGattServer.set(mBluetoothManager.openGattServer(mContext, mGattServerCallback));
- mGattServerRetryStartCount++;
- mHandler.postDelayed(() -> openGattServer(), GATT_SERVER_RETRY_DELAY_MS);
- } else {
- loge(TAG, "Gatt server not created - exceeded retry limit.");
- }
- }
-
- private void startAdvertisingInternally(
- AdvertiseSettings settings, AdvertiseData advertisement,
- AdvertiseData scanResponse, AdvertiseCallback advertiseCallback) {
- if (BluetoothAdapter.getDefaultAdapter() != null) {
- mAdvertiser.compareAndSet(null,
- BluetoothAdapter.getDefaultAdapter().getBluetoothLeAdvertiser());
- }
- BluetoothLeAdvertiser advertiser = mAdvertiser.get();
- if (advertiser != null) {
- logd(TAG, "Advertiser created, retry count: " + mAdvertiserStartCount);
- advertiser.startAdvertising(settings, advertisement, scanResponse, advertiseCallback);
- mAdvertiserStartCount = 0;
- } else if (mAdvertiserStartCount < BLE_RETRY_LIMIT) {
- mHandler.postDelayed(
- () -> startAdvertisingInternally(settings, advertisement, scanResponse,
- advertiseCallback), BLE_RETRY_INTERVAL_MS);
- mAdvertiserStartCount += 1;
- } else {
- loge(TAG, "Cannot start BLE Advertisement. Advertise Retry count: "
- + mAdvertiserStartCount);
- }
- }
-
- private final BluetoothGattServerCallback mGattServerCallback =
- new BluetoothGattServerCallback() {
- @Override
- public void onConnectionStateChange(BluetoothDevice device, int status,
- int newState) {
- switch (newState) {
- case BluetoothProfile.STATE_CONNECTED:
- logd(TAG, "BLE Connection State Change: CONNECTED");
- BluetoothGattServer gattServer = mGattServer.get();
- if (gattServer == null) {
- return;
- }
- gattServer.connect(device, /* autoConnect= */ false);
- for (Callback callback : mCallbacks) {
- callback.onRemoteDeviceConnected(device);
- }
- break;
- case BluetoothProfile.STATE_DISCONNECTED:
- logd(TAG, "BLE Connection State Change: DISCONNECTED");
- for (Callback callback : mCallbacks) {
- callback.onRemoteDeviceDisconnected(device);
- }
- break;
- default:
- logw(TAG, "Connection state not connecting or disconnecting; ignoring: "
- + newState);
- }
- }
-
- @Override
- public void onServiceAdded(int status, BluetoothGattService service) {
- logd(TAG, "Service added status: " + status + " uuid: " + service.getUuid());
- }
-
- @Override
- public void onCharacteristicWriteRequest(
- BluetoothDevice device,
- int requestId,
- BluetoothGattCharacteristic characteristic,
- boolean preparedWrite,
- boolean responseNeeded,
- int offset,
- byte[] value) {
- BluetoothGattServer gattServer = mGattServer.get();
- if (gattServer == null) {
- return;
- }
- gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset,
- value);
- for (OnCharacteristicWriteListener listener : mWriteListeners) {
- listener.onCharacteristicWrite(device, characteristic, value);
- }
- }
-
- @Override
- public void onDescriptorWriteRequest(
- BluetoothDevice device,
- int requestId,
- BluetoothGattDescriptor descriptor,
- boolean preparedWrite,
- boolean responseNeeded,
- int offset,
- byte[] value) {
- logd(TAG, "Write request for descriptor: "
- + descriptor.getUuid()
- + "; value: "
- + ByteUtils.byteArrayToHexString(value));
- BluetoothGattServer gattServer = mGattServer.get();
- if (gattServer == null) {
- return;
- }
- gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset,
- value);
- }
-
- @Override
- public void onMtuChanged(BluetoothDevice device, int mtu) {
- logd(TAG, "onMtuChanged: " + mtu + " for device " + device.getAddress());
-
- mMtuSize = mtu;
-
- for (Callback callback : mCallbacks) {
- callback.onMtuSizeChanged(mtu);
- }
- }
-
- @Override
- public void onNotificationSent(BluetoothDevice device, int status) {
- super.onNotificationSent(device, status);
- if (status == BluetoothGatt.GATT_SUCCESS) {
- logd(TAG, "Notification sent successfully. Device: " + device.getAddress()
- + ", Status: " + status + ". Notifying all listeners.");
- for (OnCharacteristicReadListener listener : mReadListeners) {
- listener.onCharacteristicRead(device);
- }
- } else {
- loge(TAG, "Notification failed. Device: " + device + ", Status: "
- + status);
- }
- }
- };
-
- private final BluetoothGattCallback mGattCallback =
- new BluetoothGattCallback() {
- @Override
- public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
- logd(TAG, "Gatt Connection State Change: " + newState);
- switch (newState) {
- case BluetoothProfile.STATE_CONNECTED:
- logd(TAG, "Gatt connected");
- BluetoothGatt bluetoothGatt = mBluetoothGatt.get();
- if (bluetoothGatt == null) {
- break;
- }
- bluetoothGatt.discoverServices();
- break;
- case BluetoothProfile.STATE_DISCONNECTED:
- logd(TAG, "Gatt Disconnected");
- break;
- default:
- logd(TAG, "Connection state not connecting or disconnecting; ignoring: "
- + newState);
- }
- }
-
- @Override
- public void onServicesDiscovered(BluetoothGatt gatt, int status) {
- logd(TAG, "Gatt Services Discovered");
- BluetoothGatt bluetoothGatt = mBluetoothGatt.get();
- if (bluetoothGatt == null) {
- return;
- }
- BluetoothGattService gapService = bluetoothGatt.getService(
- GENERIC_ACCESS_PROFILE_UUID);
- if (gapService == null) {
- loge(TAG, "Generic Access Service is null.");
- return;
- }
- BluetoothGattCharacteristic deviceNameCharacteristic =
- gapService.getCharacteristic(DEVICE_NAME_UUID);
- if (deviceNameCharacteristic == null) {
- loge(TAG, "Device Name Characteristic is null.");
- return;
- }
- bluetoothGatt.readCharacteristic(deviceNameCharacteristic);
- }
-
- @Override
- public void onCharacteristicRead(
- BluetoothGatt gatt, BluetoothGattCharacteristic characteristic,
- int status) {
- if (status == BluetoothGatt.GATT_SUCCESS) {
- String deviceName = characteristic.getStringValue(0);
- logd(TAG, "BLE Device Name: " + deviceName);
-
- for (Callback callback : mCallbacks) {
- callback.onDeviceNameRetrieved(deviceName);
- }
- } else {
- loge(TAG, "Reading GAP Failed: " + status);
- }
- }
- };
-
- /**
- * Interface to be notified of various events within the {@link BlePeripheralManager}.
- */
- interface Callback {
- /**
- * Triggered when the name of the remote device is retrieved.
- *
- * @param deviceName Name of the remote device.
- */
- void onDeviceNameRetrieved(@Nullable String deviceName);
-
- /**
- * Triggered if a remote client has requested to change the MTU for a given connection.
- *
- * @param size The new MTU size.
- */
- void onMtuSizeChanged(int size);
-
- /**
- * Triggered when a device (GATT client) connected.
- *
- * @param device Remote device that connected on BLE.
- */
- void onRemoteDeviceConnected(@NonNull BluetoothDevice device);
-
- /**
- * Triggered when a device (GATT client) disconnected.
- *
- * @param device Remote device that disconnected on BLE.
- */
- void onRemoteDeviceDisconnected(@NonNull BluetoothDevice device);
- }
-
- /**
- * An interface for classes that wish to be notified of writes to a characteristic.
- */
- interface OnCharacteristicWriteListener {
- /**
- * Triggered when this BlePeripheralManager receives a write request from a remote device.
- *
- * @param device The bluetooth device that holds the characteristic.
- * @param characteristic The characteristic that was written to.
- * @param value The value that was written.
- */
- void onCharacteristicWrite(
- @NonNull BluetoothDevice device,
- @NonNull BluetoothGattCharacteristic characteristic,
- @NonNull byte[] value);
- }
-
- /**
- * An interface for classes that wish to be notified of reads on a characteristic.
- */
- interface OnCharacteristicReadListener {
- /**
- * Triggered when this BlePeripheralManager receives a read request from a remote device.
- *
- * @param device The bluetooth device that holds the characteristic.
- */
- void onCharacteristicRead(@NonNull BluetoothDevice device);
- }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/ble/CarBleCentralManager.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/ble/CarBleCentralManager.java
deleted file mode 100644
index e8d17e5..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/ble/CarBleCentralManager.java
+++ /dev/null
@@ -1,382 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.connection.ble;
-
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-import static com.android.car.connecteddevice.util.SafeLog.logw;
-import static com.android.car.connecteddevice.util.ScanDataAnalyzer.containsUuidsInOverflow;
-
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothGatt;
-import android.bluetooth.BluetoothGattCallback;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattDescriptor;
-import android.bluetooth.BluetoothGattService;
-import android.bluetooth.BluetoothProfile;
-import android.bluetooth.le.ScanCallback;
-import android.bluetooth.le.ScanRecord;
-import android.bluetooth.le.ScanResult;
-import android.bluetooth.le.ScanSettings;
-import android.content.Context;
-import android.os.ParcelUuid;
-
-import androidx.annotation.NonNull;
-
-import com.android.car.connecteddevice.AssociationCallback;
-import com.android.car.connecteddevice.connection.CarBluetoothManager;
-import com.android.car.connecteddevice.oob.OobChannel;
-import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
-
-import java.math.BigInteger;
-import java.util.List;
-import java.util.UUID;
-import java.util.concurrent.CopyOnWriteArraySet;
-
-/**
- * Communication manager for a car that maintains continuous connections with all devices in the car
- * for the duration of a drive.
- */
-public class CarBleCentralManager extends CarBluetoothManager {
-
- private static final String TAG = "CarBleCentralManager";
-
- // system/bt/internal_include/bt_target.h#GATT_MAX_PHY_CHANNEL
- private static final int MAX_CONNECTIONS = 7;
-
- private static final UUID CHARACTERISTIC_CONFIG =
- UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
-
- private static final int STATUS_FORCED_DISCONNECT = -1;
-
- private final ScanSettings mScanSettings = new ScanSettings.Builder()
- .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
- .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
- .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
- .build();
-
- private final CopyOnWriteArraySet<ConnectedRemoteDevice> mIgnoredDevices =
- new CopyOnWriteArraySet<>();
-
- private final Context mContext;
-
- private final BleCentralManager mBleCentralManager;
-
- private final UUID mServiceUuid;
-
- private final UUID mWriteCharacteristicUuid;
-
- private final UUID mReadCharacteristicUuid;
-
- private final BigInteger mParsedBgServiceBitMask;
-
- /**
- * Create a new manager.
- *
- * @param context The caller's [Context].
- * @param bleCentralManager [BleCentralManager] for establishing connections.
- * @param connectedDeviceStorage Shared [ConnectedDeviceStorage] for companion features.
- * @param serviceUuid [UUID] of peripheral's service.
- * @param bgServiceMask iOS overflow bit mask for service UUID.
- * @param writeCharacteristicUuid [UUID] of characteristic the car will write to.
- * @param readCharacteristicUuid [UUID] of characteristic the device will write to.
- */
- public CarBleCentralManager(
- @NonNull Context context,
- @NonNull BleCentralManager bleCentralManager,
- @NonNull ConnectedDeviceStorage connectedDeviceStorage,
- @NonNull UUID serviceUuid,
- @NonNull String bgServiceMask,
- @NonNull UUID writeCharacteristicUuid,
- @NonNull UUID readCharacteristicUuid) {
- super(connectedDeviceStorage);
- mContext = context;
- mBleCentralManager = bleCentralManager;
- mServiceUuid = serviceUuid;
- mWriteCharacteristicUuid = writeCharacteristicUuid;
- mReadCharacteristicUuid = readCharacteristicUuid;
- mParsedBgServiceBitMask = new BigInteger(bgServiceMask, 16);
- }
-
- @Override
- public void start() {
- super.start();
- mBleCentralManager.startScanning(/* filters= */ null, mScanSettings, mScanCallback);
- }
-
- @Override
- public void stop() {
- super.stop();
- mBleCentralManager.stopScanning();
- }
-
- @Override
- public void disconnectDevice(String deviceId) {
- logd(TAG, "Request to disconnect from device " + deviceId + ".");
- ConnectedRemoteDevice device = getConnectedDevice(deviceId);
- if (device == null) {
- return;
- }
-
- deviceDisconnected(device, STATUS_FORCED_DISCONNECT);
- }
-
- //TODO(b/141312136): Support car central role
- @Override
- public AssociationCallback getAssociationCallback() {
- return null;
- }
-
- @Override
- public void setAssociationCallback(AssociationCallback callback) {
-
- }
-
- @Override
- public void connectToDevice(UUID deviceId) {
-
- }
-
- @Override
- public void initiateConnectionToDevice(UUID deviceId) {
-
- }
-
- @Override
- public void startAssociation(String nameForAssociation, AssociationCallback callback) {
-
- }
-
- @Override
- public void startOutOfBandAssociation(String nameForAssociation, OobChannel oobChannel,
- AssociationCallback callback) {
-
- }
-
- private void ignoreDevice(@NonNull ConnectedRemoteDevice device) {
- mIgnoredDevices.add(device);
- }
-
- private boolean isDeviceIgnored(@NonNull BluetoothDevice device) {
- for (ConnectedRemoteDevice connectedDevice : mIgnoredDevices) {
- if (device.equals(connectedDevice.mDevice)) {
- return true;
- }
- }
- return false;
- }
-
- private boolean shouldAttemptConnection(@NonNull ScanResult result) {
- // Ignore any results that are not connectable.
- if (!result.isConnectable()) {
- return false;
- }
-
- // Do not attempt to connect if we have already hit our max. This should rarely happen
- // and is protecting against a race condition of scanning stopped and new results coming in.
- if (getConnectedDevicesCount() >= MAX_CONNECTIONS) {
- return false;
- }
-
- BluetoothDevice device = result.getDevice();
-
- // Do not connect if device has already been ignored.
- if (isDeviceIgnored(device)) {
- return false;
- }
-
- // Check if already attempting to connect to this device.
- if (getConnectedDevice(device) != null) {
- return false;
- }
-
-
- // Ignore any device without a scan record.
- ScanRecord scanRecord = result.getScanRecord();
- if (scanRecord == null) {
- return false;
- }
-
- // Connect to any device that is advertising our service UUID.
- List<ParcelUuid> serviceUuids = scanRecord.getServiceUuids();
- if (serviceUuids != null) {
- for (ParcelUuid serviceUuid : serviceUuids) {
- if (serviceUuid.getUuid().equals(mServiceUuid)) {
- return true;
- }
- }
- }
- if (containsUuidsInOverflow(scanRecord.getBytes(), mParsedBgServiceBitMask)) {
- return true;
- }
-
- // Can safely ignore devices advertising unrecognized service uuids.
- if (serviceUuids != null && !serviceUuids.isEmpty()) {
- return false;
- }
-
- // TODO(b/139066293): Current implementation quickly exhausts connections resulting in
- // greatly reduced performance for connecting to devices we know we want to connect to.
- // Return true once fixed.
- return false;
- }
-
- private void startDeviceConnection(@NonNull BluetoothDevice device) {
- BluetoothGatt gatt = device.connectGatt(mContext, /* autoConnect= */ false,
- mConnectionCallback, BluetoothDevice.TRANSPORT_LE);
- if (gatt == null) {
- return;
- }
-
- ConnectedRemoteDevice bleDevice = new ConnectedRemoteDevice(device, gatt);
- bleDevice.mState = ConnectedDeviceState.CONNECTING;
- addConnectedDevice(bleDevice);
-
- // Stop scanning if we have reached the maximum number of connections.
- if (getConnectedDevicesCount() >= MAX_CONNECTIONS) {
- mBleCentralManager.stopScanning();
- }
- }
-
- private void deviceConnected(@NonNull ConnectedRemoteDevice device) {
- if (device.mGatt == null) {
- loge(TAG, "Device connected with null gatt. Disconnecting.");
- deviceDisconnected(device, BluetoothProfile.STATE_DISCONNECTED);
- return;
- }
- device.mState = ConnectedDeviceState.PENDING_VERIFICATION;
- device.mGatt.discoverServices();
- logd(TAG, "New device connected: " + device.mGatt.getDevice().getAddress()
- + ". Active connections: " + getConnectedDevicesCount() + ".");
- }
-
- private void deviceDisconnected(@NonNull ConnectedRemoteDevice device, int status) {
- removeConnectedDevice(device);
- if (device.mGatt != null) {
- device.mGatt.close();
- }
- if (device.mDeviceId != null) {
- mCallbacks.invoke(callback -> callback.onDeviceDisconnected(device.mDeviceId));
- }
- logd(TAG, "Device with id " + device.mDeviceId + " disconnected with state " + status
- + ". Remaining active connections: " + getConnectedDevicesCount() + ".");
- }
-
- private final ScanCallback mScanCallback = new ScanCallback() {
- @Override
- public void onScanResult(int callbackType, ScanResult result) {
- super.onScanResult(callbackType, result);
- if (shouldAttemptConnection(result)) {
- startDeviceConnection(result.getDevice());
- }
- }
-
- @Override
- public void onScanFailed(int errorCode) {
- super.onScanFailed(errorCode);
- loge(TAG, "BLE scanning failed with error code: " + errorCode);
- }
- };
-
- private final BluetoothGattCallback mConnectionCallback = new BluetoothGattCallback() {
- @Override
- public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
- super.onConnectionStateChange(gatt, status, newState);
- if (gatt == null) {
- logw(TAG, "Null gatt passed to onConnectionStateChange. Ignoring.");
- return;
- }
-
- ConnectedRemoteDevice connectedDevice = getConnectedDevice(gatt);
- if (connectedDevice == null) {
- return;
- }
-
- switch (newState) {
- case BluetoothProfile.STATE_CONNECTED:
- deviceConnected(connectedDevice);
- break;
- case BluetoothProfile.STATE_DISCONNECTED:
- deviceDisconnected(connectedDevice, status);
- break;
- default:
- logd(TAG, "Connection state changed. New state: " + newState + " status: "
- + status);
- }
- }
-
- @Override
- public void onServicesDiscovered(BluetoothGatt gatt, int status) {
- super.onServicesDiscovered(gatt, status);
- if (gatt == null) {
- logw(TAG, "Null gatt passed to onServicesDiscovered. Ignoring.");
- return;
- }
-
- ConnectedRemoteDevice connectedDevice = getConnectedDevice(gatt);
- if (connectedDevice == null) {
- return;
- }
- BluetoothGattService service = gatt.getService(mServiceUuid);
- if (service == null) {
- ignoreDevice(connectedDevice);
- gatt.disconnect();
- return;
- }
-
- connectedDevice.mState = ConnectedDeviceState.CONNECTED;
- BluetoothGattCharacteristic writeCharacteristic =
- service.getCharacteristic(mWriteCharacteristicUuid);
- BluetoothGattCharacteristic readCharacteristic =
- service.getCharacteristic(mReadCharacteristicUuid);
- if (writeCharacteristic == null || readCharacteristic == null) {
- logw(TAG, "Unable to find expected characteristics on peripheral.");
- gatt.disconnect();
- return;
- }
-
- // Turn on notifications for read characteristic.
- BluetoothGattDescriptor descriptor =
- readCharacteristic.getDescriptor(CHARACTERISTIC_CONFIG);
- descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
- if (!gatt.writeDescriptor(descriptor)) {
- loge(TAG, "Write descriptor to read characteristic failed.");
- gatt.disconnect();
- return;
- }
-
- if (!gatt.setCharacteristicNotification(readCharacteristic, /* enable= */ true)) {
- loge(TAG, "Set notifications to read characteristic failed.");
- gatt.disconnect();
- return;
- }
-
- logd(TAG, "Service and characteristics successfully discovered.");
- }
-
- @Override
- public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor,
- int status) {
- super.onDescriptorWrite(gatt, descriptor, status);
- if (gatt == null) {
- logw(TAG, "Null gatt passed to onDescriptorWrite. Ignoring.");
- return;
- }
- // TODO(b/141312136): Create SecureBleChannel and assign to connectedDevice.
- }
- };
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/ble/CarBlePeripheralManager.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/ble/CarBlePeripheralManager.java
deleted file mode 100644
index a05b5f7..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/ble/CarBlePeripheralManager.java
+++ /dev/null
@@ -1,510 +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.connecteddevice.connection.ble;
-
-import static com.android.car.connecteddevice.ConnectedDeviceManager.DEVICE_ERROR_UNEXPECTED_DISCONNECTION;
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-import static com.android.car.connecteddevice.util.SafeLog.logw;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattDescriptor;
-import android.bluetooth.BluetoothGattService;
-import android.bluetooth.le.AdvertiseCallback;
-import android.bluetooth.le.AdvertiseData;
-import android.bluetooth.le.AdvertiseSettings;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.ParcelUuid;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.car.connecteddevice.AssociationCallback;
-import com.android.car.connecteddevice.connection.AssociationSecureChannel;
-import com.android.car.connecteddevice.connection.CarBluetoothManager;
-import com.android.car.connecteddevice.connection.OobAssociationSecureChannel;
-import com.android.car.connecteddevice.connection.ReconnectSecureChannel;
-import com.android.car.connecteddevice.connection.SecureChannel;
-import com.android.car.connecteddevice.oob.OobChannel;
-import com.android.car.connecteddevice.oob.OobConnectionManager;
-import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
-import com.android.car.connecteddevice.util.ByteUtils;
-import com.android.car.connecteddevice.util.EventLog;
-
-import java.time.Duration;
-import java.util.Arrays;
-import java.util.UUID;
-
-/**
- * Communication manager that allows for targeted connections to a specific device in the car.
- */
-public class CarBlePeripheralManager extends CarBluetoothManager {
-
- private static final String TAG = "CarBlePeripheralManager";
-
- // Attribute protocol bytes attached to message. Available write size is MTU size minus att
- // bytes.
- private static final int ATT_PROTOCOL_BYTES = 3;
-
- private static final UUID CLIENT_CHARACTERISTIC_CONFIG =
- UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
-
- private static final int SALT_BYTES = 8;
-
- private static final int TOTAL_AD_DATA_BYTES = 16;
-
- private static final int TRUNCATED_BYTES = 3;
-
- private static final String TIMEOUT_HANDLER_THREAD_NAME = "peripheralThread";
-
- private final BluetoothGattDescriptor mDescriptor =
- new BluetoothGattDescriptor(CLIENT_CHARACTERISTIC_CONFIG,
- BluetoothGattDescriptor.PERMISSION_READ
- | BluetoothGattDescriptor.PERMISSION_WRITE);
-
- private final BlePeripheralManager mBlePeripheralManager;
-
- private final UUID mAssociationServiceUuid;
-
- private final UUID mReconnectServiceUuid;
-
- private final UUID mReconnectDataUuid;
-
- private final BluetoothGattCharacteristic mWriteCharacteristic;
-
- private final BluetoothGattCharacteristic mReadCharacteristic;
-
- private HandlerThread mTimeoutHandlerThread;
-
- private Handler mTimeoutHandler;
-
- private final Duration mMaxReconnectAdvertisementDuration;
-
- private final int mDefaultMtuSize;
-
- private String mReconnectDeviceId;
-
- private byte[] mReconnectChallenge;
-
- private AdvertiseCallback mAdvertiseCallback;
-
- private OobConnectionManager mOobConnectionManager;
-
- private AssociationCallback mAssociationCallback;
-
- /**
- * Initialize a new instance of manager.
- *
- * @param blePeripheralManager {@link BlePeripheralManager} for establishing connection.
- * @param connectedDeviceStorage Shared {@link ConnectedDeviceStorage} for companion features.
- * @param associationServiceUuid {@link UUID} of association service.
- * @param reconnectServiceUuid {@link UUID} of reconnect service.
- * @param reconnectDataUuid {@link UUID} key of reconnect advertisement data.
- * @param writeCharacteristicUuid {@link UUID} of characteristic the car will write to.
- * @param readCharacteristicUuid {@link UUID} of characteristic the device will write to.
- * @param maxReconnectAdvertisementDuration Maximum duration to advertise for reconnect before
- * restarting.
- * @param defaultMtuSize Default MTU size for new channels.
- */
- public CarBlePeripheralManager(@NonNull BlePeripheralManager blePeripheralManager,
- @NonNull ConnectedDeviceStorage connectedDeviceStorage,
- @NonNull UUID associationServiceUuid,
- @NonNull UUID reconnectServiceUuid,
- @NonNull UUID reconnectDataUuid,
- @NonNull UUID writeCharacteristicUuid,
- @NonNull UUID readCharacteristicUuid,
- @NonNull Duration maxReconnectAdvertisementDuration,
- int defaultMtuSize) {
- super(connectedDeviceStorage);
- mBlePeripheralManager = blePeripheralManager;
- mAssociationServiceUuid = associationServiceUuid;
- mReconnectServiceUuid = reconnectServiceUuid;
- mReconnectDataUuid = reconnectDataUuid;
- mDescriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);
- mWriteCharacteristic = new BluetoothGattCharacteristic(writeCharacteristicUuid,
- BluetoothGattCharacteristic.PROPERTY_NOTIFY,
- BluetoothGattCharacteristic.PROPERTY_READ);
- mReadCharacteristic = new BluetoothGattCharacteristic(readCharacteristicUuid,
- BluetoothGattCharacteristic.PROPERTY_WRITE
- | BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE,
- BluetoothGattCharacteristic.PERMISSION_WRITE);
- mReadCharacteristic.addDescriptor(mDescriptor);
- mMaxReconnectAdvertisementDuration = maxReconnectAdvertisementDuration;
- mDefaultMtuSize = defaultMtuSize;
- }
-
- @Override
- public void start() {
- super.start();
- mTimeoutHandlerThread = new HandlerThread(TIMEOUT_HANDLER_THREAD_NAME);
- mTimeoutHandlerThread.start();
- mTimeoutHandler = new Handler(mTimeoutHandlerThread.getLooper());
- }
-
- @Override
- public void stop() {
- super.stop();
- if (mTimeoutHandlerThread != null) {
- mTimeoutHandlerThread.quit();
- }
- reset();
- }
-
- @Override
- public void disconnectDevice(@NonNull String deviceId) {
- if (deviceId.equals(mReconnectDeviceId)) {
- logd(TAG, "Reconnection canceled for device " + deviceId + ".");
- reset();
- return;
- }
- ConnectedRemoteDevice connectedDevice = getConnectedDevice();
- if (connectedDevice == null || !deviceId.equals(connectedDevice.mDeviceId)) {
- return;
- }
- reset();
- }
-
- @Override
- public AssociationCallback getAssociationCallback() {
- return mAssociationCallback;
- }
-
- @Override
- public void setAssociationCallback(AssociationCallback callback) {
- mAssociationCallback = callback;
- }
-
- @Override
- public void reset() {
- super.reset();
- logd(TAG, "Resetting state.");
- mBlePeripheralManager.cleanup();
- mReconnectDeviceId = null;
- mReconnectChallenge = null;
- mOobConnectionManager = null;
- mAssociationCallback = null;
- }
-
- @Override
- public void initiateConnectionToDevice(@NonNull UUID deviceId) {
- mReconnectDeviceId = deviceId.toString();
- mAdvertiseCallback = new AdvertiseCallback() {
- @Override
- public void onStartSuccess(AdvertiseSettings settingsInEffect) {
- super.onStartSuccess(settingsInEffect);
- mTimeoutHandler.postDelayed(mTimeoutRunnable,
- mMaxReconnectAdvertisementDuration.toMillis());
- logd(TAG, "Successfully started advertising for device " + deviceId + ".");
- }
- };
- mBlePeripheralManager.unregisterCallback(mAssociationPeripheralCallback);
- mBlePeripheralManager.registerCallback(mReconnectPeripheralCallback);
- mTimeoutHandler.removeCallbacks(mTimeoutRunnable);
- byte[] advertiseData = createReconnectData(mReconnectDeviceId);
- if (advertiseData == null) {
- loge(TAG, "Unable to create advertisement data. Aborting reconnect.");
- return;
- }
- startAdvertising(mReconnectServiceUuid, mAdvertiseCallback, advertiseData,
- mReconnectDataUuid, /* scanResponse= */ null, /* scanResponseUuid= */ null);
- }
-
- /**
- * Create data for reconnection advertisement.
- *
- * <p></p><p>Process:</p>
- * <ol>
- * <li>Generate random {@value SALT_BYTES} byte salt and zero-pad to
- * {@value TOTAL_AD_DATA_BYTES} bytes.
- * <li>Hash with stored challenge secret and truncate to {@value TRUNCATED_BYTES} bytes.
- * <li>Concatenate hashed {@value TRUNCATED_BYTES} bytes with salt and return.
- * </ol>
- */
- @Nullable
- private byte[] createReconnectData(String deviceId) {
- byte[] salt = ByteUtils.randomBytes(SALT_BYTES);
- byte[] zeroPadded = ByteUtils.concatByteArrays(salt,
- new byte[TOTAL_AD_DATA_BYTES - SALT_BYTES]);
- mReconnectChallenge = mStorage.hashWithChallengeSecret(deviceId, zeroPadded);
- if (mReconnectChallenge == null) {
- return null;
- }
- return ByteUtils.concatByteArrays(Arrays.copyOf(mReconnectChallenge, TRUNCATED_BYTES),
- salt);
-
- }
-
- @Override
- public void startAssociation(@NonNull String nameForAssociation,
- @NonNull AssociationCallback callback) {
- BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
- if (adapter == null) {
- loge(TAG, "Bluetooth is unavailable on this device. Unable to start associating.");
- return;
- }
-
- reset();
- mAssociationCallback = callback;
- mBlePeripheralManager.unregisterCallback(mReconnectPeripheralCallback);
- mBlePeripheralManager.registerCallback(mAssociationPeripheralCallback);
- mAdvertiseCallback = new AdvertiseCallback() {
- @Override
- public void onStartSuccess(AdvertiseSettings settingsInEffect) {
- super.onStartSuccess(settingsInEffect);
- callback.onAssociationStartSuccess(nameForAssociation);
- logd(TAG, "Successfully started advertising for association.");
- }
-
- @Override
- public void onStartFailure(int errorCode) {
- super.onStartFailure(errorCode);
- callback.onAssociationStartFailure();
- logd(TAG, "Failed to start advertising for association. Error code: " + errorCode);
- }
- };
- startAdvertising(mAssociationServiceUuid, mAdvertiseCallback, /* advertiseData= */null,
- /* advertiseDataUuid= */ null, nameForAssociation.getBytes(), mReconnectDataUuid);
- }
-
- /** Start the association with a new device using out of band verification code exchange */
- @Override
- public void startOutOfBandAssociation(
- @NonNull String nameForAssociation,
- @NonNull OobChannel oobChannel,
- @NonNull AssociationCallback callback) {
-
- logd(TAG, "Starting out of band association.");
- startAssociation(nameForAssociation, new AssociationCallback() {
- @Override
- public void onAssociationStartSuccess(String deviceName) {
- mAssociationCallback = callback;
- boolean success = mOobConnectionManager.startOobExchange(oobChannel);
- if (!success) {
- callback.onAssociationStartFailure();
- return;
- }
- callback.onAssociationStartSuccess(deviceName);
- }
-
- @Override
- public void onAssociationStartFailure() {
- callback.onAssociationStartFailure();
- }
- });
- mOobConnectionManager = new OobConnectionManager();
- }
-
- private void startAdvertising(@NonNull UUID serviceUuid, @NonNull AdvertiseCallback callback,
- @Nullable byte[] advertiseData,
- @Nullable UUID advertiseDataUuid, @Nullable byte[] scanResponse,
- @Nullable UUID scanResponseUuid) {
- BluetoothGattService gattService = new BluetoothGattService(serviceUuid,
- BluetoothGattService.SERVICE_TYPE_PRIMARY);
- gattService.addCharacteristic(mWriteCharacteristic);
- gattService.addCharacteristic(mReadCharacteristic);
-
- AdvertiseData.Builder advertisementBuilder =
- new AdvertiseData.Builder();
- ParcelUuid uuid = new ParcelUuid(serviceUuid);
- advertisementBuilder.addServiceUuid(uuid);
- if (advertiseData != null) {
- ParcelUuid dataUuid = uuid;
- if (advertiseDataUuid != null) {
- dataUuid = new ParcelUuid(advertiseDataUuid);
- }
- advertisementBuilder.addServiceData(dataUuid, advertiseData);
- }
-
- AdvertiseData.Builder scanResponseBuilder =
- new AdvertiseData.Builder();
- if (scanResponse != null && scanResponseUuid != null) {
- ParcelUuid scanResponseParcelUuid = new ParcelUuid(scanResponseUuid);
- scanResponseBuilder.addServiceData(scanResponseParcelUuid, scanResponse);
- }
-
- mBlePeripheralManager.startAdvertising(gattService, advertisementBuilder.build(),
- scanResponseBuilder.build(), callback);
- }
-
- private void addConnectedDevice(BluetoothDevice device, boolean isReconnect) {
- addConnectedDevice(device, isReconnect, /* oobConnectionManager= */ null);
- }
-
- private void addConnectedDevice(@NonNull BluetoothDevice device, boolean isReconnect,
- @Nullable OobConnectionManager oobConnectionManager) {
- EventLog.onDeviceConnected();
- mBlePeripheralManager.stopAdvertising(mAdvertiseCallback);
- if (mTimeoutHandler != null) {
- mTimeoutHandler.removeCallbacks(mTimeoutRunnable);
- }
-
- if (device.getName() == null) {
- logd(TAG, "Device connected, but name is null; issuing request to retrieve device "
- + "name.");
- mBlePeripheralManager.retrieveDeviceName(device);
- } else {
- setClientDeviceName(device.getName());
- }
- setClientDeviceAddress(device.getAddress());
-
- BleDeviceMessageStream secureStream = new BleDeviceMessageStream(mBlePeripheralManager,
- device, mWriteCharacteristic, mReadCharacteristic,
- mDefaultMtuSize - ATT_PROTOCOL_BYTES);
- secureStream.setMessageReceivedErrorListener(
- exception -> {
- disconnectWithError("Error occurred in stream: " + exception.getMessage());
- });
- SecureChannel secureChannel;
- if (isReconnect) {
- secureChannel = new ReconnectSecureChannel(secureStream, mStorage, mReconnectDeviceId,
- mReconnectChallenge);
- } else if (oobConnectionManager != null) {
- secureChannel = new OobAssociationSecureChannel(secureStream, mStorage,
- oobConnectionManager);
- } else {
- secureChannel = new AssociationSecureChannel(secureStream, mStorage);
- }
- secureChannel.registerCallback(mSecureChannelCallback);
- ConnectedRemoteDevice connectedDevice = new ConnectedRemoteDevice(device, /* gatt= */ null);
- connectedDevice.mSecureChannel = secureChannel;
- addConnectedDevice(connectedDevice);
- if (isReconnect) {
- setDeviceIdAndNotifyCallbacks(mReconnectDeviceId);
- mReconnectDeviceId = null;
- mReconnectChallenge = null;
- }
- }
-
- private void setMtuSize(int mtuSize) {
- ConnectedRemoteDevice connectedDevice = getConnectedDevice();
- if (connectedDevice != null
- && connectedDevice.mSecureChannel != null
- && connectedDevice.mSecureChannel.getStream() != null) {
- ((BleDeviceMessageStream) connectedDevice.mSecureChannel.getStream())
- .setMaxWriteSize(mtuSize - ATT_PROTOCOL_BYTES);
- }
- }
-
- private final BlePeripheralManager.Callback mReconnectPeripheralCallback =
- new BlePeripheralManager.Callback() {
-
- @Override
- public void onDeviceNameRetrieved(String deviceName) {
- // Ignored.
- }
-
- @Override
- public void onMtuSizeChanged(int size) {
- setMtuSize(size);
- }
-
- @Override
- public void onRemoteDeviceConnected(BluetoothDevice device) {
- addConnectedDevice(device, /* isReconnect= */ true);
- }
-
- @Override
- public void onRemoteDeviceDisconnected(BluetoothDevice device) {
- String deviceId = mReconnectDeviceId;
- ConnectedRemoteDevice connectedDevice = getConnectedDevice(device);
- // Reset before invoking callbacks to avoid a race condition with reconnect
- // logic.
- reset();
- if (connectedDevice != null) {
- deviceId = connectedDevice.mDeviceId;
- }
- final String finalDeviceId = deviceId;
- if (finalDeviceId == null) {
- logw(TAG, "Callbacks were not issued for disconnect because the device id "
- + "was null.");
- return;
- }
- logd(TAG, "Connected device " + finalDeviceId + " disconnected.");
- mCallbacks.invoke(callback -> callback.onDeviceDisconnected(finalDeviceId));
- }
- };
-
- private final BlePeripheralManager.Callback mAssociationPeripheralCallback =
- new BlePeripheralManager.Callback() {
- @Override
- public void onDeviceNameRetrieved(String deviceName) {
- if (deviceName == null) {
- return;
- }
- setClientDeviceName(deviceName);
- ConnectedRemoteDevice connectedDevice = getConnectedDevice();
- if (connectedDevice == null || connectedDevice.mDeviceId == null) {
- return;
- }
- mStorage.updateAssociatedDeviceName(connectedDevice.mDeviceId, deviceName);
- }
-
- @Override
- public void onMtuSizeChanged(int size) {
- setMtuSize(size);
- }
-
- @Override
- public void onRemoteDeviceConnected(BluetoothDevice device) {
- addConnectedDevice(device, /* isReconnect= */ false, mOobConnectionManager);
- ConnectedRemoteDevice connectedDevice = getConnectedDevice();
- if (connectedDevice == null || connectedDevice.mSecureChannel == null) {
- return;
- }
- ((AssociationSecureChannel) connectedDevice.mSecureChannel)
- .setShowVerificationCodeListener(
- code -> {
- if (!isAssociating()) {
- loge(TAG, "No valid callback for association.");
- return;
- }
- mAssociationCallback.onVerificationCodeAvailable(code);
- });
- }
-
- @Override
- public void onRemoteDeviceDisconnected(BluetoothDevice device) {
- logd(TAG, "Remote device disconnected.");
- ConnectedRemoteDevice connectedDevice = getConnectedDevice(device);
- if (isAssociating()) {
- mAssociationCallback.onAssociationError(
- DEVICE_ERROR_UNEXPECTED_DISCONNECTION);
- }
- // Reset before invoking callbacks to avoid a race condition with reconnect
- // logic.
- reset();
- if (connectedDevice == null || connectedDevice.mDeviceId == null) {
- logw(TAG, "Callbacks were not issued for disconnect.");
- return;
- }
- mCallbacks.invoke(callback -> callback.onDeviceDisconnected(
- connectedDevice.mDeviceId));
- }
- };
-
- private final Runnable mTimeoutRunnable = new Runnable() {
- @Override
- public void run() {
- logd(TAG, "Timeout period expired without a connection. Restarting advertisement.");
- mBlePeripheralManager.stopAdvertising(mAdvertiseCallback);
- connectToDevice(UUID.fromString(mReconnectDeviceId));
- }
- };
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/spp/AcceptTask.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/spp/AcceptTask.java
deleted file mode 100644
index 2ada256..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/spp/AcceptTask.java
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.connection.spp;
-
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothServerSocket;
-import android.bluetooth.BluetoothSocket;
-
-import androidx.annotation.Nullable;
-
-import java.io.IOException;
-import java.util.UUID;
-
-/**
- * This task runs while listening for incoming connections. It behaves like a server. It runs until
- * a connection is accepted (or until cancelled).
- */
-public class AcceptTask implements Runnable {
- private static final String TAG = "AcceptTask";
- private static final String SERVICE_NAME_SECURE = "NAME_SECURE";
- private static final String SERVICE_NAME_INSECURE = "NAME_INSECURE";
- private final UUID mServiceUuid;
- private final boolean mIsSecure;
- private final OnTaskCompletedListener mListener;
- private final BluetoothAdapter mAdapter;
- private BluetoothServerSocket mServerSocket;
-
- AcceptTask(BluetoothAdapter adapter, boolean isSecure, UUID serviceUuid,
- OnTaskCompletedListener listener) {
- mListener = listener;
- mAdapter = adapter;
- mServiceUuid = serviceUuid;
- mIsSecure = isSecure;
- }
-
- /**
- * Start the socket to listen to any incoming connection request.
- *
- * @return {@code true} if listening is started successfully.
- */
- boolean startListening() {
- // Create a new listening server socket
- try {
- if (mIsSecure) {
- mServerSocket = mAdapter.listenUsingRfcommWithServiceRecord(SERVICE_NAME_SECURE,
- mServiceUuid);
- } else {
- mServerSocket = mAdapter.listenUsingInsecureRfcommWithServiceRecord(
- SERVICE_NAME_INSECURE, mServiceUuid);
- }
- } catch (IOException e) {
- loge(TAG, "Socket listen() failed", e);
- return false;
- }
- return true;
- }
-
- @Override
- public void run() {
- logd(TAG, "BEGIN AcceptTask: " + this);
- BluetoothSocket socket = null;
-
- // Listen to the server socket if we're not connected
- while (true) {
- try {
- socket = mServerSocket.accept();
- } catch (IOException e) {
- loge(TAG, "accept() failed", e);
- break;
- }
- if (socket != null) {
- break;
- }
- }
-
- mListener.onTaskCompleted(socket, mIsSecure);
- }
-
- void cancel() {
- logd(TAG, "CANCEL AcceptTask: " + this);
- try {
- if (mServerSocket != null) {
- mServerSocket.close();
- }
- } catch (IOException e) {
- loge(TAG, "close() of server failed", e);
- }
- }
-
- interface OnTaskCompletedListener {
- /**
- * Will be called when the accept task is completed.
- *
- * @param socket will be {@code null} if the task failed.
- * @param isSecure is {@code true} when it is listening to a secure RFCOMM channel.
- */
- void onTaskCompleted(@Nullable BluetoothSocket socket, boolean isSecure);
- }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/spp/CarSppManager.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/spp/CarSppManager.java
deleted file mode 100644
index e9f0cc1..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/spp/CarSppManager.java
+++ /dev/null
@@ -1,276 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.connection.spp;
-
-import static com.android.car.connecteddevice.ConnectedDeviceManager.DEVICE_ERROR_UNEXPECTED_DISCONNECTION;
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-
-import androidx.annotation.NonNull;
-
-import com.android.car.connecteddevice.AssociationCallback;
-import com.android.car.connecteddevice.connection.AssociationSecureChannel;
-import com.android.car.connecteddevice.connection.CarBluetoothManager;
-import com.android.car.connecteddevice.connection.DeviceMessageStream;
-import com.android.car.connecteddevice.connection.ReconnectSecureChannel;
-import com.android.car.connecteddevice.connection.SecureChannel;
-import com.android.car.connecteddevice.oob.OobChannel;
-import com.android.car.connecteddevice.oob.OobConnectionManager;
-import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
-import com.android.car.connecteddevice.util.EventLog;
-
-import java.util.UUID;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-
-
-/**
- * Communication manager that allows for targeted connections to a specific device from the car
- * using {@link SppManager} .
- */
-public class CarSppManager extends CarBluetoothManager {
-
- private static final String TAG = "CarSppManager";
-
- private final SppManager mSppManager;
-
- private final UUID mAssociationServiceUuid;
-
- private final int mPacketMaxBytes;
-
- private String mReconnectDeviceId;
-
- private OobConnectionManager mOobConnectionManager;
-
- private Executor mCallbackExecutor;
-
- private AssociationCallback mAssociationCallback;
-
- /**
- * Initialize a new instance of manager.
- *
- * @param sppManager {@link SppManager} for establishing connection.
- * @param connectedDeviceStorage Shared {@link ConnectedDeviceStorage} for companion features.
- * @param packetMaxBytes Maximum size in bytes to write in one packet.
- */
- public CarSppManager(@NonNull SppManager sppManager,
- @NonNull ConnectedDeviceStorage connectedDeviceStorage,
- @NonNull UUID associationServiceUuid,
- int packetMaxBytes) {
- super(connectedDeviceStorage);
- mSppManager = sppManager;
- mCallbackExecutor = Executors.newSingleThreadExecutor();
- mAssociationServiceUuid = associationServiceUuid;
- mPacketMaxBytes = packetMaxBytes;
- }
-
- @Override
- public void stop() {
- super.stop();
- reset();
- }
-
- @Override
- public void disconnectDevice(@NonNull String deviceId) {
- ConnectedRemoteDevice connectedDevice = getConnectedDevice();
- if (connectedDevice == null || !deviceId.equals(connectedDevice.mDeviceId)) {
- return;
- }
- reset();
- }
-
- @Override
- public AssociationCallback getAssociationCallback() {
- return mAssociationCallback;
- }
-
- @Override
- public void setAssociationCallback(AssociationCallback callback) {
- mAssociationCallback = callback;
- }
-
- @Override
- public void initiateConnectionToDevice(@NonNull UUID deviceId) {
- logd(TAG, "Start spp reconnection listening for device with id: " + deviceId.toString());
- mReconnectDeviceId = deviceId.toString();
- mSppManager.unregisterCallback(mAssociationSppCallback);
- mSppManager.registerCallback(mReconnectSppCallback, mCallbackExecutor);
- mSppManager.startListening(deviceId);
- }
-
- @Override
- public void reset() {
- super.reset();
- mReconnectDeviceId = null;
- mAssociationCallback = null;
- mSppManager.cleanup();
- }
-
- /**
- * Start the association by listening to incoming connect request.
- */
- @Override
- public void startAssociation(@NonNull String nameForAssociation,
- @NonNull AssociationCallback callback) {
- BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
- if (adapter == null) {
- loge(TAG, "Bluetooth is unavailable on this device. Unable to start associating.");
- return;
- }
-
- reset();
- mAssociationCallback = callback;
- mSppManager.unregisterCallback(mReconnectSppCallback);
- mSppManager.registerCallback(mAssociationSppCallback, mCallbackExecutor);
- if (mSppManager.startListening(mAssociationServiceUuid)) {
- callback.onAssociationStartSuccess(/* deviceName= */ null);
- } else {
- callback.onAssociationStartFailure();
- }
- }
-
- /**
- * Start the association with a new device using out of band verification code exchange
- */
- @Override
- public void startOutOfBandAssociation(@NonNull String nameForAssociation,
- @NonNull OobChannel oobChannel,
- @NonNull AssociationCallback callback) {
-
- logd(TAG, "Starting out of band association.");
- startAssociation(nameForAssociation, new AssociationCallback() {
- @Override
- public void onAssociationStartSuccess(String deviceName) {
- mAssociationCallback = callback;
- boolean success = mOobConnectionManager.startOobExchange(oobChannel);
- if (!success) {
- callback.onAssociationStartFailure();
- return;
- }
- callback.onAssociationStartSuccess(deviceName);
- }
-
- @Override
- public void onAssociationStartFailure() {
- callback.onAssociationStartFailure();
- }
- });
- mOobConnectionManager = new OobConnectionManager();
- }
-
- private void onDeviceConnected(BluetoothDevice device, boolean isReconnect) {
- onDeviceConnected(device, isReconnect, /* isOob= */ false);
- }
-
- private void onDeviceConnected(BluetoothDevice device, boolean isReconnect, boolean isOob) {
- EventLog.onDeviceConnected();
- setClientDeviceAddress(device.getAddress());
- setClientDeviceName(device.getName());
- DeviceMessageStream secureStream = new SppDeviceMessageStream(mSppManager, device,
- mPacketMaxBytes);
- secureStream.setMessageReceivedErrorListener(
- exception -> {
- disconnectWithError("Error occurred in stream: " + exception.getMessage(),
- exception);
- });
- SecureChannel secureChannel;
- // TODO(b/157492943): Define an out of band version of ReconnectSecureChannel
- if (isReconnect) {
- secureChannel = new ReconnectSecureChannel(secureStream, mStorage, mReconnectDeviceId,
- /* expectedChallengeResponse= */ null);
- } else if (isOob) {
- // TODO(b/160901821): Integrate Oob with Spp channel
- loge(TAG, "Oob verification is currently not available for Spp");
- return;
- } else {
- secureChannel = new AssociationSecureChannel(secureStream, mStorage);
- }
- secureChannel.registerCallback(mSecureChannelCallback);
- ConnectedRemoteDevice connectedDevice = new ConnectedRemoteDevice(device, /* gatt= */ null);
- connectedDevice.mSecureChannel = secureChannel;
- addConnectedDevice(connectedDevice);
- if (isReconnect) {
- setDeviceIdAndNotifyCallbacks(mReconnectDeviceId);
- mReconnectDeviceId = null;
- }
- }
-
- private final SppManager.ConnectionCallback mReconnectSppCallback =
- new SppManager.ConnectionCallback() {
- @Override
- public void onRemoteDeviceConnected(BluetoothDevice device) {
- onDeviceConnected(device, /* isReconnect= */ true);
- }
-
- @Override
- public void onRemoteDeviceDisconnected(BluetoothDevice device) {
- ConnectedRemoteDevice connectedDevice = getConnectedDevice(device);
- // Reset before invoking callbacks to avoid a race condition with reconnect
- // logic.
- reset();
- String deviceId = connectedDevice == null ? mReconnectDeviceId
- : connectedDevice.mDeviceId;
- if (deviceId != null) {
- logd(TAG, "Connected device " + deviceId + " disconnected.");
- mCallbacks.invoke(callback -> callback.onDeviceDisconnected(deviceId));
- }
- }
- };
-
- private final SppManager.ConnectionCallback mAssociationSppCallback =
- new SppManager.ConnectionCallback() {
- @Override
- public void onRemoteDeviceConnected(BluetoothDevice device) {
- onDeviceConnected(device, /* isReconnect= */ false);
- ConnectedRemoteDevice connectedDevice = getConnectedDevice();
- if (connectedDevice == null || connectedDevice.mSecureChannel == null) {
- loge(TAG,
- "No connected device or secure channel found when try to "
- + "associate.");
- return;
- }
- ((AssociationSecureChannel) connectedDevice.mSecureChannel)
- .setShowVerificationCodeListener(
- code -> {
- if (mAssociationCallback == null) {
- loge(TAG, "No valid callback for association.");
- return;
- }
- mAssociationCallback.onVerificationCodeAvailable(code);
- });
- }
-
- @Override
- public void onRemoteDeviceDisconnected(BluetoothDevice device) {
- ConnectedRemoteDevice connectedDevice = getConnectedDevice(device);
- if (isAssociating()) {
- mAssociationCallback.onAssociationError(
- DEVICE_ERROR_UNEXPECTED_DISCONNECTION);
- }
- // Reset before invoking callbacks to avoid a race condition with reconnect
- // logic.
- reset();
- if (connectedDevice != null && connectedDevice.mDeviceId != null) {
- mCallbacks.invoke(callback -> callback.onDeviceDisconnected(
- connectedDevice.mDeviceId));
- }
- }
- };
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/spp/ConnectedTask.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/spp/ConnectedTask.java
deleted file mode 100644
index 2318e69..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/spp/ConnectedTask.java
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.connection.spp;
-
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-import static com.android.car.connecteddevice.util.SafeLog.logi;
-
-import android.bluetooth.BluetoothSocket;
-
-import androidx.annotation.NonNull;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-/**
- * This task runs during a connection with a remote device. It handles all incoming and outgoing
- * data.
- */
-public class ConnectedTask implements Runnable {
- private static final String TAG = "ConnectedTask";
- private final BluetoothSocket mSocket;
- private final InputStream mInputStream;
- private final OutputStream mOutputStream;
- private Callback mCallback;
-
- ConnectedTask(@NonNull InputStream inputStream, @NonNull OutputStream outputStream,
- @NonNull BluetoothSocket socket,
- @NonNull Callback callback) {
- mInputStream = inputStream;
- mOutputStream = outputStream;
- mSocket = socket;
- mCallback = callback;
- }
-
- @Override
- public void run() {
- logi(TAG, "Begin ConnectedTask: started to listen to incoming messages.");
- // Keep listening to the InputStream when task started.
- while (true) {
- try {
- int dataLength = mInputStream.available();
- if (dataLength == 0) {
- continue;
- }
- byte[] buffer = new byte[dataLength];
- // Read from the InputStream
- mInputStream.read(buffer);
- mCallback.onMessageReceived(buffer);
- logd(TAG, "received raw bytes from remote device with length: " + dataLength);
- } catch (IOException e) {
- loge(TAG,
- "Encountered an exception when listening for incoming message, "
- + "disconnected", e);
- mCallback.onDisconnected();
- break;
- }
- }
- }
-
- /**
- * Write to the connected OutputStream.
- *
- * @param buffer The bytes to write
- */
- void write(@NonNull byte[] buffer) {
- try {
- mOutputStream.write(buffer);
- logd(TAG, "Sent buffer to remote device with length: " + buffer.length);
- } catch (IOException e) {
- loge(TAG, "Exception during write", e);
- }
- }
-
- void cancel() {
- logd(TAG, "cancel connected task: close connected socket.");
- try {
- mSocket.close();
- } catch (IOException e) {
- loge(TAG, "close() of connected socket failed", e);
- }
- }
-
- interface Callback {
- void onMessageReceived(@NonNull byte[] message);
-
- void onDisconnected();
- }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/spp/SppDeviceMessageStream.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/spp/SppDeviceMessageStream.java
deleted file mode 100644
index d92ddca..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/spp/SppDeviceMessageStream.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.connection.spp;
-
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-import static com.android.car.connecteddevice.util.SafeLog.logw;
-
-import android.bluetooth.BluetoothDevice;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
-
-import com.android.car.connecteddevice.connection.DeviceMessageStream;
-
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-
-/**
- * Spp message stream to a device.
- */
-class SppDeviceMessageStream extends DeviceMessageStream {
-
- private static final String TAG = "SppDeviceMessageStream";
-
- private final SppManager mSppManager;
- private final BluetoothDevice mDevice;
- private final Executor mCallbackExecutor = Executors.newSingleThreadExecutor();
-
-
- SppDeviceMessageStream(@NonNull SppManager sppManager,
- @NonNull BluetoothDevice device, int maxWriteSize) {
- super(maxWriteSize);
- mSppManager = sppManager;
- mDevice = device;
- mSppManager.addOnMessageReceivedListener(this::onMessageReceived, mCallbackExecutor);
- }
-
- @Override
- protected void send(byte[] data) {
- mSppManager.write(data);
- sendCompleted();
- }
-
- @VisibleForTesting
- void onMessageReceived(@NonNull BluetoothDevice device, @NonNull byte[] value) {
- logd(TAG, "Received a message from a device (" + device.getAddress() + ").");
- if (!mDevice.equals(device)) {
- logw(TAG, "Received a message from a device (" + device.getAddress() + ") that is not "
- + "the expected device (" + mDevice.getAddress() + ") registered to this "
- + "stream. Ignoring.");
- return;
- }
-
- onDataReceived(value);
- }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/spp/SppManager.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/spp/SppManager.java
deleted file mode 100644
index 57a9c89..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/spp/SppManager.java
+++ /dev/null
@@ -1,331 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.connection.spp;
-
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothSocket;
-
-import androidx.annotation.GuardedBy;
-import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
-
-import com.android.car.connecteddevice.util.ThreadSafeCallbacks;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.UUID;
-import java.util.concurrent.Executor;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-
-/**
- * A generic class that handles all the Spp connection events including:
- * <ol>
- * <li>listen and accept connection request from client.
- * <li>send a message through an established connection.
- * <li>notify any connection or message events happening during the connection.
- * </ol>
- */
-public class SppManager {
- private static final String TAG = "SppManager";
- // Service names and UUIDs of SDP(Service Discovery Protocol) record, need to keep it consistent
- // among client and server.
- private final BluetoothAdapter mAdapter = BluetoothAdapter.getDefaultAdapter();
- private final boolean mIsSecure;
- private Object mLock = new Object();
- /**
- * Task to listen to secure RFCOMM channel.
- */
- @VisibleForTesting
- AcceptTask mAcceptTask;
- /**
- * Task to start and maintain a connection.
- */
- @VisibleForTesting
- ConnectedTask mConnectedTask;
- @VisibleForTesting
- ExecutorService mConnectionExecutor = Executors.newSingleThreadExecutor();
- private BluetoothDevice mDevice;
- @GuardedBy("mLock")
- private final SppPayloadStream mPayloadStream = new SppPayloadStream();
- @GuardedBy("mLock")
- @VisibleForTesting
- ConnectionState mState;
- private final ThreadSafeCallbacks<ConnectionCallback> mCallbacks = new ThreadSafeCallbacks<>();
- private final ThreadSafeCallbacks<OnMessageReceivedListener> mReceivedListeners =
- new ThreadSafeCallbacks<>();
-
- public SppManager(@NonNull boolean isSecure) {
- mPayloadStream.setMessageCompletedListener(this::onMessageCompleted);
- mIsSecure = isSecure;
- }
-
- @VisibleForTesting
- enum ConnectionState {
- NONE,
- LISTEN,
- CONNECTING,
- CONNECTED,
- DISCONNECTED,
- }
-
- /**
- * Registers the given callback to be notified of various events within the {@link SppManager}.
- *
- * @param callback The callback to be notified.
- */
- void registerCallback(@NonNull ConnectionCallback callback, @NonNull Executor executor) {
- mCallbacks.add(callback, executor);
- }
-
- /**
- * Unregisters a previously registered callback.
- *
- * @param callback The callback to unregister.
- */
- void unregisterCallback(@NonNull ConnectionCallback callback) {
- mCallbacks.remove(callback);
- }
-
- /**
- * Adds a listener to be notified of a write to characteristics.
- *
- * @param listener The listener to invoke.
- */
- void addOnMessageReceivedListener(@NonNull OnMessageReceivedListener listener,
- @NonNull Executor executor) {
- mReceivedListeners.add(listener, executor);
- }
-
- /**
- * Removes the given listener from being notified of characteristic writes.
- *
- * @param listener The listener to remove.
- */
- void removeOnMessageReceivedListener(@NonNull OnMessageReceivedListener listener) {
- mReceivedListeners.remove(listener);
- }
-
- /**
- * Start listening to connection request from the client.
- *
- * @param serviceUuid The Uuid which the accept task is listening on.
- * @return {@code true} if listening is started successfully
- */
- boolean startListening(@NonNull UUID serviceUuid) {
- logd(TAG, "Start socket to listening to incoming connection request.");
- if (mConnectedTask != null) {
- mConnectedTask.cancel();
- mConnectedTask = null;
- }
-
- // Start the task to listen on a BluetoothServerSocket
- if (mAcceptTask != null) {
- mAcceptTask.cancel();
- }
- mAcceptTask = new AcceptTask(mAdapter, mIsSecure, serviceUuid, mAcceptTaskListener);
- if (!mAcceptTask.startListening()) {
- // TODO(b/159376003): Handle listening error.
- mAcceptTask.cancel();
- mAcceptTask = null;
- return false;
- }
- synchronized (mLock) {
- mState = ConnectionState.LISTEN;
- }
- mConnectionExecutor.execute(mAcceptTask);
- return true;
- }
-
- /**
- * Send data to remote connected bluetooth device.
- *
- * @param data the raw data that wait to be sent
- * @return {@code true} if the message is sent to client successfully.
- */
- boolean write(@NonNull byte[] data) {
- ConnectedTask connectedTask;
- // Synchronize a copy of the ConnectedTask
- synchronized (mLock) {
- if (mState != ConnectionState.CONNECTED) {
- loge(TAG, "Try to send data when device is disconnected");
- return false;
- }
- connectedTask = mConnectedTask;
- }
- byte[] dataReadyToSend = SppPayloadStream.wrapWithArrayLength(data);
- if (dataReadyToSend == null) {
- loge(TAG, "Wrapping data with array length failed.");
- return false;
- }
- connectedTask.write(dataReadyToSend);
- return true;
- }
-
- /**
- * Cleans up the registered listeners.
- */
- void cleanup() {
- // Clears all registered listeners. IHU only supports single connection.
- mReceivedListeners.clear();
- }
-
- /**
- * Start the ConnectedTask to begin and maintain a RFCOMM channel.
- *
- * @param socket The BluetoothSocket on which the connection was made
- * @param device The BluetoothDevice that has been connected
- * @param isSecure The type of current established channel
- */
- @GuardedBy("mLock")
- private void startConnectionLocked(BluetoothSocket socket, BluetoothDevice device,
- boolean isSecure) {
- logd(TAG, "Get accepted bluetooth socket, start listening to incoming messages.");
-
- // Cancel any task currently running a connection
- if (mConnectedTask != null) {
- mConnectedTask.cancel();
- mConnectedTask = null;
- }
-
- // Cancel the accept task because we only want to connect to one device
- if (mAcceptTask != null) {
- mAcceptTask.cancel();
- mAcceptTask = null;
- }
- logd(TAG, "Create ConnectedTask: is secure? " + isSecure);
- InputStream inputStream;
- OutputStream outputStream;
- mDevice = device;
-
- // Get the BluetoothSocket input and output streams
- try {
- inputStream = socket.getInputStream();
- outputStream = socket.getOutputStream();
- } catch (IOException e) {
- loge(TAG, "Can not get stream from BluetoothSocket. Connection failed.", e);
- return;
- }
- mState = ConnectionState.CONNECTED;
- mCallbacks.invoke(callback -> callback.onRemoteDeviceConnected(device));
-
- // Start the task to manage the connection and perform transmissions
- mConnectedTask = new ConnectedTask(inputStream, outputStream, socket,
- mConnectedTaskCallback);
- mConnectionExecutor.execute(mConnectedTask);
- }
-
- private void onMessageCompleted(@NonNull byte[] message) {
- mReceivedListeners.invoke(listener -> listener.onMessageReceived(mDevice, message));
- }
-
- @VisibleForTesting
- final AcceptTask.OnTaskCompletedListener mAcceptTaskListener =
- new AcceptTask.OnTaskCompletedListener() {
- @Override
- public void onTaskCompleted(BluetoothSocket socket, boolean isSecure) {
- if (socket == null) {
- loge(TAG, "AcceptTask failed getting the socket");
- return;
- }
- // Connection accepted
- synchronized (mLock) {
- switch (mState) {
- case LISTEN:
- case CONNECTING:
- startConnectionLocked(socket, socket.getRemoteDevice(), isSecure);
- break;
- case NONE:
- loge(TAG, "AcceptTask is done while in NONE state.");
- break;
- case CONNECTED:
- // Already connected. Terminate new socket.
- try {
- socket.close();
- } catch (IOException e) {
- loge(TAG, "Could not close unwanted socket", e);
- }
- break;
- case DISCONNECTED:
- loge(TAG, "AcceptTask is done while in DISCONNECTED state.");
- break;
- }
- }
- }
- };
-
- @VisibleForTesting
- final ConnectedTask.Callback mConnectedTaskCallback = new ConnectedTask.Callback() {
- @Override
- public void onMessageReceived(byte[] message) {
- synchronized (mLock) {
- try {
- mPayloadStream.write(message);
- } catch (IOException e) {
- loge(TAG, "Error writes message to spp payload stream: " + e.getMessage());
- }
- }
- }
-
- @Override
- public void onDisconnected() {
- synchronized (mLock) {
- mState = ConnectionState.DISCONNECTED;
- mCallbacks.invoke(callback -> callback.onRemoteDeviceDisconnected(mDevice));
- }
- }
- };
-
- /**
- * Interface to be notified of various events within the {@link SppManager}.
- */
- interface ConnectionCallback {
-
- /**
- * Triggered when a bluetooth device connected.
- *
- * @param device Remote device that connected on Spp.
- */
- void onRemoteDeviceConnected(@NonNull BluetoothDevice device);
-
- /**
- * Triggered when a bluetooth device disconnected.
- *
- * @param device Remote device that disconnected on Spp.
- */
- void onRemoteDeviceDisconnected(@NonNull BluetoothDevice device);
- }
-
- /**
- * An interface for classes that wish to be notified of incoming messages.
- */
- interface OnMessageReceivedListener {
- /**
- * Triggered when this SppManager receives a write request from a remote device.
- *
- * @param device The bluetooth device that sending the message.
- * @param value The value that was written.
- */
- void onMessageReceived(@NonNull BluetoothDevice device, @NonNull byte[] value);
- }
-
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/spp/SppPayloadStream.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/spp/SppPayloadStream.java
deleted file mode 100644
index f05a76f..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/spp/SppPayloadStream.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.connection.spp;
-
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.util.Arrays;
-
-/**
- * For spp stream will be segmented to several parts, a completed message length need to be prepend
- * to any message sent. This class will take care of the decode and encode of the incoming and
- * outgoing message.
- */
-class SppPayloadStream {
- private static final String TAG = "SppPayloadStream";
- // An int will take 4 bytes.
- private static final int LENGTH_BYTES_SIZE = 4;
- private final ByteArrayOutputStream mPendingStream = new ByteArrayOutputStream();
- private OnMessageCompletedListener mOnMessageCompletedListener;
- private int mCurrentMessageTotalLength;
-
- /**
- * Writes data to the {@code pendingStream}, inform the {@code messageCompletedListener} when
- * the message is completed, otherwise store the data into {@code pendingStream} and waiting for
- * the following parts.
- *
- * @param data Received byte array
- * @throws IOException If there are some errors writing data to {@code pendingStream}
- */
- public void write(@NonNull byte[] data) throws IOException {
- if (mPendingStream.size() == 0) {
- int currentLength = data.length;
- // Arbitrarily choose a byte order, need to use the same byte order for server and
- // client.
- mCurrentMessageTotalLength = ByteBuffer.wrap(
- Arrays.copyOf(data, LENGTH_BYTES_SIZE)).order(
- ByteOrder.LITTLE_ENDIAN).getInt();
- byte[] payload = Arrays.copyOfRange(data, LENGTH_BYTES_SIZE, currentLength);
- mPendingStream.write(payload);
- } else {
- mPendingStream.write(data);
- }
-
- if (mPendingStream.size() > mCurrentMessageTotalLength) {
- // TODO(b/159712861): Handle this situation, e.g. disconnect.
- loge(TAG, "Received invalid message: " + mPendingStream.toByteArray());
- return;
- }
-
- if (mPendingStream.size() < mCurrentMessageTotalLength) {
- return;
- }
- if (mOnMessageCompletedListener != null) {
- mOnMessageCompletedListener.onMessageCompleted(mPendingStream.toByteArray());
- }
- mPendingStream.reset();
-
- }
-
- /**
- * Register the given listener to be notified when a completed message is received.
- *
- * @param listener The listener to be notified
- */
- public void setMessageCompletedListener(@Nullable OnMessageCompletedListener listener) {
- mOnMessageCompletedListener = listener;
- }
-
- /**
- * Wrap the raw byte array with array length.
- * <p>
- * Should be called every time when server wants to send a message to client.
- *
- * @param rawData Original data
- * @return The wrapped data
- * @throws IOException If there are some errors writing data to {@code outputStream}
- */
- @Nullable
- public static byte[] wrapWithArrayLength(@NonNull byte[] rawData) {
- int length = rawData.length;
- byte[] lengthBytes = ByteBuffer.allocate(LENGTH_BYTES_SIZE).order(
- ByteOrder.LITTLE_ENDIAN).putInt(length).array();
- ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
- try {
- outputStream.write(lengthBytes);
- outputStream.write(rawData);
- } catch (IOException e) {
- loge(TAG, "Error wrap data with array length");
- return null;
- }
- return outputStream.toByteArray();
- }
-
- /**
- * Interface to be notified when a completed message has been received.
- */
- interface OnMessageCompletedListener {
- void onMessageCompleted(@NonNull byte[] message);
- }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/model/AssociatedDevice.java b/connected-device-lib/src/com/android/car/connecteddevice/model/AssociatedDevice.java
deleted file mode 100644
index 8622617..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/model/AssociatedDevice.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.model;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.util.Objects;
-
-/**
- * Contains basic info of an associated device.
- */
-public class AssociatedDevice {
-
- private final String mDeviceId;
-
- private final String mDeviceAddress;
-
- private final String mDeviceName;
-
- private final boolean mIsConnectionEnabled;
-
-
- /**
- * Create a new AssociatedDevice.
- *
- * @param deviceId Id of the associated device.
- * @param deviceAddress Address of the associated device.
- * @param deviceName Name of the associated device. {@code null} if not known.
- * @param isConnectionEnabled If connection is enabled for this device.
- */
- public AssociatedDevice(@NonNull String deviceId, @NonNull String deviceAddress,
- @Nullable String deviceName, boolean isConnectionEnabled) {
- mDeviceId = deviceId;
- mDeviceAddress = deviceAddress;
- mDeviceName = deviceName;
- mIsConnectionEnabled = isConnectionEnabled;
- }
-
- /** Returns the id for this device. */
- @NonNull
- public String getDeviceId() {
- return mDeviceId;
- }
-
- /** Returns the address for this device. */
- @NonNull
- public String getDeviceAddress() {
- return mDeviceAddress;
- }
-
- /** Returns the name for this device or {@code null} if not known. */
- @Nullable
- public String getDeviceName() {
- return mDeviceName;
- }
-
- /** Return if connection is enabled for this device. */
- public boolean isConnectionEnabled() {
- return mIsConnectionEnabled;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (obj == this) {
- return true;
- }
- if (!(obj instanceof AssociatedDevice)) {
- return false;
- }
- AssociatedDevice associatedDevice = (AssociatedDevice) obj;
- return Objects.equals(mDeviceId, associatedDevice.mDeviceId)
- && Objects.equals(mDeviceAddress, associatedDevice.mDeviceAddress)
- && Objects.equals(mDeviceName, associatedDevice.mDeviceName)
- && mIsConnectionEnabled == associatedDevice.mIsConnectionEnabled;
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(mDeviceId, mDeviceAddress, mDeviceName, mIsConnectionEnabled);
- }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/model/ConnectedDevice.java b/connected-device-lib/src/com/android/car/connecteddevice/model/ConnectedDevice.java
deleted file mode 100644
index 9e49a37..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/model/ConnectedDevice.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.model;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.util.Objects;
-
-/**
- * View model representing a connected device.
- */
-public class ConnectedDevice {
-
- private final String mDeviceId;
-
- private final String mDeviceName;
-
- private final boolean mBelongsToActiveUser;
-
- private final boolean mHasSecureChannel;
-
- /**
- * Create a new connected device.
- *
- * @param deviceId Id of the connected device.
- * @param deviceName Name of the connected device. {@code null} if not known.
- * @param belongsToActiveUser User associated with this device is currently in the foreground.
- * @param hasSecureChannel {@code true} if a secure channel is available for this device.
- */
- public ConnectedDevice(@NonNull String deviceId, @Nullable String deviceName,
- boolean belongsToActiveUser, boolean hasSecureChannel) {
- mDeviceId = deviceId;
- mDeviceName = deviceName;
- mBelongsToActiveUser = belongsToActiveUser;
- mHasSecureChannel = hasSecureChannel;
- }
-
- /** Returns the id for this device. */
- @NonNull
- public String getDeviceId() {
- return mDeviceId;
- }
-
- /** Returns the name for this device or {@code null} if not known. */
- @Nullable
- public String getDeviceName() {
- return mDeviceName;
- }
-
- /**
- * Returns {@code true} if this device is associated with the user currently in the foreground.
- */
- public boolean isAssociatedWithActiveUser() {
- return mBelongsToActiveUser;
- }
-
- /** Returns {@code true} if this device has a secure channel available. */
- public boolean hasSecureChannel() {
- return mHasSecureChannel;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (obj == this) {
- return true;
- }
- if (!(obj instanceof ConnectedDevice)) {
- return false;
- }
- ConnectedDevice connectedDevice = (ConnectedDevice) obj;
- return Objects.equals(mDeviceId, connectedDevice.mDeviceId)
- && Objects.equals(mDeviceName, connectedDevice.mDeviceName)
- && mBelongsToActiveUser == connectedDevice.mBelongsToActiveUser
- && mHasSecureChannel == connectedDevice.mHasSecureChannel;
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(mDeviceId, mDeviceName, mBelongsToActiveUser, mHasSecureChannel);
- }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/model/OobEligibleDevice.java b/connected-device-lib/src/com/android/car/connecteddevice/model/OobEligibleDevice.java
deleted file mode 100644
index 6e910d3..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/model/OobEligibleDevice.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.model;
-
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-
-import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-
-import java.lang.annotation.Retention;
-import java.util.Objects;
-
-/** Device that may be used for an out-of-band channel. */
-public class OobEligibleDevice {
-
- @Retention(SOURCE)
- @IntDef(value = { OOB_TYPE_BLUETOOTH })
- public @interface OobType {}
- public static final int OOB_TYPE_BLUETOOTH = 0;
-
- private final String mDeviceAddress;
-
- @OobType
- private final int mOobType;
-
- public OobEligibleDevice(@NonNull String deviceAddress, @OobType int oobType) {
- mDeviceAddress = deviceAddress;
- mOobType = oobType;
- }
-
- @NonNull
- public String getDeviceAddress() {
- return mDeviceAddress;
- }
-
- @OobType
- public int getOobType() {
- return mOobType;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (obj == this) {
- return true;
- }
- if (!(obj instanceof OobEligibleDevice)) {
- return false;
- }
- OobEligibleDevice device = (OobEligibleDevice) obj;
- return Objects.equals(device.mDeviceAddress, mDeviceAddress)
- && device.mOobType == mOobType;
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(mDeviceAddress, mOobType);
- }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/oob/BluetoothRfcommChannel.java b/connected-device-lib/src/com/android/car/connecteddevice/oob/BluetoothRfcommChannel.java
deleted file mode 100644
index 1325f11..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/oob/BluetoothRfcommChannel.java
+++ /dev/null
@@ -1,159 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.oob;
-
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothSocket;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-
-import com.android.car.connecteddevice.model.OobEligibleDevice;
-
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.time.Duration;
-import java.util.UUID;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * Handles out of band data exchange over a secure RFCOMM channel.
- */
-public class BluetoothRfcommChannel implements OobChannel {
- private static final String TAG = "BluetoothRfcommChannel";
- // TODO (b/159500330) Generate random UUID.
- private static final UUID RFCOMM_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
- private static final Duration CONNECT_RETRY_WAIT = Duration.ofSeconds(1);
- private BluetoothSocket mBluetoothSocket;
- private AtomicBoolean mIsInterrupted = new AtomicBoolean();
- @VisibleForTesting
- Callback mCallback;
-
- @Override
- public void completeOobDataExchange(@NonNull OobEligibleDevice device,
- @NonNull Callback callback) {
- completeOobDataExchange(device, callback, BluetoothAdapter.getDefaultAdapter());
- }
-
- @VisibleForTesting
- void completeOobDataExchange(OobEligibleDevice device, Callback callback,
- BluetoothAdapter bluetoothAdapter) {
- mCallback = callback;
-
- BluetoothDevice remoteDevice = bluetoothAdapter.getRemoteDevice(device.getDeviceAddress());
-
- try {
- mBluetoothSocket = remoteDevice.createRfcommSocketToServiceRecord(RFCOMM_UUID);
- } catch (IOException e) {
- notifyFailure("Rfcomm socket creation with " + remoteDevice.getName() + " failed.", e);
- return;
- }
-
- bluetoothAdapter.cancelDiscovery();
-
- new Thread() {
- @Override
- public void run() {
- while (!isInterrupted()) {
- try {
- mBluetoothSocket.connect();
- break;
- } catch (IOException e) {
- logd(TAG, "Unable to connect, trying again in "
- + CONNECT_RETRY_WAIT.toMillis() + " ms.");
- }
- try {
- Thread.sleep(CONNECT_RETRY_WAIT.toMillis());
- } catch (InterruptedException e) {
- loge(TAG, "Thread was interrupted before connection could be made.", e);
- Thread.currentThread().interrupt();
- return;
- }
- }
-
- notifySuccess();
- }
- }.start();
- }
-
- @Override
- public void sendOobData(byte[] oobData) {
- if (isInterrupted()) {
- return;
- }
- if (mBluetoothSocket == null) {
- notifyFailure("Bluetooth socket is null, oob data cannot be sent",
- /* exception= */ null);
- return;
- }
- try {
- OutputStream stream = mBluetoothSocket.getOutputStream();
- stream.write(oobData);
- stream.flush();
- stream.close();
- } catch (IOException e) {
- notifyFailure("Sending oob data failed", e);
- }
- }
-
- @Override
- public void interrupt() {
- logd(TAG, "Interrupt received.");
- mIsInterrupted.set(true);
- }
-
- @VisibleForTesting
- boolean isInterrupted() {
- if (!mIsInterrupted.get()) {
- return false;
- }
-
- if (mBluetoothSocket == null) {
- return true;
- }
-
- try {
- OutputStream stream = mBluetoothSocket.getOutputStream();
- stream.flush();
- stream.close();
- } catch (IOException e) {
- loge(TAG, "Unable to clean up bluetooth socket on interrupt.", e);
- }
-
- mBluetoothSocket = null;
- return true;
- }
-
- private void notifyFailure(@NonNull String message, @Nullable Exception exception) {
- loge(TAG, message, exception);
- if (mCallback != null && !isInterrupted()) {
- mCallback.onOobExchangeFailure();
- }
- }
-
- private void notifySuccess() {
- if (mCallback != null && !isInterrupted()) {
- mCallback.onOobExchangeSuccess();
- }
- }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/oob/OobChannel.java b/connected-device-lib/src/com/android/car/connecteddevice/oob/OobChannel.java
deleted file mode 100644
index 9be7941..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/oob/OobChannel.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.oob;
-
-import androidx.annotation.NonNull;
-
-import com.android.car.connecteddevice.model.OobEligibleDevice;
-
-/**
- * An interface for handling out of band data exchange. This interface should be implemented for
- * every out of band channel that is supported in device association.
- *
- * Usage is:
- * <pre>
- * 1. Define success and failure responses in {@link Callback}
- * 2. Call {@link OobChannel#completeOobDataExchange(OobEligibleDevice, Callback)}
- * </pre>
- */
-public interface OobChannel {
- /**
- * Exchange out of band data with a remote device. This must be done prior to the start of the
- * association with that device.
- *
- * @param device The remote device to exchange out of band data with
- */
- void completeOobDataExchange(@NonNull OobEligibleDevice device, @NonNull Callback callback);
-
- /**
- * Send raw data over the out of band channel
- *
- * @param oobData to be sent
- */
- void sendOobData(@NonNull byte[] oobData);
-
- /** Interrupt the current data exchange and prevent callbacks from being issued. */
- void interrupt();
-
- /**
- * Callbacks for {@link OobChannel#completeOobDataExchange(OobEligibleDevice, Callback)}
- */
- interface Callback {
- /**
- * Called when {@link OobChannel#completeOobDataExchange(OobEligibleDevice, Callback)}
- * finishes successfully.
- */
- void onOobExchangeSuccess();
-
- /**
- * Called when {@link OobChannel#completeOobDataExchange(OobEligibleDevice, Callback)}
- * fails.
- */
- void onOobExchangeFailure();
- }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/oob/OobConnectionManager.java b/connected-device-lib/src/com/android/car/connecteddevice/oob/OobConnectionManager.java
deleted file mode 100644
index f1564dd..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/oob/OobConnectionManager.java
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.oob;
-
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-
-import android.security.keystore.KeyProperties;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
-
-import com.google.common.primitives.Bytes;
-
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import java.util.Arrays;
-
-import javax.crypto.BadPaddingException;
-import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.KeyGenerator;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.IvParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-
-/**
- * This is a class that manages a token--{@link OobConnectionManager#mEncryptionKey}-- passed via
- * an out of band {@link OobChannel} that is distinct from the channel that is currently being
- * secured.
- * <p>Intended usage:
-* <pre>{@code
- * OobConnectionManager oobConncetionManager = new OobConnectionManager();
- * oobConnectionManager.startOobExchange(channel);
- * }</pre>
- * <pre>{@code When a message is received:
- * verificationCode = OobConnectionManager#decryptVerificationCode(byte[])
- * check that verification code is valid
- * if it is:
- * encryptedMessage = OobConnectionManager#encryptVerificationCode(byte[])
- * send encryptedMessage
- * verify handshake
- * otherwise:
- * fail handshake
- * }</pre>
- *
- * <pre>{@code
- * when oobData is received via the out of band channel:
- * OobConnectionManager#setOobData(byte[])
- *
- * encryptedMessage = OobConnectionManager#encryptVerificationCode(byte[])
- * sendMessage
- * when a message is received:
- * verificationCode = OobConnectionManager#decryptVerificationCode(byte[])
- * check that verification code is valid
- * if it is:
- * verify handshake
- * otherwise:
- * fail handshake
- * }</pre>
- */
-public class OobConnectionManager {
- private static final String TAG = "OobConnectionManager";
- private static final String ALGORITHM = "AES/GCM/NoPadding";
- // The nonce length is chosen to be consistent with the standard specification:
- // Section 8.2 of https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
- @VisibleForTesting
- static final int NONCE_LENGTH_BYTES = 12;
-
- private final Cipher mCipher;
- @VisibleForTesting
- byte[] mEncryptionIv = new byte[NONCE_LENGTH_BYTES];
- @VisibleForTesting
- byte[] mDecryptionIv = new byte[NONCE_LENGTH_BYTES];
- @VisibleForTesting
- SecretKey mEncryptionKey;
-
- public OobConnectionManager() {
- try {
- mCipher = Cipher.getInstance(ALGORITHM);
- } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
- loge(TAG, "Unable to create cipher with " + ALGORITHM + ".", e);
- throw new RuntimeException(e);
- }
- }
-
- /**
- * Encrypts {@param verificationCode} using {@link OobConnectionManager#mEncryptionKey}
- */
- @NonNull
- public byte[] encryptVerificationCode(@NonNull byte[] verificationCode)
- throws InvalidAlgorithmParameterException,
- BadPaddingException, InvalidKeyException, IllegalBlockSizeException {
- mCipher.init(Cipher.ENCRYPT_MODE, mEncryptionKey, new IvParameterSpec(mEncryptionIv));
- return mCipher.doFinal(verificationCode);
- }
-
- /**
- * Decrypts {@param encryptedMessage} using {@link OobConnectionManager#mEncryptionKey}
- */
- @NonNull
- public byte[] decryptVerificationCode(@NonNull byte[] encryptedMessage)
- throws InvalidAlgorithmParameterException, BadPaddingException, InvalidKeyException,
- IllegalBlockSizeException {
- mCipher.init(Cipher.DECRYPT_MODE, mEncryptionKey, new IvParameterSpec(mDecryptionIv));
- return mCipher.doFinal(encryptedMessage);
- }
-
- void setOobData(@NonNull byte[] oobData) {
- mEncryptionIv = Arrays.copyOfRange(oobData, 0, NONCE_LENGTH_BYTES);
- mDecryptionIv = Arrays.copyOfRange(oobData, NONCE_LENGTH_BYTES,
- NONCE_LENGTH_BYTES * 2);
- mEncryptionKey = new SecretKeySpec(
- Arrays.copyOfRange(oobData, NONCE_LENGTH_BYTES * 2, oobData.length),
- KeyProperties.KEY_ALGORITHM_AES);
- }
-
- /**
- * Start the out of band exchange with a given {@link OobChannel}.
- *
- * @param oobChannel Channel to be used for exchange.
- * @return {@code true} if exchange started successfully. {@code false} if an error occurred.
- */
- public boolean startOobExchange(@NonNull OobChannel oobChannel) {
- KeyGenerator keyGenerator = null;
- try {
- keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES);
- } catch (NoSuchAlgorithmException e) {
- loge(TAG, "Unable to get AES key generator.", e);
- return false;
- }
- mEncryptionKey = keyGenerator.generateKey();
-
- SecureRandom secureRandom = new SecureRandom();
- secureRandom.nextBytes(mEncryptionIv);
- secureRandom.nextBytes(mDecryptionIv);
-
- oobChannel.sendOobData(
- Bytes.concat(mDecryptionIv, mEncryptionIv, mEncryptionKey.getEncoded()));
- return true;
- }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/storage/AssociatedDeviceChallengeSecretEntity.java b/connected-device-lib/src/com/android/car/connecteddevice/storage/AssociatedDeviceChallengeSecretEntity.java
deleted file mode 100644
index ab802e6..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/storage/AssociatedDeviceChallengeSecretEntity.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.storage;
-
-import androidx.annotation.NonNull;
-import androidx.room.Entity;
-import androidx.room.PrimaryKey;
-
-/**
- * Table entity representing an challenge key for an associated device reconnection advertisement.
- */
-@Entity(tableName = "associated_devices_challenge_secrets")
-public class AssociatedDeviceChallengeSecretEntity {
-
- /** Id of the device. */
- @PrimaryKey
- @NonNull
- public String id;
-
- /** Encrypted challenge key. */
- @NonNull
- public String encryptedChallengeSecret;
-
- public AssociatedDeviceChallengeSecretEntity(String id, String encryptedChallengeSecret) {
- this.id = id;
- this.encryptedChallengeSecret = encryptedChallengeSecret;
- }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/storage/AssociatedDeviceDao.java b/connected-device-lib/src/com/android/car/connecteddevice/storage/AssociatedDeviceDao.java
deleted file mode 100644
index 2a1e023..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/storage/AssociatedDeviceDao.java
+++ /dev/null
@@ -1,83 +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.connecteddevice.storage;
-
-import androidx.room.Dao;
-import androidx.room.Delete;
-import androidx.room.Insert;
-import androidx.room.OnConflictStrategy;
-import androidx.room.Query;
-
-import java.util.List;
-
-/**
- * Queries for associated device table.
- */
-@Dao
-public interface AssociatedDeviceDao {
-
- /** Get an associated device based on device id. */
- @Query("SELECT * FROM associated_devices WHERE id LIKE :deviceId LIMIT 1")
- AssociatedDeviceEntity getAssociatedDevice(String deviceId);
-
- /** Get all {@link AssociatedDeviceEntity}s associated with a user. */
- @Query("SELECT * FROM associated_devices WHERE userId LIKE :userId")
- List<AssociatedDeviceEntity> getAssociatedDevicesForUser(int userId);
-
- /**
- * Add a {@link AssociatedDeviceEntity}. Replace if a device already exists with the same
- * device id.
- */
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- void addOrReplaceAssociatedDevice(AssociatedDeviceEntity associatedDevice);
-
- /** Remove a {@link AssociatedDeviceEntity}. */
- @Delete
- void removeAssociatedDevice(AssociatedDeviceEntity connectedDevice);
-
- /** Get the key associated with a device id. */
- @Query("SELECT * FROM associated_device_keys WHERE id LIKE :deviceId LIMIT 1")
- AssociatedDeviceKeyEntity getAssociatedDeviceKey(String deviceId);
-
- /**
- * Add a {@link AssociatedDeviceKeyEntity}. Replace if a device key already exists with the
- * same device id.
- */
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- void addOrReplaceAssociatedDeviceKey(AssociatedDeviceKeyEntity keyEntity);
-
- /** Remove a {@link AssociatedDeviceKeyEntity}. */
- @Delete
- void removeAssociatedDeviceKey(AssociatedDeviceKeyEntity keyEntity);
-
- /** Get the challenge secret associated with a device id. */
- @Query("SELECT * FROM associated_devices_challenge_secrets WHERE id LIKE :deviceId LIMIT 1")
- AssociatedDeviceChallengeSecretEntity getAssociatedDeviceChallengeSecret(String deviceId);
-
- /**
- * Add a {@link AssociatedDeviceChallengeSecretEntity}. Replace if a secret already exists with
- * the same device id.
- */
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- void addOrReplaceAssociatedDeviceChallengeSecret(
- AssociatedDeviceChallengeSecretEntity challengeSecretEntity);
-
- /** Remove a {@link AssociatedDeviceChallengeSecretEntity}. */
- @Delete
- void removeAssociatedDeviceChallengeSecret(
- AssociatedDeviceChallengeSecretEntity challengeSecretEntity);
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/storage/AssociatedDeviceEntity.java b/connected-device-lib/src/com/android/car/connecteddevice/storage/AssociatedDeviceEntity.java
deleted file mode 100644
index 1c5182c..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/storage/AssociatedDeviceEntity.java
+++ /dev/null
@@ -1,64 +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.connecteddevice.storage;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.room.Entity;
-import androidx.room.PrimaryKey;
-
-import com.android.car.connecteddevice.model.AssociatedDevice;
-
-/** Table entity representing an associated device. */
-@Entity(tableName = "associated_devices")
-public class AssociatedDeviceEntity {
-
- /** Id of the device. */
- @PrimaryKey
- @NonNull
- public String id;
-
- /** Id of user associated with this device. */
- public int userId;
-
- /** Bluetooth address of the device. */
- @Nullable
- public String address;
-
- /** Bluetooth device name. */
- @Nullable
- public String name;
-
- /** {@code true} if the connection is enabled for this device.*/
- public boolean isConnectionEnabled;
-
- public AssociatedDeviceEntity() { }
-
- public AssociatedDeviceEntity(int userId, AssociatedDevice associatedDevice,
- boolean isConnectionEnabled) {
- this.userId = userId;
- id = associatedDevice.getDeviceId();
- address = associatedDevice.getDeviceAddress();
- name = associatedDevice.getDeviceName();
- this.isConnectionEnabled = isConnectionEnabled;
- }
-
- /** Return a new {@link AssociatedDevice} of this entity. */
- public AssociatedDevice toAssociatedDevice() {
- return new AssociatedDevice(id, address, name, isConnectionEnabled);
- }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/storage/AssociatedDeviceKeyEntity.java b/connected-device-lib/src/com/android/car/connecteddevice/storage/AssociatedDeviceKeyEntity.java
deleted file mode 100644
index 6cd791f..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/storage/AssociatedDeviceKeyEntity.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.storage;
-
-import androidx.annotation.NonNull;
-import androidx.room.Entity;
-import androidx.room.PrimaryKey;
-
-/** Table entity representing a key for an associated device. */
-@Entity(tableName = "associated_device_keys")
-public class AssociatedDeviceKeyEntity {
-
- /** Id of the device. */
- @PrimaryKey
- @NonNull
- public String id;
-
- @NonNull
- public String encryptedKey;
-
- public AssociatedDeviceKeyEntity() { }
-
- public AssociatedDeviceKeyEntity(String deviceId, String encryptedKey) {
- id = deviceId;
- this.encryptedKey = encryptedKey;
- }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/storage/ConnectedDeviceDatabase.java b/connected-device-lib/src/com/android/car/connecteddevice/storage/ConnectedDeviceDatabase.java
deleted file mode 100644
index 98a7b0a..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/storage/ConnectedDeviceDatabase.java
+++ /dev/null
@@ -1,31 +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.connecteddevice.storage;
-
-import androidx.room.Database;
-import androidx.room.RoomDatabase;
-
-/** Database for connected devices. */
-@Database(entities = { AssociatedDeviceEntity.class, AssociatedDeviceKeyEntity.class,
- AssociatedDeviceChallengeSecretEntity.class },
- version = 2,
- exportSchema = false)
-public abstract class ConnectedDeviceDatabase extends RoomDatabase {
-
- /** Return the DAO for the associated device table. */
- public abstract AssociatedDeviceDao associatedDeviceDao();
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/storage/ConnectedDeviceStorage.java b/connected-device-lib/src/com/android/car/connecteddevice/storage/ConnectedDeviceStorage.java
deleted file mode 100644
index b62a43c..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/storage/ConnectedDeviceStorage.java
+++ /dev/null
@@ -1,564 +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.connecteddevice.storage;
-
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-import static com.android.car.connecteddevice.util.SafeLog.logw;
-
-import android.app.ActivityManager;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.security.keystore.KeyGenParameterSpec;
-import android.security.keystore.KeyProperties;
-import android.util.Base64;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-import androidx.room.Room;
-
-import com.android.car.connecteddevice.R;
-import com.android.car.connecteddevice.model.AssociatedDevice;
-
-import java.io.IOException;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.InvalidParameterException;
-import java.security.Key;
-import java.security.KeyStore;
-import java.security.KeyStoreException;
-import java.security.NoSuchAlgorithmException;
-import java.security.NoSuchProviderException;
-import java.security.UnrecoverableKeyException;
-import java.security.cert.CertificateException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.UUID;
-
-import javax.crypto.BadPaddingException;
-import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.KeyGenerator;
-import javax.crypto.Mac;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.spec.GCMParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-
-/** Storage for connected devices in a car. */
-public class ConnectedDeviceStorage {
- private static final String TAG = "CompanionStorage";
-
- private static final String UNIQUE_ID_KEY = "CTABM_unique_id";
- private static final String BT_NAME_KEY = "CTABM_bt_name";
- private static final String KEY_ALIAS = "Ukey2Key";
- private static final String CIPHER_TRANSFORMATION = "AES/GCM/NoPadding";
- private static final String KEYSTORE_PROVIDER = "AndroidKeyStore";
- private static final String DATABASE_NAME = "connected-device-database";
- private static final String IV_SPEC_SEPARATOR = ";";
-
- private static final String CHALLENGE_HASHING_ALGORITHM = "HmacSHA256";
- // This delimiter separates deviceId and deviceInfo, so it has to differ from the
- // TrustedDeviceInfo delimiter. Once new API can be added, deviceId will be added to
- // TrustedDeviceInfo and this delimiter will be removed.
-
- // The length of the authentication tag for a cipher in GCM mode. The GCM specification states
- // that this length can only have the values {128, 120, 112, 104, 96}. Using the highest
- // possible value.
- private static final int GCM_AUTHENTICATION_TAG_LENGTH = 128;
-
- @VisibleForTesting
- static final int CHALLENGE_SECRET_BYTES = 32;
-
- private final Context mContext;
-
- private SharedPreferences mSharedPreferences;
-
- private UUID mUniqueId;
-
- private AssociatedDeviceDao mAssociatedDeviceDatabase;
-
- private AssociatedDeviceCallback mAssociatedDeviceCallback;
-
- public ConnectedDeviceStorage(@NonNull Context context) {
- mContext = context;
- mAssociatedDeviceDatabase = Room.databaseBuilder(context, ConnectedDeviceDatabase.class,
- DATABASE_NAME)
- .fallbackToDestructiveMigration()
- .build()
- .associatedDeviceDao();
- }
-
- /**
- * Set a callback for associated device updates.
- *
- * @param callback {@link AssociatedDeviceCallback} to set.
- */
- public void setAssociatedDeviceCallback(
- @NonNull AssociatedDeviceCallback callback) {
- mAssociatedDeviceCallback = callback;
- }
-
- /** Clear the callback for association device callback updates. */
- public void clearAssociationDeviceCallback() {
- mAssociatedDeviceCallback = null;
- }
-
- /**
- * Get communication encryption key for the given device.
- *
- * @param deviceId id of trusted device
- * @return encryption key, null if device id is not recognized
- */
- @Nullable
- public byte[] getEncryptionKey(@NonNull String deviceId) {
- AssociatedDeviceKeyEntity entity =
- mAssociatedDeviceDatabase.getAssociatedDeviceKey(deviceId);
- if (entity == null) {
- logd(TAG, "Encryption key not found!");
- return null;
- }
- String[] values = entity.encryptedKey.split(IV_SPEC_SEPARATOR, -1);
-
- if (values.length != 2) {
- logd(TAG, "Stored encryption key had the wrong length.");
- return null;
- }
-
- byte[] encryptedKey = Base64.decode(values[0], Base64.DEFAULT);
- byte[] ivSpec = Base64.decode(values[1], Base64.DEFAULT);
- return decryptWithKeyStore(KEY_ALIAS, encryptedKey, ivSpec);
- }
-
- /**
- * Save encryption key for the given device.
- *
- * @param deviceId id of the device
- * @param encryptionKey encryption key
- */
- public void saveEncryptionKey(@NonNull String deviceId, @NonNull byte[] encryptionKey) {
- String encryptedKey = encryptWithKeyStore(KEY_ALIAS, encryptionKey);
- AssociatedDeviceKeyEntity entity = new AssociatedDeviceKeyEntity(deviceId, encryptedKey);
- mAssociatedDeviceDatabase.addOrReplaceAssociatedDeviceKey(entity);
- logd(TAG, "Successfully wrote encryption key.");
- }
-
- /**
- * Save challenge secret for the given device.
- *
- * @param deviceId id of the device
- * @param secret Secret associated with this device. Note: must be
- * {@value CHALLENGE_SECRET_BYTES} bytes in length or an
- * {@link InvalidParameterException} will be thrown.
- */
- public void saveChallengeSecret(@NonNull String deviceId, @NonNull byte[] secret) {
- if (secret.length != CHALLENGE_SECRET_BYTES) {
- throw new InvalidParameterException("Secrets must be " + CHALLENGE_SECRET_BYTES
- + " bytes in length.");
- }
-
- String encryptedKey = encryptWithKeyStore(KEY_ALIAS, secret);
- AssociatedDeviceChallengeSecretEntity entity = new AssociatedDeviceChallengeSecretEntity(
- deviceId, encryptedKey);
- mAssociatedDeviceDatabase.addOrReplaceAssociatedDeviceChallengeSecret(entity);
- logd(TAG, "Successfully wrote challenge secret.");
- }
-
- /** Get the challenge secret associated with a device. */
- public byte[] getChallengeSecret(@NonNull String deviceId) {
- AssociatedDeviceChallengeSecretEntity entity =
- mAssociatedDeviceDatabase.getAssociatedDeviceChallengeSecret(deviceId);
- if (entity == null) {
- logd(TAG, "Challenge secret not found!");
- return null;
- }
- String[] values = entity.encryptedChallengeSecret.split(IV_SPEC_SEPARATOR, -1);
-
- if (values.length != 2) {
- logd(TAG, "Stored encryption key had the wrong length.");
- return null;
- }
-
- byte[] encryptedSecret = Base64.decode(values[0], Base64.DEFAULT);
- byte[] ivSpec = Base64.decode(values[1], Base64.DEFAULT);
- return decryptWithKeyStore(KEY_ALIAS, encryptedSecret, ivSpec);
- }
-
- /**
- * Hash provided value with device's challenge secret and return result. Returns {@code null} if
- * unsuccessful.
- */
- @Nullable
- public byte[] hashWithChallengeSecret(@NonNull String deviceId, @NonNull byte[] value) {
- byte[] challengeSecret = getChallengeSecret(deviceId);
- if (challengeSecret == null) {
- loge(TAG, "Unable to find challenge secret for device " + deviceId + ".");
- return null;
- }
-
- Mac mac;
- try {
- mac = Mac.getInstance(CHALLENGE_HASHING_ALGORITHM);
- } catch (NoSuchAlgorithmException e) {
- loge(TAG, "Unable to find hashing algorithm " + CHALLENGE_HASHING_ALGORITHM + ".", e);
- return null;
- }
-
- SecretKeySpec keySpec = new SecretKeySpec(challengeSecret, CHALLENGE_HASHING_ALGORITHM);
- try {
- mac.init(keySpec);
- } catch (InvalidKeyException e) {
- loge(TAG, "Exception while initializing HMAC.", e);
- return null;
- }
-
- return mac.doFinal(value);
- }
-
- /**
- * Encrypt value with designated key
- *
- * <p>The encrypted value is of the form:
- *
- * <p>key + IV_SPEC_SEPARATOR + ivSpec
- *
- * <p>The {@code ivSpec} is needed to decrypt this key later on.
- *
- * @param keyAlias KeyStore alias for key to use
- * @param value a value to encrypt
- * @return encrypted value, null if unable to encrypt
- */
- @Nullable
- private String encryptWithKeyStore(@NonNull String keyAlias, @Nullable byte[] value) {
- if (value == null) {
- logw(TAG, "Received a null key value.");
- return null;
- }
-
- Key key = getKeyStoreKey(keyAlias);
- try {
- Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
- cipher.init(Cipher.ENCRYPT_MODE, key);
- return Base64.encodeToString(cipher.doFinal(value), Base64.DEFAULT)
- + IV_SPEC_SEPARATOR
- + Base64.encodeToString(cipher.getIV(), Base64.DEFAULT);
- } catch (IllegalBlockSizeException
- | BadPaddingException
- | NoSuchAlgorithmException
- | NoSuchPaddingException
- | IllegalStateException
- | InvalidKeyException e) {
- loge(TAG, "Unable to encrypt value with key " + keyAlias, e);
- return null;
- }
- }
-
- /**
- * Decrypt value with designated key
- *
- * @param keyAlias KeyStore alias for key to use
- * @param value encrypted value
- * @return decrypted value, null if unable to decrypt
- */
- @Nullable
- private byte[] decryptWithKeyStore(
- @NonNull String keyAlias, @Nullable byte[] value, @NonNull byte[] ivSpec) {
- if (value == null) {
- return null;
- }
-
- try {
- Key key = getKeyStoreKey(keyAlias);
- Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
- cipher.init(
- Cipher.DECRYPT_MODE, key,
- new GCMParameterSpec(GCM_AUTHENTICATION_TAG_LENGTH, ivSpec));
- return cipher.doFinal(value);
- } catch (IllegalBlockSizeException
- | BadPaddingException
- | NoSuchAlgorithmException
- | NoSuchPaddingException
- | IllegalStateException
- | InvalidKeyException
- | InvalidAlgorithmParameterException e) {
- loge(TAG, "Unable to decrypt value with key " + keyAlias, e);
- return null;
- }
- }
-
- @Nullable
- private static Key getKeyStoreKey(@NonNull String keyAlias) {
- KeyStore keyStore;
- try {
- keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER);
- keyStore.load(null);
- if (!keyStore.containsAlias(keyAlias)) {
- KeyGenerator keyGenerator =
- KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES,
- KEYSTORE_PROVIDER);
- keyGenerator.init(
- new KeyGenParameterSpec.Builder(
- keyAlias,
- KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
- .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
- .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
- .build());
- keyGenerator.generateKey();
- }
- return keyStore.getKey(keyAlias, null);
-
- } catch (KeyStoreException
- | NoSuchAlgorithmException
- | UnrecoverableKeyException
- | NoSuchProviderException
- | CertificateException
- | IOException
- | InvalidAlgorithmParameterException e) {
- loge(TAG, "Unable to retrieve key " + keyAlias + " from KeyStore.", e);
- throw new IllegalStateException(e);
- }
- }
-
- @NonNull
- private SharedPreferences getSharedPrefs() {
- // This should be called only after user 0 is unlocked.
- if (mSharedPreferences != null) {
- return mSharedPreferences;
- }
- mSharedPreferences = mContext.getSharedPreferences(
- mContext.getString(R.string.connected_device_shared_preferences),
- Context.MODE_PRIVATE);
- return mSharedPreferences;
-
- }
-
- /**
- * Get the unique id for head unit. Persists on device until factory reset. This should be
- * called only after user 0 is unlocked.
- *
- * @return unique id
- */
- @NonNull
- public UUID getUniqueId() {
- if (mUniqueId != null) {
- return mUniqueId;
- }
-
- SharedPreferences prefs = getSharedPrefs();
- if (prefs.contains(UNIQUE_ID_KEY)) {
- mUniqueId = UUID.fromString(prefs.getString(UNIQUE_ID_KEY, null));
- logd(TAG,
- "Found existing trusted unique id: " + prefs.getString(UNIQUE_ID_KEY, ""));
- }
-
- if (mUniqueId == null) {
- mUniqueId = UUID.randomUUID();
- prefs.edit().putString(UNIQUE_ID_KEY, mUniqueId.toString()).apply();
- logd(TAG,
- "Generated new trusted unique id: " + prefs.getString(UNIQUE_ID_KEY, ""));
- }
-
- return mUniqueId;
- }
-
- /** Store the current bluetooth adapter name. */
- public void storeBluetoothName(@NonNull String name) {
- getSharedPrefs().edit().putString(BT_NAME_KEY, name).apply();
- }
-
- /** Get the previously stored bluetooth adapter name or {@code null} if not found. */
- @Nullable
- public String getStoredBluetoothName() {
- return getSharedPrefs().getString(BT_NAME_KEY, null);
- }
-
- /** Remove the previously stored bluetooth adapter name from storage. */
- public void removeStoredBluetoothName() {
- getSharedPrefs().edit().remove(BT_NAME_KEY).apply();
- }
-
- /**
- * Get a list of associated devices for the given user.
- *
- * @param userId The identifier of the user.
- * @return Associated device list.
- */
- @NonNull
- public List<AssociatedDevice> getAssociatedDevicesForUser(@NonNull int userId) {
- List<AssociatedDeviceEntity> entities =
- mAssociatedDeviceDatabase.getAssociatedDevicesForUser(userId);
-
- if (entities == null) {
- return new ArrayList<>();
- }
-
- ArrayList<AssociatedDevice> userDevices = new ArrayList<>();
- for (AssociatedDeviceEntity entity : entities) {
- userDevices.add(entity.toAssociatedDevice());
- }
-
- return userDevices;
- }
-
- /**
- * Get a list of associated devices for the current user.
- *
- * @return Associated device list.
- */
- @NonNull
- public List<AssociatedDevice> getActiveUserAssociatedDevices() {
- return getAssociatedDevicesForUser(ActivityManager.getCurrentUser());
- }
-
- /**
- * Returns a list of device ids of associated devices for the given user.
- *
- * @param userId The user id for whom we want to know the device ids.
- * @return List of device ids.
- */
- @NonNull
- public List<String> getAssociatedDeviceIdsForUser(@NonNull int userId) {
- List<AssociatedDevice> userDevices = getAssociatedDevicesForUser(userId);
- ArrayList<String> userDeviceIds = new ArrayList<>();
-
- for (AssociatedDevice device : userDevices) {
- userDeviceIds.add(device.getDeviceId());
- }
-
- return userDeviceIds;
- }
-
- /**
- * Returns a list of device ids of associated devices for the current user.
- *
- * @return List of device ids.
- */
- @NonNull
- public List<String> getActiveUserAssociatedDeviceIds() {
- return getAssociatedDeviceIdsForUser(ActivityManager.getCurrentUser());
- }
-
- /**
- * Add the associated device of the given deviceId for the currently active user.
- *
- * @param device New associated device to be added.
- */
- public void addAssociatedDeviceForActiveUser(@NonNull AssociatedDevice device) {
- addAssociatedDeviceForUser(ActivityManager.getCurrentUser(), device);
- if (mAssociatedDeviceCallback != null) {
- mAssociatedDeviceCallback.onAssociatedDeviceAdded(device);
- }
- }
-
-
- /**
- * Add the associated device of the given deviceId for the given user.
- *
- * @param userId The identifier of the user.
- * @param device New associated device to be added.
- */
- public void addAssociatedDeviceForUser(int userId, @NonNull AssociatedDevice device) {
- AssociatedDeviceEntity entity = new AssociatedDeviceEntity(userId, device,
- /* isConnectionEnabled= */ true);
- mAssociatedDeviceDatabase.addOrReplaceAssociatedDevice(entity);
- }
-
- /**
- * Update the name for an associated device.
- *
- * @param deviceId The id of the associated device.
- * @param name The name to replace with.
- */
- public void updateAssociatedDeviceName(@NonNull String deviceId, @NonNull String name) {
- AssociatedDeviceEntity entity = mAssociatedDeviceDatabase.getAssociatedDevice(deviceId);
- if (entity == null) {
- logw(TAG, "Attempt to update name on an unrecognized device " + deviceId
- + ". Ignoring.");
- return;
- }
- entity.name = name;
- mAssociatedDeviceDatabase.addOrReplaceAssociatedDevice(entity);
- if (mAssociatedDeviceCallback != null) {
- mAssociatedDeviceCallback.onAssociatedDeviceUpdated(new AssociatedDevice(deviceId,
- entity.address, name, entity.isConnectionEnabled));
- }
- }
-
- /**
- * Remove the associated device of the given deviceId for the given user.
- *
- * @param userId The identifier of the user.
- * @param deviceId The identifier of the device to be cleared.
- */
- public void removeAssociatedDevice(int userId, @NonNull String deviceId) {
- AssociatedDeviceEntity entity = mAssociatedDeviceDatabase.getAssociatedDevice(deviceId);
- if (entity == null || entity.userId != userId) {
- return;
- }
- mAssociatedDeviceDatabase.removeAssociatedDevice(entity);
- if (mAssociatedDeviceCallback != null) {
- mAssociatedDeviceCallback.onAssociatedDeviceRemoved(new AssociatedDevice(deviceId,
- entity.address, entity.name, entity.isConnectionEnabled));
- }
- }
-
- /**
- * Clear the associated device of the given deviceId for the current user.
- *
- * @param deviceId The identifier of the device to be cleared.
- */
- public void removeAssociatedDeviceForActiveUser(@NonNull String deviceId) {
- removeAssociatedDevice(ActivityManager.getCurrentUser(), deviceId);
- }
-
- /**
- * Set if connection is enabled for an associated device.
- *
- * @param deviceId The id of the associated device.
- * @param isConnectionEnabled If connection enabled for this device.
- */
- public void updateAssociatedDeviceConnectionEnabled(@NonNull String deviceId,
- boolean isConnectionEnabled) {
- AssociatedDeviceEntity entity = mAssociatedDeviceDatabase.getAssociatedDevice(deviceId);
- if (entity == null) {
- logw(TAG, "Attempt to enable or disable connection on an unrecognized device "
- + deviceId + ". Ignoring.");
- return;
- }
- if (entity.isConnectionEnabled == isConnectionEnabled) {
- return;
- }
- entity.isConnectionEnabled = isConnectionEnabled;
- mAssociatedDeviceDatabase.addOrReplaceAssociatedDevice(entity);
- if (mAssociatedDeviceCallback != null) {
- mAssociatedDeviceCallback.onAssociatedDeviceUpdated(new AssociatedDevice(deviceId,
- entity.address, entity.name, isConnectionEnabled));
- }
- }
-
- /** Callback for association device related events. */
- public interface AssociatedDeviceCallback {
- /** Triggered when an associated device has been added. */
- void onAssociatedDeviceAdded(@NonNull AssociatedDevice device);
-
- /** Triggered when an associated device has been removed. */
- void onAssociatedDeviceRemoved(@NonNull AssociatedDevice device);
-
- /** Triggered when an associated device has been updated. */
- void onAssociatedDeviceUpdated(@NonNull AssociatedDevice device);
- }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/util/ByteUtils.java b/connected-device-lib/src/com/android/car/connecteddevice/util/ByteUtils.java
deleted file mode 100644
index adffa73..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/util/ByteUtils.java
+++ /dev/null
@@ -1,160 +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.connecteddevice.util;
-
-import android.annotation.SuppressLint;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.util.UUID;
-import java.util.concurrent.ThreadLocalRandom;
-
-/**
- * Utility classes for manipulating bytes.
- */
-public final class ByteUtils {
- // https://developer.android.com/reference/java/util/UUID
- private static final int UUID_LENGTH = 16;
-
- private ByteUtils() {
- }
-
- /**
- * Returns a byte buffer corresponding to the passed long argument.
- *
- * @param primitive data to convert format.
- */
- public static byte[] longToBytes(long primitive) {
- ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES);
- buffer.putLong(primitive);
- return buffer.array();
- }
-
- /**
- * Returns a byte buffer corresponding to the passed long argument.
- *
- * @param array data to convert format.
- */
- public static long bytesToLong(byte[] array) {
- ByteBuffer buffer = ByteBuffer.allocate(Long.SIZE / Byte.SIZE);
- buffer.put(array);
- buffer.flip();
- long value = buffer.getLong();
- return value;
- }
-
- /**
- * Returns a String in Hex format that is formed from the bytes in the byte array Useful for
- * debugging
- *
- * @param array the byte array
- * @return the Hex string version of the input byte array
- */
- public static String byteArrayToHexString(byte[] array) {
- StringBuilder sb = new StringBuilder(array.length * 2);
- for (byte b : array) {
- sb.append(String.format("%02x", b));
- }
- return sb.toString();
- }
-
- /**
- * Convert UUID to Big Endian byte array
- *
- * @param uuid UUID to convert
- * @return the byte array representing the UUID
- */
- @NonNull
- public static byte[] uuidToBytes(@NonNull UUID uuid) {
-
- return ByteBuffer.allocate(UUID_LENGTH)
- .order(ByteOrder.BIG_ENDIAN)
- .putLong(uuid.getMostSignificantBits())
- .putLong(uuid.getLeastSignificantBits())
- .array();
- }
-
- /**
- * Convert Big Endian byte array to UUID
- *
- * @param bytes byte array to convert
- * @return the UUID representing the byte array, or null if not a valid UUID
- */
- @Nullable
- public static UUID bytesToUUID(@NonNull byte[] bytes) {
- if (bytes.length != UUID_LENGTH) {
- return null;
- }
-
- ByteBuffer buffer = ByteBuffer.wrap(bytes);
- return new UUID(buffer.getLong(), buffer.getLong());
- }
-
- /**
- * Generate a random zero-filled string of given length
- *
- * @param length of string
- * @return generated string
- */
- @SuppressLint("DefaultLocale") // Should always have the same format regardless of locale
- public static String generateRandomNumberString(int length) {
- return String.format(
- "%0" + length + "d",
- ThreadLocalRandom.current().nextInt((int) Math.pow(10, length)));
- }
-
- /**
- * Generate a {@link byte[]} with random bytes.
- *
- * @param size of array to generate.
- * @return generated {@link byte[]}.
- */
- @NonNull
- public static byte[] randomBytes(int size) {
- byte[] array = new byte[size];
- ThreadLocalRandom.current().nextBytes(array);
- return array;
- }
-
- /**
- * Concatentate the given 2 byte arrays
- *
- * @param a input array 1
- * @param b input array 2
- * @return concatenated array of arrays 1 and 2
- */
- @Nullable
- public static byte[] concatByteArrays(@Nullable byte[] a, @Nullable byte[] b) {
- ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
- try {
- if (a != null) {
- outputStream.write(a);
- }
- if (b != null) {
- outputStream.write(b);
- }
- } catch (IOException e) {
- return null;
- }
- return outputStream.toByteArray();
- }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/util/EventLog.java b/connected-device-lib/src/com/android/car/connecteddevice/util/EventLog.java
deleted file mode 100644
index 5dcd829..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/util/EventLog.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.util;
-
-import static com.android.car.connecteddevice.util.SafeLog.logi;
-
-import com.android.car.connecteddevice.ConnectedDeviceManager;
-
-/** Logging class for collecting metrics. */
-public class EventLog {
-
- private static final String TAG = "ConnectedDeviceEvent";
-
- private EventLog() { }
-
- /** Mark in log that the service has started. */
- public static void onServiceStarted() {
- logi(TAG, "SERVICE_STARTED");
- }
-
- /** Mark in log that the {@link ConnectedDeviceManager} has started. */
- public static void onConnectedDeviceManagerStarted() {
- logi(TAG, "CONNECTED_DEVICE_MANAGER_STARTED");
- }
-
- /** Mark in the log that BLE is on. */
- public static void onBleOn() {
- logi(TAG, "BLE_ON");
- }
-
- /** Mark in the log that a search for the user's device has started. */
- public static void onStartDeviceSearchStarted() {
- logi(TAG, "SEARCHING_FOR_DEVICE");
- }
-
-
- /** Mark in the log that a device connected. */
- public static void onDeviceConnected() {
- logi(TAG, "DEVICE_CONNECTED");
- }
-
- /** Mark in the log that the device has sent its id. */
- public static void onDeviceIdReceived() {
- logi(TAG, "RECEIVED_DEVICE_ID");
- }
-
- /** Mark in the log that a secure channel has been established with a device. */
- public static void onSecureChannelEstablished() {
- logi(TAG, "SECURE_CHANNEL_ESTABLISHED");
- }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/util/RemoteCallbackBinder.java b/connected-device-lib/src/com/android/car/connecteddevice/util/RemoteCallbackBinder.java
deleted file mode 100644
index 98ad4db..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/util/RemoteCallbackBinder.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.util;
-
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-
-import android.os.IBinder;
-import android.os.RemoteException;
-
-import java.util.function.Consumer;
-
-/**
- * Class that holds the binder of a remote callback and an action to be executed when this
- * binder dies.
- * It registers for death notification of the {@link #mCallbackBinder} and executes
- * {@link #mOnDiedConsumer} when {@link #mCallbackBinder} dies.
- */
-public class RemoteCallbackBinder implements IBinder.DeathRecipient {
- private static final String TAG = "BinderClient";
- private final IBinder mCallbackBinder;
- private final Consumer<IBinder> mOnDiedConsumer;
-
- public RemoteCallbackBinder(IBinder binder, Consumer<IBinder> onBinderDied) {
- mCallbackBinder = binder;
- mOnDiedConsumer = onBinderDied;
- try {
- binder.linkToDeath(this, 0);
- } catch (RemoteException e) {
- logd(TAG, "Cannot link death recipient to binder " + mCallbackBinder + ", "
- + e);
- }
- }
-
- @Override
- public void binderDied() {
- logd(TAG, "Binder died " + mCallbackBinder);
- mOnDiedConsumer.accept(mCallbackBinder);
- cleanUp();
- }
-
- /** Clean up the client. */
- public void cleanUp() {
- mCallbackBinder.unlinkToDeath(this, 0);
- }
-
- /** Get the callback binder of the client. */
- public IBinder getCallbackBinder() {
- return mCallbackBinder;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (obj == this) {
- return true;
- }
- if (!(obj instanceof RemoteCallbackBinder)) {
- return false;
- }
- RemoteCallbackBinder remoteCallbackBinder = (RemoteCallbackBinder) obj;
- return mCallbackBinder.equals(remoteCallbackBinder.mCallbackBinder);
- }
-
- @Override
- public int hashCode() {
- return mCallbackBinder.hashCode();
- }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/util/SafeLog.java b/connected-device-lib/src/com/android/car/connecteddevice/util/SafeLog.java
deleted file mode 100644
index 8419e0f..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/util/SafeLog.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.util;
-
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-/**
- * Convenience logging methods that respect whitelisted tags.
- */
-public class SafeLog {
-
- private SafeLog() { }
-
- /** Log message if tag is whitelisted for {@code Log.VERBOSE}. */
- public static void logv(@NonNull String tag, @NonNull String message) {
- if (Log.isLoggable(tag, Log.VERBOSE)) {
- Log.v(tag, message);
- }
- }
-
- /** Log message if tag is whitelisted for {@code Log.INFO}. */
- public static void logi(@NonNull String tag, @NonNull String message) {
- if (Log.isLoggable(tag, Log.INFO)) {
- Log.i(tag, message);
- }
- }
-
- /** Log message if tag is whitelisted for {@code Log.DEBUG}. */
- public static void logd(@NonNull String tag, @NonNull String message) {
- if (Log.isLoggable(tag, Log.DEBUG)) {
- Log.d(tag, message);
- }
- }
-
- /** Log message if tag is whitelisted for {@code Log.WARN}. */
- public static void logw(@NonNull String tag, @NonNull String message) {
- if (Log.isLoggable(tag, Log.WARN)) {
- Log.w(tag, message);
- }
- }
-
- /** Log message if tag is whitelisted for {@code Log.ERROR}. */
- public static void loge(@NonNull String tag, @NonNull String message) {
- loge(tag, message, /* exception= */ null);
- }
-
- /** Log message and optional exception if tag is whitelisted for {@code Log.ERROR}. */
- public static void loge(@NonNull String tag, @NonNull String message,
- @Nullable Exception exception) {
- if (Log.isLoggable(tag, Log.ERROR)) {
- Log.e(tag, message, exception);
- }
- }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/util/ScanDataAnalyzer.java b/connected-device-lib/src/com/android/car/connecteddevice/util/ScanDataAnalyzer.java
deleted file mode 100644
index fcd6dc5..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/util/ScanDataAnalyzer.java
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.util;
-
-import static com.android.car.connecteddevice.util.SafeLog.logw;
-
-import android.bluetooth.le.ScanResult;
-
-import androidx.annotation.NonNull;
-
-import java.math.BigInteger;
-
-/**
- * Analyzer of {@link ScanResult} data to identify an Apple device that is advertising from the
- * background.
- */
-public class ScanDataAnalyzer {
-
- private static final String TAG = "ScanDataAnalyzer";
-
- private static final byte IOS_OVERFLOW_LENGTH = (byte) 0x14;
- private static final byte IOS_ADVERTISING_TYPE = (byte) 0xff;
- private static final int IOS_ADVERTISING_TYPE_LENGTH = 1;
- private static final long IOS_OVERFLOW_CUSTOM_ID = 0x4c0001;
- private static final int IOS_OVERFLOW_CUSTOM_ID_LENGTH = 3;
- private static final int IOS_OVERFLOW_CONTENT_LENGTH =
- IOS_OVERFLOW_LENGTH - IOS_OVERFLOW_CUSTOM_ID_LENGTH - IOS_ADVERTISING_TYPE_LENGTH;
-
- private ScanDataAnalyzer() { }
-
- /**
- * Returns {@code true} if the given bytes from a [ScanResult] contains service UUIDs once the
- * given serviceUuidMask is applied.
- *
- * When an iOS peripheral device goes into a background state, the service UUIDs and other
- * identifying information are removed from the advertising data and replaced with a hashed
- * bit in a special "overflow" area. There is no documentation on the layout of this area,
- * and the below was compiled from experimentation and examples from others who have worked
- * on reverse engineering iOS background peripherals.
- *
- * My best guess is Apple is taking the service UUID and hashing it into a bloom filter. This
- * would allow any device with the same hashing function to filter for all devices that
- * might contain the desired service. Since we do not have access to this hashing function,
- * we must first advertise our service from an iOS device and manually inspect the bit that
- * is flipped. Once known, it can be passed to serviceUuidMask and used as a filter.
- *
- * EXAMPLE
- *
- * Foreground contents:
- * 02011A1107FB349B5F8000008000100000C53A00000709546573746572000000000000000000000000000000000000000000000000000000000000000000
- *
- * Background contents:
- * 02011A14FF4C0001000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000
- *
- * The overflow bytes are comprised of four parts:
- * Length -> 14
- * Advertising type -> FF
- * Id custom to Apple -> 4C0001
- * Contents where hashed values are stored -> 00000000000000000000000000200000
- *
- * Apple's documentation on advertising from the background:
- * https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/CoreBluetoothBackgroundProcessingForIOSApps/PerformingTasksWhileYourAppIsInTheBackground.html#//apple_ref/doc/uid/TP40013257-CH7-SW9
- *
- * Other similar reverse engineering:
- * http://www.pagepinner.com/2014/04/how-to-get-ble-overflow-hash-bit-from.html
- */
- public static boolean containsUuidsInOverflow(@NonNull byte[] scanData,
- @NonNull BigInteger serviceUuidMask) {
- byte[] overflowBytes = new byte[IOS_OVERFLOW_CONTENT_LENGTH];
- int overflowPtr = 0;
- int outPtr = 0;
- try {
- while (overflowPtr < scanData.length - IOS_OVERFLOW_LENGTH) {
- byte length = scanData[overflowPtr++];
- if (length == 0) {
- break;
- } else if (length != IOS_OVERFLOW_LENGTH) {
- continue;
- }
-
- if (scanData[overflowPtr++] != IOS_ADVERTISING_TYPE) {
- return false;
- }
-
- byte[] idBytes = new byte[IOS_OVERFLOW_CUSTOM_ID_LENGTH];
- for (int i = 0; i < IOS_OVERFLOW_CUSTOM_ID_LENGTH; i++) {
- idBytes[i] = scanData[overflowPtr++];
- }
-
- if (!new BigInteger(idBytes).equals(BigInteger.valueOf(IOS_OVERFLOW_CUSTOM_ID))) {
- return false;
- }
-
- for (outPtr = 0; outPtr < IOS_OVERFLOW_CONTENT_LENGTH; outPtr++) {
- overflowBytes[outPtr] = scanData[overflowPtr++];
- }
- break;
- }
-
- if (outPtr == IOS_OVERFLOW_CONTENT_LENGTH) {
- BigInteger overflowBytesValue = new BigInteger(overflowBytes);
- return overflowBytesValue.and(serviceUuidMask).signum() == 1;
- }
-
- } catch (ArrayIndexOutOfBoundsException e) {
- logw(TAG, "Inspecting advertisement overflow bytes went out of bounds.");
- }
-
- return false;
- }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/util/ThreadSafeCallbacks.java b/connected-device-lib/src/com/android/car/connecteddevice/util/ThreadSafeCallbacks.java
deleted file mode 100644
index 152c464..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/util/ThreadSafeCallbacks.java
+++ /dev/null
@@ -1,59 +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.connecteddevice.util;
-
-import androidx.annotation.NonNull;
-
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.Executor;
-import java.util.function.Consumer;
-
-/**
- * Class for invoking thread-safe callbacks.
- *
- * @param <T> Callback type.
- */
-public class ThreadSafeCallbacks<T> {
-
- private final ConcurrentHashMap<T, Executor> mCallbacks = new ConcurrentHashMap<>();
-
- /** Add a callback to be notified on its executor. */
- public void add(@NonNull T callback, @NonNull Executor executor) {
- mCallbacks.put(callback, executor);
- }
-
- /** Remove a callback from the collection. */
- public void remove(@NonNull T callback) {
- mCallbacks.remove(callback);
- }
-
- /** Clear all callbacks from the collection. */
- public void clear() {
- mCallbacks.clear();
- }
-
- /** Return the number of callbacks in collection. */
- public int size() {
- return mCallbacks.size();
- }
-
- /** Invoke notification on all callbacks with their supplied {@link Executor}. */
- public void invoke(Consumer<T> notification) {
- mCallbacks.forEach((callback, executor) ->
- executor.execute(() -> notification.accept(callback)));
- }
-}
diff --git a/connected-device-lib/tests/unit/Android.bp b/connected-device-lib/tests/unit/Android.bp
deleted file mode 100644
index 92810a4..0000000
--- a/connected-device-lib/tests/unit/Android.bp
+++ /dev/null
@@ -1,54 +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.
-//
-
-android_test {
- name: "connected-device-lib-unit-tests",
-
- srcs: ["src/**/*.java"],
-
- libs: [
- "android.test.runner",
- "android.test.base",
- "android.test.mock",
- ],
-
- static_libs: [
- "android.car",
- "androidx.test.core",
- "androidx.test.ext.junit",
- "androidx.test.rules",
- "connected-device-lib",
- "mockito-target-extended-minus-junit4",
- "testables",
- // TODO: remove once Android migrates to JUnit 4.13,
- // which provides assertThrows
- "testng",
- "truth-prebuilt",
- ],
-
- jni_libs: [
- // For mockito extended
- "libdexmakerjvmtiagent",
- "libstaticjvmtiagent",
- ],
-
- sdk_version: "system_current",
- min_sdk_version: "28",
-
- certificate: "platform",
-
- privileged: true,
-}
\ No newline at end of file
diff --git a/connected-device-lib/tests/unit/AndroidManifest.xml b/connected-device-lib/tests/unit/AndroidManifest.xml
deleted file mode 100644
index 9863ccf..0000000
--- a/connected-device-lib/tests/unit/AndroidManifest.xml
+++ /dev/null
@@ -1,37 +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.
- -->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.car.connecteddevice.tests.unit">
-
- <!-- Needed for BLE scanning/advertising -->
- <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
- <uses-permission android:name="android.permission.BLUETOOTH"/>
- <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
-
- <!-- Needed for detecting foreground user -->
- <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>
- <uses-permission android:name="android.permission.MANAGE_USERS" />
-
- <application android:testOnly="true"
- android:debuggable="true">
- <uses-library android:name="android.test.runner" />
- </application>
-
- <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
- android:targetPackage="com.android.car.connecteddevice.tests.unit"
- android:label="Connected Device Lib Test Cases" />
-</manifest>
diff --git a/connected-device-lib/tests/unit/README.md b/connected-device-lib/tests/unit/README.md
deleted file mode 100644
index 4543058..0000000
--- a/connected-device-lib/tests/unit/README.md
+++ /dev/null
@@ -1,24 +0,0 @@
-# Instructions for running unit tests
-
-### Build unit test module
-
-`m connected-device-lib-unit-tests`
-
-### Install resulting apk on device
-
-`adb install -r -t $OUT/testcases/connected-device-lib-unit-tests/arm64/connected-device-lib-unit-tests.apk`
-
-### Run all tests
-
-`adb shell am instrument -w com.android.car.connecteddevice.tests.unit`
-
-### Run tests in a class
-
-`adb shell am instrument -w -e class com.android.car.connecteddevice.<classPath> com.android.car.connecteddevice.tests.unit`
-
-### Run a specific test
-
-`adb shell am instrument -w -e class com.android.car.connecteddevice.<classPath>#<testMethod> com.android.car.connecteddevice.tests.unit`
-
-More general information can be found at
-http://developer.android.com/reference/android/support/test/runner/AndroidJUnitRunner.html
\ No newline at end of file
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/ConnectedDeviceManagerTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/ConnectedDeviceManagerTest.java
deleted file mode 100644
index fe0386c..0000000
--- a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/ConnectedDeviceManagerTest.java
+++ /dev/null
@@ -1,743 +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.connecteddevice;
-
-import static com.android.car.connecteddevice.ConnectedDeviceManager.DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED;
-import static com.android.car.connecteddevice.ConnectedDeviceManager.DEVICE_ERROR_INVALID_SECURITY_KEY;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.mockitoSession;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import androidx.annotation.NonNull;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import com.android.car.connecteddevice.ConnectedDeviceManager.ConnectionCallback;
-import com.android.car.connecteddevice.ConnectedDeviceManager.DeviceAssociationCallback;
-import com.android.car.connecteddevice.ConnectedDeviceManager.DeviceCallback;
-import com.android.car.connecteddevice.ConnectedDeviceManager.MessageDeliveryDelegate;
-import com.android.car.connecteddevice.connection.CarBluetoothManager;
-import com.android.car.connecteddevice.connection.DeviceMessage;
-import com.android.car.connecteddevice.model.AssociatedDevice;
-import com.android.car.connecteddevice.model.ConnectedDevice;
-import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
-import com.android.car.connecteddevice.storage.ConnectedDeviceStorage.AssociatedDeviceCallback;
-import com.android.car.connecteddevice.util.ByteUtils;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoSession;
-import org.mockito.quality.Strictness;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.UUID;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
-
-@RunWith(AndroidJUnit4.class)
-public class ConnectedDeviceManagerTest {
-
- private static final String TEST_DEVICE_ADDRESS = "00:11:22:33:44:55";
-
- private static final String TEST_DEVICE_NAME = "TEST_DEVICE_NAME";
-
-
- private final Executor mCallbackExecutor = Executors.newSingleThreadExecutor();
-
- private final UUID mRecipientId = UUID.randomUUID();
-
- private final List<String> mUserDeviceIds = new ArrayList<>();
-
- private final List<AssociatedDevice> mUserDevices = new ArrayList<>();
-
- @Mock
- private ConnectedDeviceStorage mMockStorage;
-
- @Mock
- private CarBluetoothManager mMockCarBluetoothManager;
-
- private ConnectedDeviceManager mConnectedDeviceManager;
-
- private MockitoSession mMockingSession;
-
- private AssociatedDeviceCallback mAssociatedDeviceCallback;
-
- @Before
- public void setUp() {
- mMockingSession = mockitoSession()
- .initMocks(this)
- .strictness(Strictness.WARN)
- .startMocking();
- ArgumentCaptor<AssociatedDeviceCallback> callbackCaptor = ArgumentCaptor
- .forClass(AssociatedDeviceCallback.class);
- mConnectedDeviceManager = new ConnectedDeviceManager(mMockCarBluetoothManager,
- mMockStorage);
- verify(mMockStorage).setAssociatedDeviceCallback(callbackCaptor.capture());
- when(mMockStorage.getActiveUserAssociatedDevices()).thenReturn(mUserDevices);
- when(mMockStorage.getActiveUserAssociatedDeviceIds()).thenReturn(mUserDeviceIds);
- mAssociatedDeviceCallback = callbackCaptor.getValue();
- mConnectedDeviceManager.start();
- }
-
- @After
- public void tearDown() {
- if (mMockingSession != null) {
- mMockingSession.finishMocking();
- }
- }
-
- @Test
- public void getActiveUserConnectedDevices_initiallyShouldReturnEmptyList() {
- assertThat(mConnectedDeviceManager.getActiveUserConnectedDevices()).isEmpty();
- }
-
- @Test
- public void getActiveUserConnectedDevices_includesNewlyConnectedDevice() {
- String deviceId = connectNewDevice();
- List<ConnectedDevice> activeUserDevices =
- mConnectedDeviceManager.getActiveUserConnectedDevices();
- ConnectedDevice expectedDevice = new ConnectedDevice(deviceId, /* deviceName= */ null,
- /* belongsToActiveUser= */ true, /* hasSecureChannel= */ false);
- assertThat(activeUserDevices).containsExactly(expectedDevice);
- }
-
- @Test
- public void getActiveUserConnectedDevices_excludesDevicesNotBelongingToActiveUser() {
- String deviceId = UUID.randomUUID().toString();
- String otherUserDeviceId = UUID.randomUUID().toString();
- when(mMockStorage.getActiveUserAssociatedDeviceIds()).thenReturn(
- Collections.singletonList(otherUserDeviceId));
- mConnectedDeviceManager.addConnectedDevice(deviceId);
- assertThat(mConnectedDeviceManager.getActiveUserConnectedDevices()).isEmpty();
- }
-
- @Test
- public void getActiveUserConnectedDevices_reflectsSecureChannelEstablished() {
- String deviceId = connectNewDevice();
- mConnectedDeviceManager.onSecureChannelEstablished(deviceId);
- ConnectedDevice connectedDevice =
- mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
- assertThat(connectedDevice.hasSecureChannel()).isTrue();
- }
-
- @Test
- public void getActiveUserConnectedDevices_excludesDisconnectedDevice() {
- String deviceId = connectNewDevice();
- mConnectedDeviceManager.removeConnectedDevice(deviceId);
- assertThat(mConnectedDeviceManager.getActiveUserConnectedDevices()).isEmpty();
- }
-
- @Test(expected = IllegalStateException.class)
- public void sendMessageSecurely_throwsIllegalStateExceptionIfNoSecureChannel() {
- connectNewDevice();
- ConnectedDevice device = mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
- UUID recipientId = UUID.randomUUID();
- byte[] message = ByteUtils.randomBytes(10);
- mConnectedDeviceManager.sendMessageSecurely(device, recipientId, message);
- }
-
- @Test
- public void sendMessageSecurely_sendsEncryptedMessage() {
- String deviceId = connectNewDevice();
- mConnectedDeviceManager.onSecureChannelEstablished(deviceId);
- ConnectedDevice device = mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
- UUID recipientId = UUID.randomUUID();
- byte[] message = ByteUtils.randomBytes(10);
- mConnectedDeviceManager.sendMessageSecurely(device, recipientId, message);
- ArgumentCaptor<DeviceMessage> messageCaptor = ArgumentCaptor.forClass(DeviceMessage.class);
- verify(mMockCarBluetoothManager).sendMessage(eq(deviceId), messageCaptor.capture());
- assertThat(messageCaptor.getValue().isMessageEncrypted()).isTrue();
- }
-
- @Test
- public void sendMessageSecurely_doesNotSendIfDeviceDisconnected() {
- String deviceId = connectNewDevice();
- ConnectedDevice device = mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
- mConnectedDeviceManager.removeConnectedDevice(deviceId);
- UUID recipientId = UUID.randomUUID();
- byte[] message = ByteUtils.randomBytes(10);
- mConnectedDeviceManager.sendMessageSecurely(device, recipientId, message);
- verify(mMockCarBluetoothManager, times(0)).sendMessage(eq(deviceId),
- any(DeviceMessage.class));
- }
-
- @Test
- public void sendMessageUnsecurely_sendsMessageWithoutEncryption() {
- String deviceId = connectNewDevice();
- ConnectedDevice device = mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
- UUID recipientId = UUID.randomUUID();
- byte[] message = ByteUtils.randomBytes(10);
- mConnectedDeviceManager.sendMessageUnsecurely(device, recipientId, message);
- ArgumentCaptor<DeviceMessage> messageCaptor = ArgumentCaptor.forClass(DeviceMessage.class);
- verify(mMockCarBluetoothManager).sendMessage(eq(deviceId), messageCaptor.capture());
- assertThat(messageCaptor.getValue().isMessageEncrypted()).isFalse();
- }
-
- @Test
- public void connectionCallback_onDeviceConnectedInvokedForNewlyConnectedDevice()
- throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- ConnectionCallback connectionCallback = createConnectionCallback(semaphore);
- mConnectedDeviceManager.registerActiveUserConnectionCallback(connectionCallback,
- mCallbackExecutor);
- String deviceId = connectNewDevice();
- assertThat(tryAcquire(semaphore)).isTrue();
- ArgumentCaptor<ConnectedDevice> deviceCaptor =
- ArgumentCaptor.forClass(ConnectedDevice.class);
- verify(connectionCallback).onDeviceConnected(deviceCaptor.capture());
- ConnectedDevice connectedDevice = deviceCaptor.getValue();
- assertThat(connectedDevice.getDeviceId()).isEqualTo(deviceId);
- assertThat(connectedDevice.hasSecureChannel()).isFalse();
- }
-
- @Test
- public void connectionCallback_onDeviceConnectedNotInvokedDeviceConnectedForDifferentUser()
- throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- ConnectionCallback connectionCallback = createConnectionCallback(semaphore);
- mConnectedDeviceManager.registerActiveUserConnectionCallback(connectionCallback,
- mCallbackExecutor);
- String deviceId = UUID.randomUUID().toString();
- String otherUserDeviceId = UUID.randomUUID().toString();
- when(mMockStorage.getActiveUserAssociatedDeviceIds()).thenReturn(
- Collections.singletonList(otherUserDeviceId));
- mConnectedDeviceManager.addConnectedDevice(deviceId);
- assertThat(tryAcquire(semaphore)).isFalse();
- }
-
- @Test
- public void connectionCallback_onDeviceConnectedNotInvokedForDifferentBleManager()
- throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- String deviceId = connectNewDevice();
- ConnectionCallback connectionCallback = createConnectionCallback(semaphore);
- mConnectedDeviceManager.registerActiveUserConnectionCallback(connectionCallback,
- mCallbackExecutor);
- mConnectedDeviceManager.addConnectedDevice(deviceId);
- assertThat(tryAcquire(semaphore)).isFalse();
- }
-
- @Test
- public void connectionCallback_onDeviceDisconnectedInvokedForActiveUserDevice()
- throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- String deviceId = connectNewDevice();
- ConnectionCallback connectionCallback = createConnectionCallback(semaphore);
- mConnectedDeviceManager.registerActiveUserConnectionCallback(connectionCallback,
- mCallbackExecutor);
- mConnectedDeviceManager.removeConnectedDevice(deviceId);
- assertThat(tryAcquire(semaphore)).isTrue();
- ArgumentCaptor<ConnectedDevice> deviceCaptor =
- ArgumentCaptor.forClass(ConnectedDevice.class);
- verify(connectionCallback).onDeviceDisconnected(deviceCaptor.capture());
- assertThat(deviceCaptor.getValue().getDeviceId()).isEqualTo(deviceId);
- }
-
- @Test
- public void connectionCallback_onDeviceDisconnectedNotInvokedDeviceForDifferentUser()
- throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- String deviceId = UUID.randomUUID().toString();
- mConnectedDeviceManager.addConnectedDevice(deviceId);
- ConnectionCallback connectionCallback = createConnectionCallback(semaphore);
- mConnectedDeviceManager.registerActiveUserConnectionCallback(connectionCallback,
- mCallbackExecutor);
- mConnectedDeviceManager.removeConnectedDevice(deviceId);
- assertThat(tryAcquire(semaphore)).isFalse();
- }
-
- @Test
- public void unregisterConnectionCallback_removesCallbackAndNotInvoked()
- throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- ConnectionCallback connectionCallback = createConnectionCallback(semaphore);
- mConnectedDeviceManager.registerActiveUserConnectionCallback(connectionCallback,
- mCallbackExecutor);
- mConnectedDeviceManager.unregisterConnectionCallback(connectionCallback);
- connectNewDevice();
- assertThat(tryAcquire(semaphore)).isFalse();
- }
-
- @Test
- public void registerDeviceCallback_blacklistsDuplicateRecipientId()
- throws InterruptedException {
- connectNewDevice();
- ConnectedDevice connectedDevice =
- mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
- Semaphore firstSemaphore = new Semaphore(0);
- Semaphore secondSemaphore = new Semaphore(0);
- Semaphore thirdSemaphore = new Semaphore(0);
- DeviceCallback firstDeviceCallback = createDeviceCallback(firstSemaphore);
- DeviceCallback secondDeviceCallback = createDeviceCallback(secondSemaphore);
- DeviceCallback thirdDeviceCallback = createDeviceCallback(thirdSemaphore);
-
- // Register three times for following chain of events:
- // 1. First callback registered without issue.
- // 2. Second callback with same recipientId triggers blacklisting both callbacks and issues
- // error callbacks on both. Both callbacks should be unregistered at this point.
- // 3. Third callback gets rejected at registration and issues error callback.
-
- mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
- firstDeviceCallback, mCallbackExecutor);
- mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
- secondDeviceCallback, mCallbackExecutor);
- DeviceMessage message = new DeviceMessage(mRecipientId, false, new byte[10]);
- mConnectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
- assertThat(tryAcquire(firstSemaphore)).isTrue();
- assertThat(tryAcquire(secondSemaphore)).isTrue();
- verify(firstDeviceCallback)
- .onDeviceError(connectedDevice, DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED);
- verify(secondDeviceCallback)
- .onDeviceError(connectedDevice, DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED);
- verify(firstDeviceCallback, times(0)).onMessageReceived(any(), any());
- verify(secondDeviceCallback, times(0)).onMessageReceived(any(), any());
-
- mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
- thirdDeviceCallback, mCallbackExecutor);
- assertThat(tryAcquire(thirdSemaphore)).isTrue();
- verify(thirdDeviceCallback)
- .onDeviceError(connectedDevice, DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED);
- }
-
- @Test
- public void deviceCallback_onSecureChannelEstablishedInvoked() throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- connectNewDevice();
- ConnectedDevice connectedDevice =
- mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
- DeviceCallback deviceCallback = createDeviceCallback(semaphore);
- mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
- deviceCallback, mCallbackExecutor);
- mConnectedDeviceManager.onSecureChannelEstablished(connectedDevice.getDeviceId());
- connectedDevice =
- mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
- assertThat(tryAcquire(semaphore)).isTrue();
- verify(deviceCallback).onSecureChannelEstablished(connectedDevice);
- }
-
- @Test
- public void deviceCallback_onMessageReceivedInvokedForSameRecipientId()
- throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- connectNewDevice();
- ConnectedDevice connectedDevice =
- mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
- DeviceCallback deviceCallback = createDeviceCallback(semaphore);
- mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
- deviceCallback, mCallbackExecutor);
- byte[] payload = ByteUtils.randomBytes(10);
- DeviceMessage message = new DeviceMessage(mRecipientId, false, payload);
- mConnectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
- assertThat(tryAcquire(semaphore)).isTrue();
- verify(deviceCallback).onMessageReceived(connectedDevice, payload);
- }
-
- @Test
- public void deviceCallback_onMessageReceivedNotInvokedForDifferentRecipientId()
- throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- connectNewDevice();
- ConnectedDevice connectedDevice =
- mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
- DeviceCallback deviceCallback = createDeviceCallback(semaphore);
- mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
- deviceCallback, mCallbackExecutor);
- byte[] payload = ByteUtils.randomBytes(10);
- DeviceMessage message = new DeviceMessage(UUID.randomUUID(), false, payload);
- mConnectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
- assertThat(tryAcquire(semaphore)).isFalse();
- }
-
- @Test
- public void deviceCallback_onDeviceErrorInvokedOnChannelError() throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- connectNewDevice();
- ConnectedDevice connectedDevice =
- mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
- DeviceCallback deviceCallback = createDeviceCallback(semaphore);
- mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
- deviceCallback, mCallbackExecutor);
- mConnectedDeviceManager.deviceErrorOccurred(connectedDevice.getDeviceId());
- assertThat(tryAcquire(semaphore)).isTrue();
- verify(deviceCallback).onDeviceError(connectedDevice, DEVICE_ERROR_INVALID_SECURITY_KEY);
- }
-
- @Test
- public void unregisterDeviceCallback_removesCallbackAndNotInvoked()
- throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- connectNewDevice();
- ConnectedDevice connectedDevice =
- mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
- DeviceCallback deviceCallback = createDeviceCallback(semaphore);
- mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
- deviceCallback, mCallbackExecutor);
- mConnectedDeviceManager.unregisterDeviceCallback(connectedDevice, mRecipientId,
- deviceCallback);
- mConnectedDeviceManager.onSecureChannelEstablished(connectedDevice.getDeviceId());
- assertThat(tryAcquire(semaphore)).isFalse();
- }
-
- @Test
- public void registerDeviceCallback_sendsMissedMessageAfterRegistration()
- throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- connectNewDevice();
- ConnectedDevice connectedDevice =
- mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
- byte[] payload = ByteUtils.randomBytes(10);
- DeviceMessage message = new DeviceMessage(mRecipientId, false, payload);
- mConnectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
- DeviceCallback deviceCallback = createDeviceCallback(semaphore);
- mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
- deviceCallback, mCallbackExecutor);
- assertThat(tryAcquire(semaphore)).isTrue();
- verify(deviceCallback).onMessageReceived(connectedDevice, payload);
- }
-
- @Test
- public void registerDeviceCallback_sendsMultipleMissedMessagesAfterRegistration()
- throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- connectNewDevice();
- ConnectedDevice connectedDevice =
- mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
- byte[] payload1 = ByteUtils.randomBytes(10);
- byte[] payload2 = ByteUtils.randomBytes(10);
- DeviceMessage message1 = new DeviceMessage(mRecipientId, false, payload1);
- DeviceMessage message2 = new DeviceMessage(mRecipientId, false, payload2);
- mConnectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message1);
- mConnectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message2);
- DeviceCallback deviceCallback = createDeviceCallback(semaphore);
- mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
- deviceCallback, mCallbackExecutor);
- assertThat(tryAcquire(semaphore)).isTrue();
- verify(deviceCallback).onMessageReceived(connectedDevice, payload1);
- verify(deviceCallback, timeout(1000)).onMessageReceived(connectedDevice, payload2);
- }
-
- @Test
- public void registerDeviceCallback_doesNotSendMissedMessageForDifferentRecipient()
- throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- connectNewDevice();
- ConnectedDevice connectedDevice =
- mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
- byte[] payload = ByteUtils.randomBytes(10);
- DeviceMessage message = new DeviceMessage(UUID.randomUUID(), false, payload);
- mConnectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
- DeviceCallback deviceCallback = createDeviceCallback(semaphore);
- mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
- deviceCallback, mCallbackExecutor);
- assertThat(tryAcquire(semaphore)).isFalse();
- }
-
- @Test
- public void registerDeviceCallback_doesNotSendMissedMessageForDifferentDevice()
- throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- connectNewDevice();
- connectNewDevice();
- List<ConnectedDevice> connectedDevices =
- mConnectedDeviceManager.getActiveUserConnectedDevices();
- ConnectedDevice connectedDevice = connectedDevices.get(0);
- ConnectedDevice otherDevice = connectedDevices.get(1);
- byte[] payload = ByteUtils.randomBytes(10);
- DeviceMessage message = new DeviceMessage(mRecipientId, false, payload);
- mConnectedDeviceManager.onMessageReceived(otherDevice.getDeviceId(), message);
- DeviceCallback deviceCallback = createDeviceCallback(semaphore);
- mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
- deviceCallback, mCallbackExecutor);
- assertThat(tryAcquire(semaphore)).isFalse();
- }
-
- @Test
- public void onAssociationCompleted_disconnectsOriginalDeviceAndReconnectsAsActiveUser()
- throws InterruptedException {
- String deviceId = UUID.randomUUID().toString();
- mConnectedDeviceManager.addConnectedDevice(deviceId);
- Semaphore semaphore = new Semaphore(0);
- ConnectionCallback connectionCallback = createConnectionCallback(semaphore);
- mConnectedDeviceManager.registerActiveUserConnectionCallback(connectionCallback,
- mCallbackExecutor);
- when(mMockStorage.getActiveUserAssociatedDeviceIds()).thenReturn(
- Collections.singletonList(deviceId));
- mConnectedDeviceManager.onAssociationCompleted(deviceId);
- assertThat(tryAcquire(semaphore)).isTrue();
- }
-
- private boolean tryAcquire(Semaphore semaphore) throws InterruptedException {
- return semaphore.tryAcquire(100, TimeUnit.MILLISECONDS);
- }
-
- @Test
- public void deviceAssociationCallback_onAssociatedDeviceAdded() throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- DeviceAssociationCallback callback = createDeviceAssociationCallback(semaphore);
- mConnectedDeviceManager.registerDeviceAssociationCallback(callback, mCallbackExecutor);
- String deviceId = UUID.randomUUID().toString();
- AssociatedDevice testDevice = new AssociatedDevice(deviceId, TEST_DEVICE_ADDRESS,
- TEST_DEVICE_NAME, /* isConnectionEnabled= */ true);
- mAssociatedDeviceCallback.onAssociatedDeviceAdded(testDevice);
- assertThat(tryAcquire(semaphore)).isTrue();
- verify(callback).onAssociatedDeviceAdded(eq(testDevice));
- }
-
- @Test
- public void deviceAssociationCallback_onAssociationDeviceRemoved() throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- DeviceAssociationCallback callback = createDeviceAssociationCallback(semaphore);
- mConnectedDeviceManager.registerDeviceAssociationCallback(callback, mCallbackExecutor);
- String deviceId = UUID.randomUUID().toString();
- AssociatedDevice testDevice = new AssociatedDevice(deviceId, TEST_DEVICE_ADDRESS,
- TEST_DEVICE_NAME, /* isConnectionEnabled= */ true);
- mAssociatedDeviceCallback.onAssociatedDeviceRemoved(testDevice);
- assertThat(tryAcquire(semaphore)).isTrue();
- verify(callback).onAssociatedDeviceRemoved(eq(testDevice));
- }
-
- @Test
- public void deviceAssociationCallback_onAssociatedDeviceUpdated() throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- DeviceAssociationCallback callback = createDeviceAssociationCallback(semaphore);
- mConnectedDeviceManager.registerDeviceAssociationCallback(callback, mCallbackExecutor);
- String deviceId = UUID.randomUUID().toString();
- AssociatedDevice testDevice = new AssociatedDevice(deviceId, TEST_DEVICE_ADDRESS,
- TEST_DEVICE_NAME, /* isConnectionEnabled= */ true);
- mAssociatedDeviceCallback.onAssociatedDeviceUpdated(testDevice);
- assertThat(tryAcquire(semaphore)).isTrue();
- verify(callback).onAssociatedDeviceUpdated(eq(testDevice));
- }
-
- @Test
- public void removeConnectedDevice_startsAdvertisingForActiveUserDeviceOnActiveUserDisconnect() {
- String deviceId = UUID.randomUUID().toString();
- when(mMockStorage.getActiveUserAssociatedDeviceIds()).thenReturn(
- Collections.singletonList(deviceId));
- AssociatedDevice device = new AssociatedDevice(deviceId, TEST_DEVICE_ADDRESS,
- TEST_DEVICE_NAME, /* isConnectionEnabled= */ true);
- when(mMockStorage.getActiveUserAssociatedDevices()).thenReturn(
- Collections.singletonList(device));
- mConnectedDeviceManager.addConnectedDevice(deviceId);
- clearInvocations(mMockCarBluetoothManager);
- mConnectedDeviceManager.removeConnectedDevice(deviceId);
- verify(mMockCarBluetoothManager, timeout(1000))
- .connectToDevice(eq(UUID.fromString(deviceId)));
- }
-
- @Test
- public void removeConnectedDevice_startsAdvertisingForActiveUserDeviceOnLastDeviceDisconnect() {
- String deviceId = UUID.randomUUID().toString();
- String userDeviceId = UUID.randomUUID().toString();
- when(mMockStorage.getActiveUserAssociatedDeviceIds()).thenReturn(
- Collections.singletonList(userDeviceId));
- AssociatedDevice userDevice = new AssociatedDevice(userDeviceId, TEST_DEVICE_ADDRESS,
- TEST_DEVICE_NAME, /* isConnectionEnabled= */ true);
- when(mMockStorage.getActiveUserAssociatedDevices()).thenReturn(
- Collections.singletonList(userDevice));
- mConnectedDeviceManager.addConnectedDevice(deviceId);
- clearInvocations(mMockCarBluetoothManager);
- mConnectedDeviceManager.removeConnectedDevice(deviceId);
- verify(mMockCarBluetoothManager, timeout(1000)).connectToDevice(
- eq(UUID.fromString(userDeviceId)));
- }
-
- @Test
- public void removeConnectedDevice_doesNotAdvertiseForNonActiveUserDeviceNotLastDevice() {
- String deviceId = UUID.randomUUID().toString();
- String userDeviceId = UUID.randomUUID().toString();
- when(mMockStorage.getActiveUserAssociatedDeviceIds()).thenReturn(
- Collections.singletonList(userDeviceId));
- AssociatedDevice userDevice = new AssociatedDevice(userDeviceId, TEST_DEVICE_ADDRESS,
- TEST_DEVICE_NAME, /* isConnectionEnabled= */ true);
- when(mMockStorage.getActiveUserAssociatedDevices()).thenReturn(
- Collections.singletonList(userDevice));
- mConnectedDeviceManager.addConnectedDevice(deviceId);
- mConnectedDeviceManager.addConnectedDevice(userDeviceId);
- clearInvocations(mMockCarBluetoothManager);
- mConnectedDeviceManager.removeConnectedDevice(deviceId);
- verify(mMockCarBluetoothManager, timeout(1000).times(0))
- .connectToDevice(any());
- }
-
- @Test
- public void removeActiveUserAssociatedDevice_deletesAssociatedDeviceFromStorage() {
- String deviceId = UUID.randomUUID().toString();
- mConnectedDeviceManager.removeActiveUserAssociatedDevice(deviceId);
- verify(mMockStorage).removeAssociatedDeviceForActiveUser(deviceId);
- }
-
- @Test
- public void removeActiveUserAssociatedDevice_disconnectsIfConnected() {
- String deviceId = connectNewDevice();
- mConnectedDeviceManager.removeActiveUserAssociatedDevice(deviceId);
- verify(mMockCarBluetoothManager).disconnectDevice(deviceId);
- }
-
- @Test
- public void enableAssociatedDeviceConnection_enableDeviceConnectionInStorage() {
- String deviceId = UUID.randomUUID().toString();
- mConnectedDeviceManager.enableAssociatedDeviceConnection(deviceId);
- verify(mMockStorage).updateAssociatedDeviceConnectionEnabled(deviceId, true);
- }
-
- @Test
- public void disableAssociatedDeviceConnection_disableDeviceConnectionInStorage() {
- String deviceId = UUID.randomUUID().toString();
- mConnectedDeviceManager.disableAssociatedDeviceConnection(deviceId);
- verify(mMockStorage).updateAssociatedDeviceConnectionEnabled(deviceId, false);
- }
-
- @Test
- public void disableAssociatedDeviceConnection_disconnectsIfConnected() {
- String deviceId = connectNewDevice();
- mConnectedDeviceManager.disableAssociatedDeviceConnection(deviceId);
- verify(mMockCarBluetoothManager).disconnectDevice(deviceId);
- }
-
- @Test
- public void onMessageReceived_deliversMessageIfDelegateIsNull() throws InterruptedException {
- connectNewDevice();
- ConnectedDevice connectedDevice =
- mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
- Semaphore semaphore = new Semaphore(0);
- DeviceCallback deviceCallback = createDeviceCallback(semaphore);
- mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
- deviceCallback, mCallbackExecutor);
- DeviceMessage message = new DeviceMessage(mRecipientId, false, new byte[10]);
- mConnectedDeviceManager.setMessageDeliveryDelegate(null);
- mConnectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
- assertThat(tryAcquire(semaphore)).isTrue();
- }
-
- @Test
- public void onMessageReceived_deliversMessageIfDelegateAccepts() throws InterruptedException {
- connectNewDevice();
- ConnectedDevice connectedDevice =
- mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
- Semaphore semaphore = new Semaphore(0);
- DeviceCallback deviceCallback = createDeviceCallback(semaphore);
- mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
- deviceCallback, mCallbackExecutor);
- DeviceMessage message = new DeviceMessage(mRecipientId, false, new byte[10]);
- MessageDeliveryDelegate delegate = device -> true;
- mConnectedDeviceManager.setMessageDeliveryDelegate(delegate);
- mConnectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
- assertThat(tryAcquire(semaphore)).isTrue();
- }
-
- @Test
- public void onMessageReceived_doesNotDeliverMessageIfDelegateRejects()
- throws InterruptedException {
- connectNewDevice();
- ConnectedDevice connectedDevice =
- mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
- Semaphore semaphore = new Semaphore(0);
- DeviceCallback deviceCallback = createDeviceCallback(semaphore);
- mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
- deviceCallback, mCallbackExecutor);
- DeviceMessage message = new DeviceMessage(mRecipientId, false, new byte[10]);
- MessageDeliveryDelegate delegate = device -> false;
- mConnectedDeviceManager.setMessageDeliveryDelegate(delegate);
- mConnectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
- assertThat(tryAcquire(semaphore)).isFalse();
- }
-
- @NonNull
- private String connectNewDevice() {
- String deviceId = UUID.randomUUID().toString();
- AssociatedDevice device = new AssociatedDevice(deviceId, TEST_DEVICE_ADDRESS,
- TEST_DEVICE_NAME, /* isConnectionEnabled= */ true);
- mUserDeviceIds.add(deviceId);
- mUserDevices.add(device);
- mConnectedDeviceManager.addConnectedDevice(deviceId);
- return deviceId;
- }
-
- @NonNull
- private ConnectionCallback createConnectionCallback(@NonNull final Semaphore semaphore) {
- return spy(new ConnectionCallback() {
- @Override
- public void onDeviceConnected(ConnectedDevice device) {
- semaphore.release();
- }
-
- @Override
- public void onDeviceDisconnected(ConnectedDevice device) {
- semaphore.release();
- }
- });
- }
-
- @NonNull
- private DeviceCallback createDeviceCallback(@NonNull final Semaphore semaphore) {
- return spy(new DeviceCallback() {
- @Override
- public void onSecureChannelEstablished(ConnectedDevice device) {
- semaphore.release();
- }
-
- @Override
- public void onMessageReceived(ConnectedDevice device, byte[] message) {
- semaphore.release();
- }
-
- @Override
- public void onDeviceError(ConnectedDevice device, int error) {
- semaphore.release();
- }
- });
- }
-
- @NonNull
- private DeviceAssociationCallback createDeviceAssociationCallback(
- @NonNull final Semaphore semaphore) {
- return spy(new DeviceAssociationCallback() {
- @Override
- public void onAssociatedDeviceAdded(AssociatedDevice device) {
- semaphore.release();
- }
-
- @Override
- public void onAssociatedDeviceRemoved(
- AssociatedDevice device) {
- semaphore.release();
- }
-
- @Override
- public void onAssociatedDeviceUpdated(AssociatedDevice device) {
- semaphore.release();
- }
- });
- }
-}
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/AssociationSecureChannelTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/AssociationSecureChannelTest.java
deleted file mode 100644
index 118eca9..0000000
--- a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/AssociationSecureChannelTest.java
+++ /dev/null
@@ -1,236 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.connection;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.anyString;
-import static org.mockito.Mockito.mockitoSession;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.car.encryptionrunner.EncryptionRunner;
-import android.car.encryptionrunner.EncryptionRunnerFactory;
-import android.car.encryptionrunner.FakeEncryptionRunner;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import com.android.car.connecteddevice.StreamProtos.OperationProto.OperationType;
-import com.android.car.connecteddevice.connection.ble.BleDeviceMessageStream;
-import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
-import com.android.car.connecteddevice.util.ByteUtils;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoSession;
-import org.mockito.quality.Strictness;
-
-import java.util.UUID;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
-
-@RunWith(AndroidJUnit4.class)
-public final class AssociationSecureChannelTest {
- private static final UUID CLIENT_DEVICE_ID =
- UUID.fromString("a5645523-3280-410a-90c1-582a6c6f4969");
- private static final UUID SERVER_DEVICE_ID =
- UUID.fromString("a29f0c74-2014-4b14-ac02-be6ed15b545a");
- private static final byte[] CLIENT_SECRET = ByteUtils.randomBytes(32);
-
- @Mock
- private BleDeviceMessageStream mStreamMock;
- @Mock
- private ConnectedDeviceStorage mStorageMock;
- @Mock
- private AssociationSecureChannel.ShowVerificationCodeListener mShowVerificationCodeListenerMock;
- private MockitoSession mMockitoSession;
-
- private AssociationSecureChannel mChannel;
- private DeviceMessageStream.MessageReceivedListener mMessageReceivedListener;
-
- @Before
- public void setUp() {
- mMockitoSession = mockitoSession()
- .initMocks(this)
- .strictness(Strictness.WARN)
- .startMocking();
- when(mStorageMock.getUniqueId()).thenReturn(SERVER_DEVICE_ID);
- }
-
- @After
- public void tearDown() {
- if (mMockitoSession != null) {
- mMockitoSession.finishMocking();
- }
- }
-
- @Test
- public void testEncryptionHandshake_Association() throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- ChannelCallback callbackSpy = spy(new ChannelCallback(semaphore));
- setupAssociationSecureChannel(callbackSpy, EncryptionRunnerFactory::newFakeRunner);
- ArgumentCaptor<String> deviceIdCaptor = ArgumentCaptor.forClass(String.class);
- ArgumentCaptor<DeviceMessage> messageCaptor =
- ArgumentCaptor.forClass(DeviceMessage.class);
-
- initHandshakeMessage();
- verify(mStreamMock).writeMessage(messageCaptor.capture(), any());
- byte[] response = messageCaptor.getValue().getMessage();
- assertThat(response).isEqualTo(FakeEncryptionRunner.INIT_RESPONSE.getBytes());
-
- respondToContinueMessage();
- verify(mShowVerificationCodeListenerMock).showVerificationCode(anyString());
-
- mChannel.notifyOutOfBandAccepted();
- sendDeviceId();
- assertThat(semaphore.tryAcquire(100, TimeUnit.MILLISECONDS)).isTrue();
- verify(callbackSpy).onDeviceIdReceived(deviceIdCaptor.capture());
- verify(mStreamMock, times(2)).writeMessage(messageCaptor.capture(), any());
- byte[] deviceIdMessage = messageCaptor.getValue().getMessage();
- assertThat(deviceIdMessage).isEqualTo(ByteUtils.uuidToBytes(SERVER_DEVICE_ID));
- assertThat(deviceIdCaptor.getValue()).isEqualTo(CLIENT_DEVICE_ID.toString());
- verify(mStorageMock).saveEncryptionKey(eq(CLIENT_DEVICE_ID.toString()), any());
- verify(mStorageMock).saveChallengeSecret(CLIENT_DEVICE_ID.toString(), CLIENT_SECRET);
-
- assertThat(semaphore.tryAcquire(100, TimeUnit.MILLISECONDS)).isTrue();
- verify(callbackSpy).onSecureChannelEstablished();
- }
-
- @Test
- public void testEncryptionHandshake_Association_wrongInitHandshakeMessage()
- throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- ChannelCallback callbackSpy = spy(new ChannelCallback(semaphore));
- setupAssociationSecureChannel(callbackSpy, EncryptionRunnerFactory::newFakeRunner);
-
- // Wrong init handshake message
- respondToContinueMessage();
- assertThat(semaphore.tryAcquire(100, TimeUnit.MILLISECONDS)).isTrue();
- verify(callbackSpy).onEstablishSecureChannelFailure(
- eq(SecureChannel.CHANNEL_ERROR_INVALID_HANDSHAKE)
- );
- }
-
- @Test
- public void testEncryptionHandshake_Association_wrongRespondToContinueMessage()
- throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- ChannelCallback callbackSpy = spy(new ChannelCallback(semaphore));
- setupAssociationSecureChannel(callbackSpy, EncryptionRunnerFactory::newFakeRunner);
-
- initHandshakeMessage();
-
- // Wrong respond to continue message
- initHandshakeMessage();
- assertThat(semaphore.tryAcquire(100, TimeUnit.MILLISECONDS)).isTrue();
- verify(callbackSpy).onEstablishSecureChannelFailure(
- eq(SecureChannel.CHANNEL_ERROR_INVALID_HANDSHAKE)
- );
- }
-
- private void setupAssociationSecureChannel(ChannelCallback callback,
- EncryptionRunnerProvider encryptionRunnerProvider) {
- mChannel = new AssociationSecureChannel(mStreamMock, mStorageMock,
- encryptionRunnerProvider.getEncryptionRunner());
- mChannel.registerCallback(callback);
- mChannel.setShowVerificationCodeListener(mShowVerificationCodeListenerMock);
- ArgumentCaptor<DeviceMessageStream.MessageReceivedListener> listenerCaptor =
- ArgumentCaptor.forClass(DeviceMessageStream.MessageReceivedListener.class);
- verify(mStreamMock).setMessageReceivedListener(listenerCaptor.capture());
- mMessageReceivedListener = listenerCaptor.getValue();
- }
-
- private void sendDeviceId() {
- DeviceMessage message = new DeviceMessage(
- /* recipient= */ null,
- /* isMessageEncrypted= */ true,
- ByteUtils.concatByteArrays(ByteUtils.uuidToBytes(CLIENT_DEVICE_ID), CLIENT_SECRET)
- );
- mMessageReceivedListener.onMessageReceived(message, OperationType.ENCRYPTION_HANDSHAKE);
- }
-
- private void initHandshakeMessage() {
- DeviceMessage message = new DeviceMessage(
- /* recipient= */ null,
- /* isMessageEncrypted= */ false,
- FakeEncryptionRunner.INIT.getBytes()
- );
- mMessageReceivedListener.onMessageReceived(message, OperationType.ENCRYPTION_HANDSHAKE);
- }
-
- private void respondToContinueMessage() {
- DeviceMessage message = new DeviceMessage(
- /* recipient= */ null,
- /* isMessageEncrypted= */ false,
- FakeEncryptionRunner.CLIENT_RESPONSE.getBytes()
- );
- mMessageReceivedListener.onMessageReceived(message, OperationType.ENCRYPTION_HANDSHAKE);
- }
-
- /**
- * Add the thread control logic into {@link SecureChannel.Callback} only for spy purpose.
- *
- * <p>The callback will release the semaphore which hold by one test when this callback
- * is called, telling the test that it can verify certain behaviors which will only occurred
- * after the callback is notified. This is needed mainly because of the callback is notified
- * in a different thread.
- */
- private static class ChannelCallback implements SecureChannel.Callback {
- private final Semaphore mSemaphore;
-
- ChannelCallback(Semaphore semaphore) {
- mSemaphore = semaphore;
- }
-
- @Override
- public void onSecureChannelEstablished() {
- mSemaphore.release();
- }
-
- @Override
- public void onEstablishSecureChannelFailure(int error) {
- mSemaphore.release();
- }
-
- @Override
- public void onMessageReceived(DeviceMessage deviceMessage) {
- mSemaphore.release();
- }
-
- @Override
- public void onMessageReceivedError(Exception exception) {
- mSemaphore.release();
- }
-
- @Override
- public void onDeviceIdReceived(String deviceId) {
- mSemaphore.release();
- }
- }
-
- interface EncryptionRunnerProvider {
- EncryptionRunner getEncryptionRunner();
- }
-}
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/DeviceMessageStreamTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/DeviceMessageStreamTest.java
deleted file mode 100644
index 661bece..0000000
--- a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/DeviceMessageStreamTest.java
+++ /dev/null
@@ -1,210 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.connection;
-
-import static com.android.car.connecteddevice.StreamProtos.DeviceMessageProto.Message;
-import static com.android.car.connecteddevice.StreamProtos.OperationProto.OperationType;
-import static com.android.car.connecteddevice.StreamProtos.PacketProto.Packet;
-import static com.android.car.connecteddevice.connection.DeviceMessageStream.MessageReceivedErrorListener;
-import static com.android.car.connecteddevice.connection.DeviceMessageStream.MessageReceivedListener;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-
-import androidx.annotation.NonNull;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import com.android.car.connecteddevice.util.ByteUtils;
-import com.android.car.protobuf.ByteString;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.ThreadLocalRandom;
-import java.util.concurrent.TimeUnit;
-
-@RunWith(AndroidJUnit4.class)
-public class DeviceMessageStreamTest {
-
- private static final int WRITE_SIZE = 500;
-
- private DeviceMessageStream mStream;
-
- @Before
- public void setup() {
- mStream = spy(new DeviceMessageStream(WRITE_SIZE) {
- @Override
- protected void send(byte[] data) { }
- });
- }
-
- @Test
- public void processPacket_notifiesWithEntireMessageForSinglePacketMessage()
- throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- MessageReceivedListener listener = createMessageReceivedListener(semaphore);
- mStream.setMessageReceivedListener(listener);
- byte[] data = ByteUtils.randomBytes(5);
- processMessage(data);
- assertThat(tryAcquire(semaphore)).isTrue();
- ArgumentCaptor<DeviceMessage> messageCaptor = ArgumentCaptor.forClass(DeviceMessage.class);
- verify(listener).onMessageReceived(messageCaptor.capture(), any());
- }
-
- @Test
- public void processPacket_notifiesWithEntireMessageForMultiPacketMessage()
- throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- MessageReceivedListener listener = createMessageReceivedListener(semaphore);
- mStream.setMessageReceivedListener(listener);
- byte[] data = ByteUtils.randomBytes(750);
- processMessage(data);
- assertThat(tryAcquire(semaphore)).isTrue();
- ArgumentCaptor<DeviceMessage> messageCaptor = ArgumentCaptor.forClass(DeviceMessage.class);
- verify(listener).onMessageReceived(messageCaptor.capture(), any());
- assertThat(Arrays.equals(data, messageCaptor.getValue().getMessage())).isTrue();
- }
-
- @Test
- public void processPacket_receivingMultipleMessagesInParallelParsesSuccessfully()
- throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- MessageReceivedListener listener = createMessageReceivedListener(semaphore);
- mStream.setMessageReceivedListener(listener);
- byte[] data = ByteUtils.randomBytes((int) (WRITE_SIZE * 1.5));
- List<Packet> packets1 = createPackets(data);
- List<Packet> packets2 = createPackets(data);
-
- for (int i = 0; i < packets1.size(); i++) {
- mStream.processPacket(packets1.get(i));
- if (i == packets1.size() - 1) {
- break;
- }
- mStream.processPacket(packets2.get(i));
- }
- assertThat(tryAcquire(semaphore)).isTrue();
- ArgumentCaptor<DeviceMessage> messageCaptor = ArgumentCaptor.forClass(DeviceMessage.class);
- verify(listener).onMessageReceived(messageCaptor.capture(), any());
- assertThat(Arrays.equals(data, messageCaptor.getValue().getMessage())).isTrue();
-
- semaphore = new Semaphore(0);
- listener = createMessageReceivedListener(semaphore);
- mStream.setMessageReceivedListener(listener);
- mStream.processPacket(packets2.get(packets2.size() - 1));
- verify(listener).onMessageReceived(messageCaptor.capture(), any());
- assertThat(Arrays.equals(data, messageCaptor.getValue().getMessage())).isTrue();
- }
-
- @Test
- public void processPacket_doesNotNotifyOfNewMessageIfNotAllPacketsReceived()
- throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- mStream.setMessageReceivedListener(createMessageReceivedListener(semaphore));
- mStream.setMessageReceivedErrorListener(createMessageReceivedErrorListener(semaphore));
- byte[] data = ByteUtils.randomBytes((int) (WRITE_SIZE * 1.5));
- List<Packet> packets = createPackets(data);
- for (int i = 0; i < packets.size() - 1; i++) {
- mStream.processPacket(packets.get(i));
- }
- assertThat(tryAcquire(semaphore)).isFalse();
- }
-
- @Test
- public void processPacket_ignoresDuplicatePacket() {
- Semaphore semaphore = new Semaphore(0);
- byte[] data = ByteUtils.randomBytes((int) (WRITE_SIZE * 2.5));
- MessageReceivedListener listener = createMessageReceivedListener(semaphore);
- mStream.setMessageReceivedListener(listener);
- ArgumentCaptor<DeviceMessage> messageCaptor = ArgumentCaptor.forClass(DeviceMessage.class);
- List<Packet> packets = createPackets(data);
- for (int i = 0; i < packets.size(); i++) {
- mStream.processPacket(packets.get(i));
- mStream.processPacket(packets.get(i)); // Process each packet twice.
- }
- verify(listener).onMessageReceived(messageCaptor.capture(), any());
- assertThat(Arrays.equals(data, messageCaptor.getValue().getMessage())).isTrue();
- }
-
- @Test
- public void processPacket_packetBeforeExpectedRangeNotifiesMessageError()
- throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- mStream.setMessageReceivedErrorListener(createMessageReceivedErrorListener(semaphore));
- List<Packet> packets = createPackets(ByteUtils.randomBytes((int) (WRITE_SIZE * 2.5)));
- mStream.processPacket(packets.get(0));
- mStream.processPacket(packets.get(1));
- mStream.processPacket(packets.get(0));
- assertThat(tryAcquire(semaphore)).isTrue();
- }
-
- @Test
- public void processPacket_packetAfterExpectedNotifiesMessageError()
- throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- mStream.setMessageReceivedErrorListener(createMessageReceivedErrorListener(semaphore));
- List<Packet> packets = createPackets(ByteUtils.randomBytes((int) (WRITE_SIZE * 1.5)));
- mStream.processPacket(packets.get(1));
- assertThat(tryAcquire(semaphore)).isTrue();
- }
-
- @NonNull
- private List<Packet> createPackets(byte[] data) {
- try {
- Message message = Message.newBuilder()
- .setPayload(ByteString.copyFrom(data))
- .setOperation(OperationType.CLIENT_MESSAGE)
- .build();
- return PacketFactory.makePackets(message.toByteArray(),
- ThreadLocalRandom.current().nextInt(), WRITE_SIZE);
- } catch (Exception e) {
- assertWithMessage("Uncaught exception while making packets.").fail();
- return new ArrayList<>();
- }
- }
-
- private void processMessage(byte[] data) {
- List<Packet> packets = createPackets(data);
- for (Packet packet : packets) {
- mStream.processPacket(packet);
- }
- }
-
- private boolean tryAcquire(@NonNull Semaphore semaphore) throws InterruptedException {
- return semaphore.tryAcquire(100, TimeUnit.MILLISECONDS);
- }
-
- @NonNull
- private MessageReceivedListener createMessageReceivedListener(@NonNull Semaphore semaphore) {
- return spy((deviceMessage, operationType) -> semaphore.release());
- }
-
- @NonNull
- private MessageReceivedErrorListener createMessageReceivedErrorListener(
- @NonNull Semaphore semaphore) {
- return exception -> semaphore.release();
- }
-}
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/OobAssociationSecureChannelTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/OobAssociationSecureChannelTest.java
deleted file mode 100644
index 3ad2a8d..0000000
--- a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/OobAssociationSecureChannelTest.java
+++ /dev/null
@@ -1,229 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.connection;
-
-import static com.android.car.connecteddevice.StreamProtos.OperationProto.OperationType;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mockitoSession;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.car.encryptionrunner.EncryptionRunnerFactory;
-import android.car.encryptionrunner.FakeEncryptionRunner;
-
-import com.android.car.connecteddevice.connection.ble.BleDeviceMessageStream;
-import com.android.car.connecteddevice.oob.OobConnectionManager;
-import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
-import com.android.car.connecteddevice.util.ByteUtils;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoSession;
-import org.mockito.quality.Strictness;
-
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.util.UUID;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
-
-import javax.crypto.BadPaddingException;
-import javax.crypto.IllegalBlockSizeException;
-
-public class OobAssociationSecureChannelTest {
- private static final UUID CLIENT_DEVICE_ID =
- UUID.fromString("a5645523-3280-410a-90c1-582a6c6f4969");
-
- private static final UUID SERVER_DEVICE_ID =
- UUID.fromString("a29f0c74-2014-4b14-ac02-be6ed15b545a");
-
- private static final byte[] CLIENT_SECRET = ByteUtils.randomBytes(32);
-
- @Mock
- private BleDeviceMessageStream mStreamMock;
-
- @Mock
- private ConnectedDeviceStorage mStorageMock;
-
- @Mock
- private OobConnectionManager mOobConnectionManagerMock;
-
- private OobAssociationSecureChannel mChannel;
-
- private BleDeviceMessageStream.MessageReceivedListener mMessageReceivedListener;
-
- private MockitoSession mMockitoSession;
-
- @Before
- public void setUp() {
- mMockitoSession = mockitoSession()
- .initMocks(this)
- .strictness(Strictness.WARN)
- .startMocking();
- when(mStorageMock.getUniqueId()).thenReturn(SERVER_DEVICE_ID);
- }
-
- @After
- public void tearDown() {
- if (mMockitoSession != null) {
- mMockitoSession.finishMocking();
- }
- }
-
- @Test
- public void testEncryptionHandshake_oobAssociation() throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- ChannelCallback
- callbackSpy = spy(new ChannelCallback(semaphore));
- setupOobAssociationSecureChannel(callbackSpy);
- ArgumentCaptor<String> deviceIdCaptor = ArgumentCaptor.forClass(String.class);
- ArgumentCaptor<DeviceMessage> messageCaptor =
- ArgumentCaptor.forClass(DeviceMessage.class);
-
- initHandshakeMessage();
- verify(mStreamMock).writeMessage(messageCaptor.capture(), any());
- byte[] response = messageCaptor.getValue().getMessage();
- assertThat(response).isEqualTo(FakeEncryptionRunner.INIT_RESPONSE.getBytes());
- reset(mStreamMock);
- respondToContinueMessage();
- verify(mStreamMock).writeMessage(messageCaptor.capture(), any());
- byte[] oobCodeResponse = messageCaptor.getValue().getMessage();
- assertThat(oobCodeResponse).isEqualTo(FakeEncryptionRunner.VERIFICATION_CODE.getBytes());
- respondToOobCode();
- sendDeviceId();
- assertThat(semaphore.tryAcquire(100, TimeUnit.MILLISECONDS)).isTrue();
- verify(callbackSpy).onDeviceIdReceived(deviceIdCaptor.capture());
- verify(mStreamMock, times(2)).writeMessage(messageCaptor.capture(), any());
- byte[] deviceIdMessage = messageCaptor.getValue().getMessage();
- assertThat(deviceIdMessage).isEqualTo(ByteUtils.uuidToBytes(SERVER_DEVICE_ID));
- assertThat(deviceIdCaptor.getValue()).isEqualTo(CLIENT_DEVICE_ID.toString());
- verify(mStorageMock).saveEncryptionKey(eq(CLIENT_DEVICE_ID.toString()), any());
- verify(mStorageMock).saveChallengeSecret(CLIENT_DEVICE_ID.toString(), CLIENT_SECRET);
-
- assertThat(semaphore.tryAcquire(100, TimeUnit.MILLISECONDS)).isTrue();
- verify(callbackSpy).onSecureChannelEstablished();
- }
-
- private void setupOobAssociationSecureChannel(ChannelCallback callback) {
- mChannel = new OobAssociationSecureChannel(mStreamMock, mStorageMock,
- mOobConnectionManagerMock, EncryptionRunnerFactory.newOobFakeRunner());
- mChannel.registerCallback(callback);
- ArgumentCaptor<BleDeviceMessageStream.MessageReceivedListener> listenerCaptor =
- ArgumentCaptor.forClass(BleDeviceMessageStream.MessageReceivedListener.class);
- verify(mStreamMock).setMessageReceivedListener(listenerCaptor.capture());
- mMessageReceivedListener = listenerCaptor.getValue();
- try {
- when(mOobConnectionManagerMock.encryptVerificationCode(any()))
- .thenReturn(FakeEncryptionRunner.VERIFICATION_CODE.getBytes());
- } catch (InvalidAlgorithmParameterException | BadPaddingException | InvalidKeyException
- | IllegalBlockSizeException e) {
- }
- try {
- when(mOobConnectionManagerMock.decryptVerificationCode(any()))
- .thenReturn(FakeEncryptionRunner.VERIFICATION_CODE.getBytes());
- } catch (InvalidAlgorithmParameterException | BadPaddingException | InvalidKeyException
- | IllegalBlockSizeException e) {
- }
- }
-
- private void sendDeviceId() {
- DeviceMessage message = new DeviceMessage(
- /* recipient= */ null,
- /* isMessageEncrypted= */ true,
- ByteUtils.concatByteArrays(ByteUtils.uuidToBytes(CLIENT_DEVICE_ID), CLIENT_SECRET)
- );
- mMessageReceivedListener.onMessageReceived(message, OperationType.ENCRYPTION_HANDSHAKE);
- }
-
- private void initHandshakeMessage() {
- DeviceMessage message = new DeviceMessage(
- /* recipient= */ null,
- /* isMessageEncrypted= */ false,
- FakeEncryptionRunner.INIT.getBytes()
- );
- mMessageReceivedListener.onMessageReceived(message, OperationType.ENCRYPTION_HANDSHAKE);
- }
-
- private void respondToContinueMessage() {
- DeviceMessage message = new DeviceMessage(
- /* recipient= */ null,
- /* isMessageEncrypted= */ false,
- FakeEncryptionRunner.CLIENT_RESPONSE.getBytes()
- );
- mMessageReceivedListener.onMessageReceived(message, OperationType.ENCRYPTION_HANDSHAKE);
- }
-
- private void respondToOobCode() {
- DeviceMessage message = new DeviceMessage(
- /* recipient= */ null,
- /* isMessageEncrypted= */ false,
- FakeEncryptionRunner.VERIFICATION_CODE.getBytes()
- );
- mMessageReceivedListener.onMessageReceived(message, OperationType.ENCRYPTION_HANDSHAKE);
- }
-
- /**
- * Add the thread control logic into {@link SecureChannel.Callback} only for spy purpose.
- *
- * <p>The callback will release the semaphore which hold by one test when this callback
- * is called, telling the test that it can verify certain behaviors which will only occurred
- * after the callback is notified. This is needed mainly because of the callback is notified
- * in a different thread.
- */
- private static class ChannelCallback implements SecureChannel.Callback {
- private final Semaphore mSemaphore;
-
- ChannelCallback(Semaphore semaphore) {
- mSemaphore = semaphore;
- }
-
- @Override
- public void onSecureChannelEstablished() {
- mSemaphore.release();
- }
-
- @Override
- public void onEstablishSecureChannelFailure(int error) {
- mSemaphore.release();
- }
-
- @Override
- public void onMessageReceived(DeviceMessage deviceMessage) {
- mSemaphore.release();
- }
-
- @Override
- public void onMessageReceivedError(Exception exception) {
- mSemaphore.release();
- }
-
- @Override
- public void onDeviceIdReceived(String deviceId) {
- mSemaphore.release();
- }
- }
-}
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/PacketFactoryTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/PacketFactoryTest.java
deleted file mode 100644
index d5fdc03..0000000
--- a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/PacketFactoryTest.java
+++ /dev/null
@@ -1,170 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.connection;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import com.android.car.connecteddevice.StreamProtos.PacketProto.Packet;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.io.ByteArrayOutputStream;
-import java.util.List;
-import java.util.Random;
-
-@RunWith(AndroidJUnit4.class)
-public class PacketFactoryTest {
- @Test
- public void testGetHeaderSize() {
- // 1 byte to encode the ID, 1 byte for the field number.
- int messageId = 1;
- int messageIdEncodingSize = 2;
-
- // 1 byte for the payload size, 1 byte for the field number.
- int payloadSize = 2;
- int payloadSizeEncodingSize = 2;
-
- // 1 byte for total packets, 1 byte for field number.
- int totalPackets = 5;
- int totalPacketsEncodingSize = 2;
-
- // Packet number if a fixed32, so 4 bytes + 1 byte for field number.
- int packetNumberEncodingSize = 5;
-
- int expectedHeaderSize = messageIdEncodingSize + payloadSizeEncodingSize
- + totalPacketsEncodingSize + packetNumberEncodingSize;
-
- assertThat(PacketFactory.getPacketHeaderSize(totalPackets, messageId, payloadSize))
- .isEqualTo(expectedHeaderSize);
- }
-
- @Test
- public void testGetTotalPackets_withVarintSize1_returnsCorrectPackets()
- throws PacketFactoryException {
- int messageId = 1;
- int maxSize = 49;
- int payloadSize = 100;
-
- // This leaves us 40 bytes to use for the payload and its encoding size. Assuming a varint
- // of size 1 means it takes 2 bytes to encode its value. This leaves 38 bytes for the
- // payload. ceil(payloadSize/38) gives the total packets.
- int expectedTotalPackets = 3;
-
- assertThat(PacketFactory.getTotalPacketNumber(messageId, payloadSize, maxSize))
- .isEqualTo(expectedTotalPackets);
- }
-
- @Test
- public void testGetTotalPackets_withVarintSize2_returnsCorrectPackets()
- throws PacketFactoryException {
- int messageId = 1;
- int maxSize = 49;
- int payloadSize = 6000;
-
- // This leaves us 40 bytes to use for the payload and its encoding size. Assuming a varint
- // of size 2 means it takes 3 bytes to encode its value. This leaves 37 bytes for the
- // payload. ceil(payloadSize/37) gives the total packets.
- int expectedTotalPackets = 163;
-
- assertThat(PacketFactory.getTotalPacketNumber(messageId, payloadSize, maxSize))
- .isEqualTo(expectedTotalPackets);
- }
-
- @Test
- public void testGetTotalPackets_withVarintSize3_returnsCorrectPackets()
- throws PacketFactoryException {
- int messageId = 1;
- int maxSize = 49;
- int payloadSize = 1000000;
-
- // This leaves us 40 bytes to use for the payload and its encoding size. Assuming a varint
- // of size 3 means it takes 4 bytes to encode its value. This leaves 36 bytes for the
- // payload. ceil(payloadSize/36) gives the total packets.
- int expectedTotalPackets = 27778;
-
- assertThat(PacketFactory.getTotalPacketNumber(messageId, payloadSize, maxSize))
- .isEqualTo(expectedTotalPackets);
- }
-
- @Test
- public void testGetTotalPackets_withVarintSize4_returnsCorrectPackets()
- throws PacketFactoryException {
- int messageId = 1;
- int maxSize = 49;
- int payloadSize = 178400320;
-
- // This leaves us 40 bytes to use for the payload and its encoding size. Assuming a varint
- // of size 4 means it takes 5 bytes to encode its value. This leaves 35 bytes for the
- // payload. ceil(payloadSize/35) gives the total packets.
- int expectedTotalPackets = 5097152;
-
- assertThat(PacketFactory.getTotalPacketNumber(messageId, payloadSize, maxSize))
- .isEqualTo(expectedTotalPackets);
- }
-
- @Test
- public void testMakePackets_correctlyChunksPayload() throws Exception {
- // Payload of size 100, but maxSize of 1000 to ensure it fits.
- byte[] payload = makePayload(/* length= */ 100);
- int maxSize = 1000;
-
- List<Packet> packets =
- PacketFactory.makePackets(payload, /* messageId= */ 1, maxSize);
-
- assertThat(packets).hasSize(1);
-
- ByteArrayOutputStream reconstructedPayload = new ByteArrayOutputStream();
-
- // Combine together all the payloads within the BlePackets.
- for (Packet packet : packets) {
- reconstructedPayload.write(packet.getPayload().toByteArray());
- }
-
- assertThat(reconstructedPayload.toByteArray()).isEqualTo(payload);
- }
-
- @Test
- public void testMakePackets_correctlyChunksSplitPayload() throws Exception {
- // Payload size of 10000 but max size of 50 to ensure the payload is split.
- byte[] payload = makePayload(/* length= */ 10000);
- int maxSize = 50;
-
- List<Packet> packets =
- PacketFactory.makePackets(payload, /* messageId= */ 1, maxSize);
-
- assertThat(packets.size()).isGreaterThan(1);
-
- ByteArrayOutputStream reconstructedPayload = new ByteArrayOutputStream();
-
- // Combine together all the payloads within the BlePackets.
- for (Packet packet : packets) {
- reconstructedPayload.write(packet.getPayload().toByteArray());
- }
-
- assertThat(reconstructedPayload.toByteArray()).isEqualTo(payload);
- }
-
- /** Creates a byte array of the given length, populated with random bytes. */
- private byte[] makePayload(int length) {
- byte[] payload = new byte[length];
- new Random().nextBytes(payload);
- return payload;
- }
-}
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/SecureChannelTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/SecureChannelTest.java
deleted file mode 100644
index ce56ebb..0000000
--- a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/SecureChannelTest.java
+++ /dev/null
@@ -1,210 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.connection;
-
-import static com.android.car.connecteddevice.StreamProtos.OperationProto.OperationType.CLIENT_MESSAGE;
-import static com.android.car.connecteddevice.StreamProtos.OperationProto.OperationType.ENCRYPTION_HANDSHAKE;
-import static com.android.car.connecteddevice.connection.SecureChannel.CHANNEL_ERROR_INVALID_HANDSHAKE;
-import static com.android.car.connecteddevice.connection.SecureChannel.Callback;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mockitoSession;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-import android.car.encryptionrunner.EncryptionRunnerFactory;
-import android.car.encryptionrunner.FakeEncryptionRunner;
-import android.car.encryptionrunner.HandshakeException;
-import android.car.encryptionrunner.Key;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import com.android.car.connecteddevice.connection.ble.BleDeviceMessageStream;
-import com.android.car.connecteddevice.util.ByteUtils;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoSession;
-import org.mockito.quality.Strictness;
-
-import java.security.NoSuchAlgorithmException;
-import java.security.SignatureException;
-import java.util.UUID;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
-
-@RunWith(AndroidJUnit4.class)
-public class SecureChannelTest {
-
- @Mock private BleDeviceMessageStream mMockStream;
-
- @Mock private Key mKey = spy(new Key() {
- @Override
- public byte[] asBytes() {
- return new byte[0];
- }
-
- @Override
- public byte[] encryptData(byte[] data) {
- return data;
- }
-
- @Override
- public byte[] decryptData(byte[] encryptedData) throws SignatureException {
- return encryptedData;
- }
-
- @Override
- public byte[] getUniqueSession() throws NoSuchAlgorithmException {
- return new byte[0];
- }
- });
-
- private MockitoSession mMockitoSession;
-
- private SecureChannel mSecureChannel;
-
- @Before
- public void setUp() throws SignatureException {
- mMockitoSession = mockitoSession()
- .initMocks(this)
- .strictness(Strictness.WARN)
- .startMocking();
-
- mSecureChannel = new SecureChannel(mMockStream,
- EncryptionRunnerFactory.newFakeRunner()) {
- @Override
- void processHandshake(byte[] message) { }
- };
- mSecureChannel.setEncryptionKey(mKey);
- }
-
- @After
- public void tearDown() {
- if (mMockitoSession != null) {
- mMockitoSession.finishMocking();
- }
- }
-
- @Test
- public void processMessage_doesNothingForUnencryptedMessage() throws SignatureException {
- byte[] payload = ByteUtils.randomBytes(10);
- DeviceMessage message = new DeviceMessage(UUID.randomUUID(), /* isEncrypted= */ false,
- payload);
- mSecureChannel.processMessage(message);
- assertThat(message.getMessage()).isEqualTo(payload);
- verify(mKey, times(0)).decryptData(any());
- }
-
- @Test
- public void processMessage_decryptsEncryptedMessage() throws SignatureException {
- byte[] payload = ByteUtils.randomBytes(10);
- DeviceMessage message = new DeviceMessage(UUID.randomUUID(), /* isEncrypted= */ true,
- payload);
- mSecureChannel.processMessage(message);
- verify(mKey).decryptData(any());
- }
-
- @Test
- public void processMessage_onMessageReceivedErrorForEncryptedMessageWithNoKey()
- throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- DeviceMessage message = new DeviceMessage(UUID.randomUUID(), /* isEncrypted= */ true,
- ByteUtils.randomBytes(10));
-
- mSecureChannel.setEncryptionKey(null);
- mSecureChannel.registerCallback(new Callback() {
- @Override
- public void onMessageReceivedError(Exception exception) {
- semaphore.release();
- }
- });
- mSecureChannel.processMessage(message);
- assertThat(tryAcquire(semaphore)).isTrue();
- assertThat(message.getMessage()).isNull();
- }
-
- @Test
- public void onMessageReceived_onEstablishSecureChannelFailureBadHandshakeMessage()
- throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- DeviceMessage message = new DeviceMessage(UUID.randomUUID(), /* isEncrypted= */ true,
- ByteUtils.randomBytes(10));
-
- mSecureChannel.setEncryptionKey(null);
- mSecureChannel.registerCallback(new Callback() {
- @Override
- public void onEstablishSecureChannelFailure(int error) {
- assertThat(error).isEqualTo(CHANNEL_ERROR_INVALID_HANDSHAKE);
- semaphore.release();
- }
- });
- mSecureChannel.onMessageReceived(message, ENCRYPTION_HANDSHAKE);
- assertThat(tryAcquire(semaphore)).isTrue();
- }
-
- @Test
- public void onMessageReceived_onMessageReceivedNotIssuedForNullMessage()
- throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- DeviceMessage message = new DeviceMessage(UUID.randomUUID(), /* isEncrypted= */ false,
- /* message= */ null);
-
- mSecureChannel.registerCallback(new Callback() {
- @Override
- public void onMessageReceived(DeviceMessage message) {
- semaphore.release();
- }
- });
- mSecureChannel.onMessageReceived(message, CLIENT_MESSAGE);
- assertThat(tryAcquire(semaphore)).isFalse();
- }
-
- @Test
- public void onMessageReceived_processHandshakeExceptionIssuesSecureChannelFailureCallback()
- throws InterruptedException {
- SecureChannel secureChannel = new SecureChannel(mMockStream,
- EncryptionRunnerFactory.newFakeRunner()) {
- @Override
- void processHandshake(byte[] message) throws HandshakeException {
- FakeEncryptionRunner.throwHandshakeException("test");
- }
- };
- Semaphore semaphore = new Semaphore(0);
- secureChannel.registerCallback(new Callback() {
- @Override
- public void onEstablishSecureChannelFailure(int error) {
- semaphore.release();
- }
- });
- DeviceMessage message = new DeviceMessage(UUID.randomUUID(), /* isEncrypted= */ true,
- /* message= */ ByteUtils.randomBytes(10));
-
- secureChannel.onMessageReceived(message, ENCRYPTION_HANDSHAKE);
- assertThat(tryAcquire(semaphore)).isTrue();
- }
-
- private boolean tryAcquire(Semaphore semaphore) throws InterruptedException {
- return semaphore.tryAcquire(100, TimeUnit.MILLISECONDS);
- }
-}
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/ble/CarBlePeripheralManagerTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/ble/CarBlePeripheralManagerTest.java
deleted file mode 100644
index b4a229e..0000000
--- a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/ble/CarBlePeripheralManagerTest.java
+++ /dev/null
@@ -1,244 +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.connecteddevice.connection.ble;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mockitoSession;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.le.AdvertiseCallback;
-import android.bluetooth.le.AdvertiseData;
-import android.bluetooth.le.AdvertiseSettings;
-import android.os.ParcelUuid;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import com.android.car.connecteddevice.AssociationCallback;
-import com.android.car.connecteddevice.connection.AssociationSecureChannel;
-import com.android.car.connecteddevice.connection.SecureChannel;
-import com.android.car.connecteddevice.model.AssociatedDevice;
-import com.android.car.connecteddevice.oob.OobConnectionManager;
-import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
-import com.android.car.connecteddevice.util.ByteUtils;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoSession;
-import org.mockito.quality.Strictness;
-
-import java.time.Duration;
-import java.util.UUID;
-
-@RunWith(AndroidJUnit4.class)
-public class CarBlePeripheralManagerTest {
- private static final UUID ASSOCIATION_SERVICE_UUID = UUID.randomUUID();
- private static final UUID RECONNECT_SERVICE_UUID = UUID.randomUUID();
- private static final UUID RECONNECT_DATA_UUID = UUID.randomUUID();
- private static final UUID WRITE_UUID = UUID.randomUUID();
- private static final UUID READ_UUID = UUID.randomUUID();
- private static final int DEVICE_NAME_LENGTH_LIMIT = 8;
- private static final String TEST_REMOTE_DEVICE_ADDRESS = "00:11:22:33:AA:BB";
- private static final UUID TEST_REMOTE_DEVICE_ID = UUID.randomUUID();
- private static final String TEST_VERIFICATION_CODE = "000000";
- private static final String TEST_ENCRYPTED_VERIFICATION_CODE = "12345";
- private static final Duration RECONNECT_ADVERTISEMENT_DURATION = Duration.ofSeconds(2);
- private static final int DEFAULT_MTU_SIZE = 23;
-
- @Mock
- private BlePeripheralManager mMockPeripheralManager;
- @Mock
- private ConnectedDeviceStorage mMockStorage;
- @Mock
- private OobConnectionManager mMockOobConnectionManager;
- @Mock
- private AssociationCallback mAssociationCallback;
-
- private CarBlePeripheralManager mCarBlePeripheralManager;
-
- private MockitoSession mMockitoSession;
-
- @Before
- public void setUp() throws Exception {
- mMockitoSession = mockitoSession()
- .initMocks(this)
- .strictness(Strictness.WARN)
- .startMocking();
- mCarBlePeripheralManager = new CarBlePeripheralManager(mMockPeripheralManager, mMockStorage,
- ASSOCIATION_SERVICE_UUID, RECONNECT_SERVICE_UUID,
- RECONNECT_DATA_UUID, WRITE_UUID, READ_UUID, RECONNECT_ADVERTISEMENT_DURATION,
- DEFAULT_MTU_SIZE);
-
- when(mMockOobConnectionManager.encryptVerificationCode(
- TEST_VERIFICATION_CODE.getBytes())).thenReturn(
- TEST_ENCRYPTED_VERIFICATION_CODE.getBytes());
- when(mMockOobConnectionManager.decryptVerificationCode(
- TEST_ENCRYPTED_VERIFICATION_CODE.getBytes())).thenReturn(
- TEST_VERIFICATION_CODE.getBytes());
- mCarBlePeripheralManager.start();
- }
-
- @After
- public void tearDown() {
- if (mCarBlePeripheralManager != null) {
- mCarBlePeripheralManager.stop();
- }
- if (mMockitoSession != null) {
- mMockitoSession.finishMocking();
- }
- }
-
- @Test
- public void testStartAssociationAdvertisingSuccess() {
- String testDeviceName = getNameForAssociation();
- startAssociation(mAssociationCallback, testDeviceName);
- ArgumentCaptor<AdvertiseData> advertisementDataCaptor =
- ArgumentCaptor.forClass(AdvertiseData.class);
- ArgumentCaptor<AdvertiseData> scanResponseDataCaptor =
- ArgumentCaptor.forClass(AdvertiseData.class);
- verify(mMockPeripheralManager).startAdvertising(any(), advertisementDataCaptor.capture(),
- scanResponseDataCaptor.capture(), any());
- AdvertiseData advertisementData = advertisementDataCaptor.getValue();
- ParcelUuid serviceUuid = new ParcelUuid(ASSOCIATION_SERVICE_UUID);
- assertThat(advertisementData.getServiceUuids()).contains(serviceUuid);
- AdvertiseData scanResponseData = scanResponseDataCaptor.getValue();
- assertThat(scanResponseData.getIncludeDeviceName()).isFalse();
- ParcelUuid dataUuid = new ParcelUuid(RECONNECT_DATA_UUID);
- assertThat(scanResponseData.getServiceData().get(dataUuid)).isEqualTo(
- testDeviceName.getBytes());
- }
-
- @Test
- public void testStartAssociationAdvertisingFailure() {
- startAssociation(mAssociationCallback, getNameForAssociation());
- ArgumentCaptor<AdvertiseCallback> callbackCaptor =
- ArgumentCaptor.forClass(AdvertiseCallback.class);
- verify(mMockPeripheralManager).startAdvertising(any(), any(), any(),
- callbackCaptor.capture());
- AdvertiseCallback advertiseCallback = callbackCaptor.getValue();
- int testErrorCode = 2;
- advertiseCallback.onStartFailure(testErrorCode);
- verify(mAssociationCallback).onAssociationStartFailure();
- }
-
- @Test
- public void testNotifyAssociationSuccess() {
- String testDeviceName = getNameForAssociation();
- startAssociation(mAssociationCallback, testDeviceName);
- ArgumentCaptor<AdvertiseCallback> callbackCaptor =
- ArgumentCaptor.forClass(AdvertiseCallback.class);
- verify(mMockPeripheralManager).startAdvertising(any(), any(), any(),
- callbackCaptor.capture());
- AdvertiseCallback advertiseCallback = callbackCaptor.getValue();
- AdvertiseSettings settings = new AdvertiseSettings.Builder().build();
- advertiseCallback.onStartSuccess(settings);
- verify(mAssociationCallback).onAssociationStartSuccess(eq(testDeviceName));
- }
-
- @Test
- public void testShowVerificationCode() {
- AssociationSecureChannel channel = getChannelForAssociation(mAssociationCallback);
- channel.getShowVerificationCodeListener().showVerificationCode(TEST_VERIFICATION_CODE);
- verify(mAssociationCallback).onVerificationCodeAvailable(eq(TEST_VERIFICATION_CODE));
- }
-
- @Test
- public void testAssociationSuccess() {
- SecureChannel channel = getChannelForAssociation(mAssociationCallback);
- SecureChannel.Callback channelCallback = channel.getCallback();
- assertThat(channelCallback).isNotNull();
- channelCallback.onDeviceIdReceived(TEST_REMOTE_DEVICE_ID.toString());
- channelCallback.onSecureChannelEstablished();
- ArgumentCaptor<AssociatedDevice> deviceCaptor =
- ArgumentCaptor.forClass(AssociatedDevice.class);
- verify(mMockStorage).addAssociatedDeviceForActiveUser(deviceCaptor.capture());
- AssociatedDevice device = deviceCaptor.getValue();
- assertThat(device.getDeviceId()).isEqualTo(TEST_REMOTE_DEVICE_ID.toString());
- verify(mAssociationCallback).onAssociationCompleted(eq(TEST_REMOTE_DEVICE_ID.toString()));
- }
-
- @Test
- public void testAssociationFailure_channelError() {
- SecureChannel channel = getChannelForAssociation(mAssociationCallback);
- SecureChannel.Callback channelCallback = channel.getCallback();
- int testErrorCode = 1;
- assertThat(channelCallback).isNotNull();
- channelCallback.onDeviceIdReceived(TEST_REMOTE_DEVICE_ID.toString());
- channelCallback.onEstablishSecureChannelFailure(testErrorCode);
- verify(mAssociationCallback).onAssociationError(eq(testErrorCode));
- }
-
- @Test
- public void connectToDevice_stopsAdvertisingAfterTimeout() {
- when(mMockStorage.hashWithChallengeSecret(any(), any()))
- .thenReturn(ByteUtils.randomBytes(32));
- mCarBlePeripheralManager.connectToDevice(UUID.randomUUID());
- ArgumentCaptor<AdvertiseCallback> callbackCaptor =
- ArgumentCaptor.forClass(AdvertiseCallback.class);
- verify(mMockPeripheralManager).startAdvertising(any(), any(), any(),
- callbackCaptor.capture());
- callbackCaptor.getValue().onStartSuccess(null);
- verify(mMockPeripheralManager,
- timeout(RECONNECT_ADVERTISEMENT_DURATION.plusSeconds(1).toMillis()))
- .stopAdvertising(any(AdvertiseCallback.class));
- }
-
- @Test
- public void disconnectDevice_stopsAdvertisingForPendingReconnect() {
- when(mMockStorage.hashWithChallengeSecret(any(), any()))
- .thenReturn(ByteUtils.randomBytes(32));
- UUID deviceId = UUID.randomUUID();
- mCarBlePeripheralManager.connectToDevice(deviceId);
- reset(mMockPeripheralManager);
- mCarBlePeripheralManager.disconnectDevice(deviceId.toString());
- verify(mMockPeripheralManager).cleanup();
- }
-
- private BlePeripheralManager.Callback startAssociation(AssociationCallback callback,
- String deviceName) {
- ArgumentCaptor<BlePeripheralManager.Callback> callbackCaptor =
- ArgumentCaptor.forClass(BlePeripheralManager.Callback.class);
- mCarBlePeripheralManager.startAssociation(deviceName, callback);
- verify(mMockPeripheralManager, timeout(3000)).registerCallback(callbackCaptor.capture());
- return callbackCaptor.getValue();
- }
-
- private AssociationSecureChannel getChannelForAssociation(AssociationCallback callback) {
- BlePeripheralManager.Callback bleManagerCallback = startAssociation(callback,
- getNameForAssociation());
- BluetoothDevice bluetoothDevice = BluetoothAdapter.getDefaultAdapter()
- .getRemoteDevice(TEST_REMOTE_DEVICE_ADDRESS);
- bleManagerCallback.onRemoteDeviceConnected(bluetoothDevice);
- return (AssociationSecureChannel) mCarBlePeripheralManager.getConnectedDeviceChannel();
- }
-
- private String getNameForAssociation() {
- return ByteUtils.generateRandomNumberString(DEVICE_NAME_LENGTH_LIMIT);
-
- }
-}
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/spp/CarSppManagerTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/spp/CarSppManagerTest.java
deleted file mode 100644
index ef0ee85..0000000
--- a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/spp/CarSppManagerTest.java
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.connection.spp;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mockitoSession;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-
-import androidx.annotation.NonNull;
-
-import com.android.car.connecteddevice.AssociationCallback;
-import com.android.car.connecteddevice.connection.AssociationSecureChannel;
-import com.android.car.connecteddevice.connection.SecureChannel;
-import com.android.car.connecteddevice.model.AssociatedDevice;
-import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoSession;
-import org.mockito.quality.Strictness;
-
-import java.util.UUID;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
-
-public class CarSppManagerTest {
- private static final String TEST_REMOTE_DEVICE_ADDRESS = "00:11:22:33:AA:BB";
- private static final UUID TEST_REMOTE_DEVICE_ID = UUID.randomUUID();
- private static final UUID TEST_SERVICE_UUID = UUID.randomUUID();
- private static final String TEST_VERIFICATION_CODE = "000000";
- private static final int MAX_PACKET_SIZE = 700;
- @Mock
- private SppManager mMockSppManager;
- @Mock
- private ConnectedDeviceStorage mMockStorage;
-
- private CarSppManager mCarSppManager;
-
- private MockitoSession mMockitoSession;
-
- @Before
- public void setUp() throws Exception {
- mMockitoSession = mockitoSession()
- .initMocks(this)
- .strictness(Strictness.WARN)
- .startMocking();
- mCarSppManager = new CarSppManager(mMockSppManager, mMockStorage, TEST_SERVICE_UUID,
- MAX_PACKET_SIZE);
- }
-
- @After
- public void tearDown() {
- if (mCarSppManager != null) {
- mCarSppManager.stop();
- }
- if (mMockitoSession != null) {
- mMockitoSession.finishMocking();
- }
- }
-
- @Test
- public void testStartAssociationSuccess() throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- AssociationCallback callback = createAssociationCallback(semaphore);
- when(mMockSppManager.startListening(TEST_SERVICE_UUID)).thenReturn(true);
-
- mCarSppManager.startAssociation(null, callback);
-
- verify(mMockSppManager).startListening(TEST_SERVICE_UUID);
- assertThat(tryAcquire(semaphore)).isTrue();
- verify(callback).onAssociationStartSuccess(eq(null));
- }
-
- @Test
- public void testStartAssociationFailure() throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- AssociationCallback callback = createAssociationCallback(semaphore);
- when(mMockSppManager.startListening(TEST_SERVICE_UUID)).thenReturn(false);
-
- mCarSppManager.startAssociation(null, callback);
-
- assertThat(tryAcquire(semaphore)).isTrue();
- verify(callback).onAssociationStartFailure();
- }
-
- @Test
- public void testShowVerificationCode() throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- AssociationCallback callback = createAssociationCallback(semaphore);
- AssociationSecureChannel channel = getChannelForAssociation(callback);
-
- channel.getShowVerificationCodeListener().showVerificationCode(TEST_VERIFICATION_CODE);
-
- assertThat(tryAcquire(semaphore)).isTrue();
- verify(callback).onVerificationCodeAvailable(eq(TEST_VERIFICATION_CODE));
- }
-
- @Test
- public void testAssociationSuccess() throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- AssociationCallback callback = createAssociationCallback(semaphore);
- SecureChannel channel = getChannelForAssociation(callback);
- SecureChannel.Callback channelCallback = channel.getCallback();
-
- assertThat(channelCallback).isNotNull();
-
- channelCallback.onDeviceIdReceived(TEST_REMOTE_DEVICE_ID.toString());
- channelCallback.onSecureChannelEstablished();
- ArgumentCaptor<AssociatedDevice> deviceCaptor =
- ArgumentCaptor.forClass(AssociatedDevice.class);
- verify(mMockStorage).addAssociatedDeviceForActiveUser(deviceCaptor.capture());
- AssociatedDevice device = deviceCaptor.getValue();
-
- assertThat(device.getDeviceId()).isEqualTo(TEST_REMOTE_DEVICE_ID.toString());
- assertThat(tryAcquire(semaphore)).isTrue();
- verify(callback).onAssociationCompleted(eq(TEST_REMOTE_DEVICE_ID.toString()));
- }
-
- private boolean tryAcquire(Semaphore semaphore) throws InterruptedException {
- return semaphore.tryAcquire(100, TimeUnit.MILLISECONDS);
- }
-
- private AssociationSecureChannel getChannelForAssociation(AssociationCallback callback) {
- ArgumentCaptor<SppManager.ConnectionCallback> callbackCaptor =
- ArgumentCaptor.forClass(SppManager.ConnectionCallback.class);
- mCarSppManager.startAssociation(null, callback);
- verify(mMockSppManager).registerCallback(callbackCaptor.capture(), any());
- BluetoothDevice bluetoothDevice = BluetoothAdapter.getDefaultAdapter()
- .getRemoteDevice(TEST_REMOTE_DEVICE_ADDRESS);
- callbackCaptor.getValue().onRemoteDeviceConnected(bluetoothDevice);
- return (AssociationSecureChannel) mCarSppManager.getConnectedDeviceChannel();
- }
-
- @NonNull
- private AssociationCallback createAssociationCallback(@NonNull final Semaphore semaphore) {
- return spy(new AssociationCallback() {
- @Override
- public void onAssociationStartSuccess(String deviceName) {
- semaphore.release();
- }
-
- @Override
- public void onAssociationStartFailure() {
- semaphore.release();
- }
-
- @Override
- public void onAssociationError(int error) {
- semaphore.release();
- }
-
- @Override
- public void onVerificationCodeAvailable(String code) {
- semaphore.release();
- }
-
- @Override
- public void onAssociationCompleted(String deviceId) {
- semaphore.release();
- }
- });
- }
-}
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/spp/ConnectedTaskTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/spp/ConnectedTaskTest.java
deleted file mode 100644
index 7ae8192..0000000
--- a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/spp/ConnectedTaskTest.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.connection.spp;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
-
-@RunWith(AndroidJUnit4.class)
-public class ConnectedTaskTest {
-
- private final byte[] mTestData = "testData".getBytes();
- private ConnectedTask mConnectedTask;
- private InputStream mInputStream = new ByteArrayInputStream(mTestData);
- private OutputStream mOutputStream = new ByteArrayOutputStream();
- private ConnectedTask.Callback mCallback;
- private Executor mExecutor = Executors.newSingleThreadExecutor();
- private Semaphore mSemaphore = new Semaphore(0);
-
-
- @Before
- public void setUp() {
- mCallback = spy(new ConnectedTask.Callback() {
- @Override
- public void onMessageReceived(byte[] message) {
- mSemaphore.release();
- }
-
- @Override
- public void onDisconnected() {
-
- }
- });
- mConnectedTask = new ConnectedTask(mInputStream, mOutputStream, null, mCallback);
- }
-
- @Test
- public void testTaskRun_InformCallback() throws InterruptedException {
- mExecutor.execute(mConnectedTask);
- assertThat(tryAcquire(mSemaphore)).isTrue();
- verify(mCallback).onMessageReceived(mTestData);
- }
-
- @Test
- public void testWrite_WriteToOutputStream() {
- mConnectedTask.write(mTestData);
- assertThat(mOutputStream.toString()).isEqualTo(new String(mTestData));
- }
-
- private boolean tryAcquire(Semaphore semaphore) throws InterruptedException {
- return semaphore.tryAcquire(100, TimeUnit.MILLISECONDS);
- }
-}
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/spp/SppDeviceMessageStreamTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/spp/SppDeviceMessageStreamTest.java
deleted file mode 100644
index a126d04..0000000
--- a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/spp/SppDeviceMessageStreamTest.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.connection.spp;
-
-import static org.mockito.Mockito.mockitoSession;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import com.android.car.connecteddevice.util.ByteUtils;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoSession;
-import org.mockito.quality.Strictness;
-
-@RunWith(AndroidJUnit4.class)
-public class SppDeviceMessageStreamTest {
- private static final int MAX_WRITE_SIZE = 700;
-
- @Mock
- private SppManager mMockSppManager;
- private BluetoothDevice mBluetoothDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(
- "00:11:22:33:44:55");
- private MockitoSession mMockingSession;
- private SppDeviceMessageStream mSppDeviceMessageStream;
-
- @Before
- public void setUp() {
- mMockingSession = mockitoSession()
- .initMocks(this)
- .strictness(Strictness.WARN)
- .startMocking();
- mSppDeviceMessageStream = spy(
- new SppDeviceMessageStream(mMockSppManager, mBluetoothDevice, MAX_WRITE_SIZE));
- }
-
- @After
- public void tearDown() {
- if (mMockingSession != null) {
- mMockingSession.finishMocking();
- }
- }
-
- @Test
- public void send_callsWriteAndSendCompleted() {
- byte[] data = ByteUtils.randomBytes(10);
- mSppDeviceMessageStream.send(data);
- verify(mMockSppManager).write(data);
- verify(mSppDeviceMessageStream).sendCompleted();
- }
-}
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/spp/SppManagerTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/spp/SppManagerTest.java
deleted file mode 100644
index a99fc6b..0000000
--- a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/spp/SppManagerTest.java
+++ /dev/null
@@ -1,144 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.connection.spp;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mockitoSession;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-
-import android.bluetooth.BluetoothDevice;
-
-import androidx.annotation.NonNull;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoSession;
-import org.mockito.quality.Strictness;
-
-import java.io.IOException;
-import java.util.UUID;
-import java.util.concurrent.Executor;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
-
-@RunWith(AndroidJUnit4.class)
-public class SppManagerTest {
- private static final UUID TEST_SERVICE_UUID = UUID.randomUUID();
- private final boolean mIsSecureRfcommChannel = true;
- private final byte[] mTestData = "testData".getBytes();
- private SppManager mSppManager;
- private Executor mCallbackExecutor = Executors.newSingleThreadExecutor();
- private byte[] mCompletedMessage = SppPayloadStream.wrapWithArrayLength(mTestData);
- @Mock
- private ConnectedTask mMockConnectedTask;
-
- @Mock
- private ExecutorService mMockExecutorService;
- private MockitoSession mMockitoSession;
-
-
- @Before
- public void setUp() throws IOException {
- mSppManager = new SppManager(mIsSecureRfcommChannel);
- mMockitoSession = mockitoSession()
- .initMocks(this)
- .strictness(Strictness.WARN)
- .startMocking();
- }
-
- @After
- public void tearDown() {
- if (mMockitoSession != null) {
- mMockitoSession.finishMocking();
- }
- }
-
- @Test
- public void testStartListen_StartAcceptTask() {
- mSppManager.mConnectionExecutor = mMockExecutorService;
- mSppManager.startListening(TEST_SERVICE_UUID);
- assertThat(mSppManager.mAcceptTask).isNotNull();
- verify(mMockExecutorService).execute(mSppManager.mAcceptTask);
- }
-
- @Test
- public void testWrite_CallConnectedTaskToWrite() {
- mSppManager.mConnectedTask = mMockConnectedTask;
- mSppManager.mState = SppManager.ConnectionState.CONNECTED;
- mSppManager.write(mTestData);
- verify(mMockConnectedTask).write(SppPayloadStream.wrapWithArrayLength(mTestData));
- }
-
- @Test
- public void testConnectedTaskCallback_onMessageReceived_CallOnMessageReceivedListener()
- throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- SppManager.OnMessageReceivedListener listener = createOnMessageReceivedListener(semaphore);
- mSppManager.addOnMessageReceivedListener(listener, mCallbackExecutor);
- mSppManager.mConnectedTaskCallback.onMessageReceived(mCompletedMessage);
- assertThat(tryAcquire(semaphore)).isTrue();
- verify(listener).onMessageReceived(any(), eq(mTestData));
- }
-
- @Test
- public void testConnectedTaskCallback_onDisconnected_CallOnRemoteDeviceDisconnected()
- throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- SppManager.ConnectionCallback callback = createConnectionCallback(semaphore);
- mSppManager.registerCallback(callback, mCallbackExecutor);
- mSppManager.mConnectedTaskCallback.onDisconnected();
- assertThat(tryAcquire(semaphore)).isTrue();
- verify(callback).onRemoteDeviceDisconnected(any());
- }
-
- @NonNull
- private SppManager.ConnectionCallback createConnectionCallback(
- @NonNull final Semaphore semaphore) {
- return spy(new SppManager.ConnectionCallback() {
-
- @Override
- public void onRemoteDeviceConnected(BluetoothDevice device) {
- semaphore.release();
- }
-
- @Override
- public void onRemoteDeviceDisconnected(BluetoothDevice device) {
- semaphore.release();
- }
- });
- }
-
- @NonNull
- private SppManager.OnMessageReceivedListener createOnMessageReceivedListener(
- @NonNull final Semaphore semaphore) {
- return spy((device, value) -> semaphore.release());
- }
-
- private boolean tryAcquire(Semaphore semaphore) throws InterruptedException {
- return semaphore.tryAcquire(100, TimeUnit.MILLISECONDS);
- }
-}
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/spp/SppPayloadStreamTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/spp/SppPayloadStreamTest.java
deleted file mode 100644
index 9f0b42b..0000000
--- a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/spp/SppPayloadStreamTest.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.connection.spp;
-
-import static org.mockito.Mockito.mockitoSession;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoSession;
-import org.mockito.quality.Strictness;
-
-import java.io.IOException;
-import java.util.Arrays;
-
-@RunWith(AndroidJUnit4.class)
-public class SppPayloadStreamTest {
- private SppPayloadStream mSppPayloadStream;
- private MockitoSession mMockitoSession;
- private final byte[] mTestData = "testData".getBytes();
- private byte[] mCompletedMessage = SppPayloadStream.wrapWithArrayLength(mTestData);
- ;
- private byte[] mCompletedMessageSplit1;
- private byte[] mCompletedMessageSplit2;
- @Mock
- private SppPayloadStream.OnMessageCompletedListener mMockListener;
-
-
- @Before
- public void setUp() {
- mMockitoSession = mockitoSession()
- .initMocks(this)
- .strictness(Strictness.WARN)
- .startMocking();
- mSppPayloadStream = new SppPayloadStream();
- mSppPayloadStream.setMessageCompletedListener(mMockListener);
- int length = mCompletedMessage.length;
- mCompletedMessageSplit1 = Arrays.copyOfRange(mCompletedMessage, 0, (length + 1) / 2);
- mCompletedMessageSplit2 = Arrays.copyOfRange(mCompletedMessage, (length + 1) / 2, length);
- }
-
- @After
- public void tearDown() {
- if (mMockitoSession != null) {
- mMockitoSession.finishMocking();
- }
- }
-
- @Test
- public void testWriteCompletedMessage_InformListener() throws IOException {
- mSppPayloadStream.write(mCompletedMessage);
- verify(mMockListener).onMessageCompleted(mTestData);
- }
-
- @Test
- public void testWriteIncompleteMessage_DoNotInformListener() throws IOException {
- mSppPayloadStream.write(mCompletedMessageSplit1);
- verify(mMockListener, never()).onMessageCompleted(mTestData);
- }
-
- @Test
- public void testWriteTwoMessage_InformListenerCompletedMessage() throws IOException {
- mSppPayloadStream.write(mCompletedMessageSplit1);
- mSppPayloadStream.write(mCompletedMessageSplit2);
-
- verify(mMockListener).onMessageCompleted(mTestData);
- }
-}
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/oob/BluetoothRfcommChannelTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/oob/BluetoothRfcommChannelTest.java
deleted file mode 100644
index 9628afa..0000000
--- a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/oob/BluetoothRfcommChannelTest.java
+++ /dev/null
@@ -1,185 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.oob;
-
-
-import static com.android.car.connecteddevice.model.OobEligibleDevice.OOB_TYPE_BLUETOOTH;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.mockitoSession;
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothSocket;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import com.android.car.connecteddevice.model.OobEligibleDevice;
-import com.android.car.connecteddevice.util.ByteUtils;
-
-import com.google.common.primitives.Bytes;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoSession;
-import org.mockito.quality.Strictness;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.util.UUID;
-
-@RunWith(AndroidJUnit4.class)
-public class BluetoothRfcommChannelTest {
- private BluetoothRfcommChannel mBluetoothRfcommChannel;
- private OobEligibleDevice mOobEligibleDevice;
- @Mock
- private OobChannel.Callback mMockCallback;
- @Mock
- private BluetoothAdapter mMockBluetoothAdapter;
- @Mock
- private BluetoothDevice mMockBluetoothDevice;
- @Mock
- private BluetoothSocket mMockBluetoothSocket;
- @Mock
- private OutputStream mMockOutputStream;
-
- private MockitoSession mMockingSession;
-
- @Before
- public void setUp() {
- mMockingSession = mockitoSession()
- .initMocks(this)
- .strictness(Strictness.WARN)
- .startMocking();
-
-
- mBluetoothRfcommChannel = new BluetoothRfcommChannel();
-
- mOobEligibleDevice = new OobEligibleDevice("00:11:22:33:44:55", OOB_TYPE_BLUETOOTH);
-
- when(mMockBluetoothAdapter.getRemoteDevice(
- mOobEligibleDevice.getDeviceAddress())).thenReturn(mMockBluetoothDevice);
- }
-
- @After
- public void tearDown() {
- if (mMockingSession != null) {
- mMockingSession.finishMocking();
- }
- }
-
- @Test
- public void completeOobExchange_success() throws Exception {
- when(mMockBluetoothSocket.getOutputStream()).thenReturn(mMockOutputStream);
- when(mMockBluetoothDevice.createRfcommSocketToServiceRecord(any(UUID.class))).thenReturn(
- mMockBluetoothSocket);
- mBluetoothRfcommChannel.completeOobDataExchange(mOobEligibleDevice, mMockCallback,
- mMockBluetoothAdapter);
-
- verify(mMockCallback, timeout(1000)).onOobExchangeSuccess();
- OobConnectionManager oobConnectionManager = new OobConnectionManager();
- oobConnectionManager.startOobExchange(mBluetoothRfcommChannel);
-
- ArgumentCaptor<byte[]> oobDataCaptor = ArgumentCaptor.forClass(byte[].class);
- verify(mMockOutputStream).write(oobDataCaptor.capture());
- byte[] oobData = oobDataCaptor.getValue();
-
- assertThat(oobData).isEqualTo(Bytes.concat(oobConnectionManager.mDecryptionIv,
- oobConnectionManager.mEncryptionIv,
- oobConnectionManager.mEncryptionKey.getEncoded()));
- }
-
- @Test
- public void completeOobExchange_ioExceptionCausesRetry() throws Exception {
- doThrow(IOException.class).doAnswer(invocation -> null)
- .when(mMockBluetoothSocket).connect();
- when(mMockBluetoothDevice.createRfcommSocketToServiceRecord(any(UUID.class))).thenReturn(
- mMockBluetoothSocket);
- mBluetoothRfcommChannel.completeOobDataExchange(mOobEligibleDevice, mMockCallback,
- mMockBluetoothAdapter);
- verify(mMockBluetoothSocket, timeout(3000).times(2)).connect();
- }
-
- @Test
- public void completeOobExchange_createRfcommSocketFails_callOnFailed() throws Exception {
- when(mMockBluetoothDevice.createRfcommSocketToServiceRecord(any(UUID.class))).thenThrow(
- IOException.class);
-
- mBluetoothRfcommChannel.completeOobDataExchange(mOobEligibleDevice, mMockCallback,
- mMockBluetoothAdapter);
- verify(mMockCallback).onOobExchangeFailure();
- }
-
- @Test
- public void sendOobData_nullBluetoothDevice_callOnFailed() {
- mBluetoothRfcommChannel.mCallback = mMockCallback;
-
- mBluetoothRfcommChannel.sendOobData("someData".getBytes());
- verify(mMockCallback).onOobExchangeFailure();
- }
-
- @Test
- public void sendOobData_writeFails_callOnFailed() throws Exception {
- byte[] testMessage = "testMessage".getBytes();
- completeOobExchange_success();
- doThrow(IOException.class).when(mMockOutputStream).write(testMessage);
-
- mBluetoothRfcommChannel.sendOobData(testMessage);
- verify(mMockCallback).onOobExchangeFailure();
- }
-
- @Test
- public void interrupt_closesSocket() throws Exception {
- when(mMockBluetoothSocket.getOutputStream()).thenReturn(mMockOutputStream);
- when(mMockBluetoothDevice.createRfcommSocketToServiceRecord(any(UUID.class))).thenReturn(
- mMockBluetoothSocket);
- mBluetoothRfcommChannel.completeOobDataExchange(mOobEligibleDevice, mMockCallback,
- mMockBluetoothAdapter);
- mBluetoothRfcommChannel.interrupt();
- mBluetoothRfcommChannel.sendOobData(ByteUtils.randomBytes(10));
- verify(mMockOutputStream, times(0)).write(any());
- verify(mMockOutputStream).flush();
- verify(mMockOutputStream).close();
- }
-
- @Test
- public void interrupt_preventsCallbacks() throws Exception {
- when(mMockBluetoothSocket.getOutputStream()).thenReturn(mMockOutputStream);
- when(mMockBluetoothDevice.createRfcommSocketToServiceRecord(any(UUID.class))).thenReturn(
- mMockBluetoothSocket);
- doAnswer(invocation -> {
- mBluetoothRfcommChannel.interrupt();
- return invocation.callRealMethod();
- }).when(mMockBluetoothSocket).connect();
- mBluetoothRfcommChannel.completeOobDataExchange(mOobEligibleDevice, mMockCallback,
- mMockBluetoothAdapter);
- verify(mMockCallback, times(0)).onOobExchangeSuccess();
- verify(mMockCallback, times(0)).onOobExchangeFailure();
- }
-}
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/oob/OobConnectionManagerTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/oob/OobConnectionManagerTest.java
deleted file mode 100644
index 13f93c9..0000000
--- a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/oob/OobConnectionManagerTest.java
+++ /dev/null
@@ -1,176 +0,0 @@
-/*
- * Copyright (C) 2020 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.connecteddevice.oob;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.testng.Assert.assertThrows;
-
-import android.security.keystore.KeyProperties;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import com.android.car.connecteddevice.model.OobEligibleDevice;
-
-import com.google.common.primitives.Bytes;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.security.InvalidKeyException;
-import java.security.SecureRandom;
-
-import javax.crypto.AEADBadTagException;
-import javax.crypto.KeyGenerator;
-import javax.crypto.SecretKey;
-
-@RunWith(AndroidJUnit4.class)
-public class OobConnectionManagerTest {
- private static final byte[] TEST_MESSAGE = "testMessage".getBytes();
- private TestChannel mTestChannel;
- private SecretKey mTestKey;
- private byte[] mTestEncryptionIv = new byte[OobConnectionManager.NONCE_LENGTH_BYTES];
- private byte[] mTestDecryptionIv = new byte[OobConnectionManager.NONCE_LENGTH_BYTES];
- private byte[] mTestOobData;
-
- @Before
- public void setUp() throws Exception {
- mTestChannel = new TestChannel();
- mTestKey = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES).generateKey();
-
- SecureRandom secureRandom = new SecureRandom();
- secureRandom.nextBytes(mTestEncryptionIv);
- secureRandom.nextBytes(mTestDecryptionIv);
-
- mTestOobData = Bytes.concat(mTestDecryptionIv, mTestEncryptionIv, mTestKey.getEncoded());
- }
-
- @Test
- public void testInitAsServer_keyIsNull() {
- OobConnectionManager oobConnectionManager = new OobConnectionManager();
- assertThat(oobConnectionManager.mEncryptionKey).isNull();
- }
-
- @Test
- public void testServer_onSetOobData_setsKeyAndNonce() {
- OobConnectionManager oobConnectionManager = new OobConnectionManager();
- oobConnectionManager.setOobData(mTestOobData);
- assertThat(oobConnectionManager.mEncryptionKey).isEqualTo(mTestKey);
- // The decryption IV for the server is the encryption IV for the client and vice versa
- assertThat(oobConnectionManager.mDecryptionIv).isEqualTo(mTestEncryptionIv);
- assertThat(oobConnectionManager.mEncryptionIv).isEqualTo(mTestDecryptionIv);
- }
-
- @Test
- public void testInitAsClient_keyAndNoncesAreNonNullAndSent() {
- OobConnectionManager oobConnectionManager = new OobConnectionManager();
- oobConnectionManager.startOobExchange(mTestChannel);
- assertThat(oobConnectionManager.mEncryptionKey).isNotNull();
- assertThat(oobConnectionManager.mEncryptionIv).isNotNull();
- assertThat(oobConnectionManager.mDecryptionIv).isNotNull();
- assertThat(mTestChannel.mSentOobData).isEqualTo(Bytes.concat(
- oobConnectionManager.mDecryptionIv,
- oobConnectionManager.mEncryptionIv,
- oobConnectionManager.mEncryptionKey.getEncoded()
- ));
- }
-
- @Test
- public void testServerEncryptAndClientDecrypt() throws Exception {
- OobConnectionManager clientOobConnectionManager = new OobConnectionManager();
- clientOobConnectionManager.startOobExchange(mTestChannel);
- OobConnectionManager serverOobConnectionManager = new OobConnectionManager();
- serverOobConnectionManager.setOobData(mTestChannel.mSentOobData);
-
- byte[] encryptedTestMessage = clientOobConnectionManager.encryptVerificationCode(
- TEST_MESSAGE);
- byte[] decryptedTestMessage = serverOobConnectionManager.decryptVerificationCode(
- encryptedTestMessage);
-
- assertThat(decryptedTestMessage).isEqualTo(TEST_MESSAGE);
- }
-
- @Test
- public void testClientEncryptAndServerDecrypt() throws Exception {
- OobConnectionManager clientOobConnectionManager = new OobConnectionManager();
- clientOobConnectionManager.startOobExchange(mTestChannel);
- OobConnectionManager serverOobConnectionManager = new OobConnectionManager();
- serverOobConnectionManager.setOobData(mTestChannel.mSentOobData);
-
- byte[] encryptedTestMessage = serverOobConnectionManager.encryptVerificationCode(
- TEST_MESSAGE);
- byte[] decryptedTestMessage = clientOobConnectionManager.decryptVerificationCode(
- encryptedTestMessage);
-
- assertThat(decryptedTestMessage).isEqualTo(TEST_MESSAGE);
- }
-
- @Test
- public void testEncryptAndDecryptWithDifferentNonces_throwsAEADBadTagException()
- throws Exception {
- // The OobConnectionManager stores a different nonce for encryption and decryption, so it
- // can't decrypt messages that it encrypted itself. It can only send encrypted messages to
- // an OobConnectionManager on another device that share its nonces and encryption key.
- OobConnectionManager oobConnectionManager = new OobConnectionManager();
- oobConnectionManager.startOobExchange(mTestChannel);
- byte[] encryptedMessage = oobConnectionManager.encryptVerificationCode(TEST_MESSAGE);
- assertThrows(AEADBadTagException.class,
- () -> oobConnectionManager.decryptVerificationCode(encryptedMessage));
- }
-
- @Test
- public void testDecryptWithShortMessage_throwsAEADBadTagException() {
- OobConnectionManager oobConnectionManager = new OobConnectionManager();
- oobConnectionManager.startOobExchange(mTestChannel);
- assertThrows(AEADBadTagException.class,
- () -> oobConnectionManager.decryptVerificationCode("short".getBytes()));
- }
-
- @Test
- public void testEncryptWithNullKey_throwsInvalidKeyException() {
- OobConnectionManager oobConnectionManager = new OobConnectionManager();
- assertThrows(InvalidKeyException.class,
- () -> oobConnectionManager.encryptVerificationCode(TEST_MESSAGE));
- }
-
- @Test
- public void testDecryptWithNullKey_throwsInvalidKeyException() {
- OobConnectionManager oobConnectionManager = new OobConnectionManager();
- assertThrows(InvalidKeyException.class,
- () -> oobConnectionManager.decryptVerificationCode(TEST_MESSAGE));
- }
-
- private static class TestChannel implements OobChannel {
- byte[] mSentOobData = null;
-
- @Override
- public void completeOobDataExchange(OobEligibleDevice device, Callback callback) {
-
- }
-
- @Override
- public void sendOobData(byte[] oobData) {
- mSentOobData = oobData;
- }
-
- @Override
- public void interrupt() {
-
- }
- }
-}
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/storage/ConnectedDeviceStorageTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/storage/ConnectedDeviceStorageTest.java
deleted file mode 100644
index 755ce64..0000000
--- a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/storage/ConnectedDeviceStorageTest.java
+++ /dev/null
@@ -1,153 +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.connecteddevice.storage;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.testng.Assert.assertThrows;
-
-import android.content.Context;
-import android.util.Pair;
-
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import com.android.car.connecteddevice.model.AssociatedDevice;
-import com.android.car.connecteddevice.util.ByteUtils;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.security.InvalidParameterException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.UUID;
-
-@RunWith(AndroidJUnit4.class)
-public final class ConnectedDeviceStorageTest {
- private final Context mContext = ApplicationProvider.getApplicationContext();
-
- private final int mActiveUserId = 10;
-
- private ConnectedDeviceStorage mConnectedDeviceStorage;
-
- private List<Pair<Integer, AssociatedDevice>> mAddedAssociatedDevices;
-
- @Before
- public void setUp() {
- mConnectedDeviceStorage = new ConnectedDeviceStorage(mContext);
- mAddedAssociatedDevices = new ArrayList<>();
- }
-
- @After
- public void tearDown() {
- // Clear any associated devices added during tests.
- for (Pair<Integer, AssociatedDevice> device : mAddedAssociatedDevices) {
- mConnectedDeviceStorage.removeAssociatedDevice(device.first,
- device.second.getDeviceId());
- }
- }
-
- @Test
- public void getAssociatedDeviceIdsForUser_includesNewlyAddedDevice() {
- AssociatedDevice addedDevice = addRandomAssociatedDevice(mActiveUserId);
- List<String> associatedDevices =
- mConnectedDeviceStorage.getAssociatedDeviceIdsForUser(mActiveUserId);
- assertThat(associatedDevices).containsExactly(addedDevice.getDeviceId());
- }
-
- @Test
- public void getAssociatedDeviceIdsForUser_excludesDeviceAddedForOtherUser() {
- addRandomAssociatedDevice(mActiveUserId);
- List<String> associatedDevices =
- mConnectedDeviceStorage.getAssociatedDeviceIdsForUser(mActiveUserId + 1);
- assertThat(associatedDevices).isEmpty();
- }
-
- @Test
- public void getAssociatedDeviceIdsForUser_excludesRemovedDevice() {
- AssociatedDevice addedDevice = addRandomAssociatedDevice(mActiveUserId);
- mConnectedDeviceStorage.removeAssociatedDevice(mActiveUserId, addedDevice.getDeviceId());
- List<String> associatedDevices =
- mConnectedDeviceStorage.getAssociatedDeviceIdsForUser(mActiveUserId);
- assertThat(associatedDevices).isEmpty();
- }
-
- @Test
- public void getAssociatedDevicesForUser_includesNewlyAddedDevice() {
- AssociatedDevice addedDevice = addRandomAssociatedDevice(mActiveUserId);
- List<AssociatedDevice> associatedDevices =
- mConnectedDeviceStorage.getAssociatedDevicesForUser(mActiveUserId);
- assertThat(associatedDevices).containsExactly(addedDevice);
- }
-
- @Test
- public void getAssociatedDevicesForUser_excludesDeviceAddedForOtherUser() {
- addRandomAssociatedDevice(mActiveUserId);
- List<String> associatedDevices =
- mConnectedDeviceStorage.getAssociatedDeviceIdsForUser(mActiveUserId + 1);
- assertThat(associatedDevices).isEmpty();
- }
-
- @Test
- public void getAssociatedDevicesForUser_excludesRemovedDevice() {
- AssociatedDevice addedDevice = addRandomAssociatedDevice(mActiveUserId);
- mConnectedDeviceStorage.removeAssociatedDevice(mActiveUserId, addedDevice.getDeviceId());
- List<AssociatedDevice> associatedDevices =
- mConnectedDeviceStorage.getAssociatedDevicesForUser(mActiveUserId);
- assertThat(associatedDevices).isEmpty();
- }
-
- @Test
- public void getEncryptionKey_returnsSavedKey() {
- String deviceId = addRandomAssociatedDevice(mActiveUserId).getDeviceId();
- byte[] key = ByteUtils.randomBytes(16);
- mConnectedDeviceStorage.saveEncryptionKey(deviceId, key);
- assertThat(mConnectedDeviceStorage.getEncryptionKey(deviceId)).isEqualTo(key);
- }
-
- @Test
- public void getEncryptionKey_returnsNullForUnrecognizedDeviceId() {
- String deviceId = addRandomAssociatedDevice(mActiveUserId).getDeviceId();
- mConnectedDeviceStorage.saveEncryptionKey(deviceId, ByteUtils.randomBytes(16));
- assertThat(mConnectedDeviceStorage.getEncryptionKey(UUID.randomUUID().toString())).isNull();
- }
-
- @Test
- public void saveChallengeSecret_throwsForInvalidLengthSecret() {
- byte[] invalidSecret =
- ByteUtils.randomBytes(ConnectedDeviceStorage.CHALLENGE_SECRET_BYTES - 1);
- assertThrows(InvalidParameterException.class,
- () -> mConnectedDeviceStorage.saveChallengeSecret(UUID.randomUUID().toString(),
- invalidSecret));
- }
-
- private AssociatedDevice addRandomAssociatedDevice(int userId) {
- AssociatedDevice device = new AssociatedDevice(UUID.randomUUID().toString(),
- "00:00:00:00:00:00", "Test Device", true);
- addAssociatedDevice(userId, device, ByteUtils.randomBytes(16));
- return device;
- }
-
- private void addAssociatedDevice(int userId, AssociatedDevice device, byte[] encryptionKey) {
- mConnectedDeviceStorage.addAssociatedDeviceForUser(userId, device);
- mConnectedDeviceStorage.saveEncryptionKey(device.getDeviceId(), encryptionKey);
- mAddedAssociatedDevices.add(new Pair<>(userId, device));
- }
-}
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/util/ScanDataAnalyzerTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/util/ScanDataAnalyzerTest.java
deleted file mode 100644
index 92e8d34..0000000
--- a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/util/ScanDataAnalyzerTest.java
+++ /dev/null
@@ -1,108 +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.connecteddevice.util;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.math.BigInteger;
-
-@RunWith(AndroidJUnit4.class)
-public class ScanDataAnalyzerTest {
- private static final BigInteger CORRECT_DATA =
- new BigInteger(
- "02011A14FF4C000100000000000000000000000000200000000000000000000000000000"
- + "0000000000000000000000000000000000000000000000000000",
- 16);
-
- private static final BigInteger CORRECT_MASK =
- new BigInteger("00000000000000000000000000200000", 16);
-
- private static final BigInteger MULTIPLE_BIT_MASK =
- new BigInteger("00000000000000000100000000200000", 16);
-
- @Test
- public void containsUuidsInOverflow_correctBitFlipped_shouldReturnTrue() {
- assertThat(
- ScanDataAnalyzer.containsUuidsInOverflow(CORRECT_DATA.toByteArray(), CORRECT_MASK))
- .isTrue();
- }
-
- @Test
- public void containsUuidsInOverflow_bitNotFlipped_shouldReturnFalse() {
- assertThat(
- ScanDataAnalyzer.containsUuidsInOverflow(
- CORRECT_DATA.negate().toByteArray(), CORRECT_MASK))
- .isFalse();
- }
-
- @Test
- public void containsUuidsInOverflow_maskWithMultipleBitsIncompleteMatch_shouldReturnTrue() {
- assertThat(
- ScanDataAnalyzer.containsUuidsInOverflow(CORRECT_DATA.toByteArray(),
- MULTIPLE_BIT_MASK))
- .isTrue();
- }
-
- @Test
- public void containsUuidsInOverflow_incorrectLengthByte_shouldReturnFalse() {
- // Incorrect length of 0x20
- byte[] data =
- new BigInteger(
- "02011A20FF4C00010000000000000000000000000020000000000000000000000000000000"
- + "00000000000000000000000000000000000000000000000000",
- 16)
- .toByteArray();
- BigInteger mask = new BigInteger("00000000000000000000000000200000", 16);
- assertThat(ScanDataAnalyzer.containsUuidsInOverflow(data, mask)).isFalse();
- }
-
- @Test
- public void containsUuidsInOverflow_incorrectAdTypeByte_shouldReturnFalse() {
- // Incorrect advertising type of 0xEF
- byte[] data =
- new BigInteger(
- "02011A14EF4C00010000000000000000000000000020000000000000000000000000000000"
- + "00000000000000000000000000000000000000000000000000",
- 16)
- .toByteArray();
- assertThat(ScanDataAnalyzer.containsUuidsInOverflow(data, CORRECT_MASK)).isFalse();
- }
-
- @Test
- public void containsUuidsInOverflow_incorrectCustomId_shouldReturnFalse() {
- // Incorrect custom id of 0x4C1001
- byte[] data =
- new BigInteger(
- "02011A14FF4C10010000000000000000000000000020000000000000000000000000000000"
- + "00000000000000000000000000000000000000000000000000",
- 16)
- .toByteArray();
- assertThat(ScanDataAnalyzer.containsUuidsInOverflow(data, CORRECT_MASK)).isFalse();
- }
-
- @Test
- public void containsUuidsInOverflow_incorrectContentLength_shouldReturnFalse() {
- byte[] data = new BigInteger("02011A14FF4C1001000000000000000000000000002", 16)
- .toByteArray();
- assertThat(ScanDataAnalyzer.containsUuidsInOverflow(data, CORRECT_MASK)).isFalse();
- }
-}