Merge "Merge identical phone numbers for the same contact" into pi-car-dev
diff --git a/car-apps-common/src/com/android/car/apps/common/CarUxRestrictionsUtil.java b/car-apps-common/src/com/android/car/apps/common/CarUxRestrictionsUtil.java
index ed219ae..219bf59 100644
--- a/car-apps-common/src/com/android/car/apps/common/CarUxRestrictionsUtil.java
+++ b/car-apps-common/src/com/android/car/apps/common/CarUxRestrictionsUtil.java
@@ -38,6 +38,8 @@
* This class must be a singleton because only one listener can be registered with
* {@link CarUxRestrictionsManager} at a time, as documented in
* {@link CarUxRestrictionsManager#registerListener}.
+ *
+ * @deprecated Use {@link com.android.car.ui.utils.CarUxRestrictionsUtil} instead
*/
public class CarUxRestrictionsUtil {
private static final String TAG = "CarUxRestrictionsUtil";
@@ -63,7 +65,7 @@
}
};
- mCarApi = Car.createCar(context);
+ mCarApi = Car.createCar(context.getApplicationContext());
mObservers = Collections.newSetFromMap(new WeakHashMap<>());
try {
diff --git a/car-apps-common/src/com/android/car/apps/common/ClickThroughToolbar.java b/car-apps-common/src/com/android/car/apps/common/ClickThroughToolbar.java
index 3a05111..7e5b79c 100644
--- a/car-apps-common/src/com/android/car/apps/common/ClickThroughToolbar.java
+++ b/car-apps-common/src/com/android/car/apps/common/ClickThroughToolbar.java
@@ -28,7 +28,7 @@
* <p>By default, the {@link Toolbar} eats all touches on it. This view will override
* {@link #onTouchEvent(MotionEvent)} and return {@code false} if configured to allow pass through.
*
- * @deprecated Use {@link com.android.car.ui.Toolbar} instead
+ * @deprecated Use {@link com.android.car.ui.toolbar.Toolbar} instead
*/
@Deprecated
public class ClickThroughToolbar extends Toolbar {
diff --git a/car-apps-common/src/com/android/car/apps/common/imaging/ImageBinder.java b/car-apps-common/src/com/android/car/apps/common/imaging/ImageBinder.java
index 0e59a54..f640218 100644
--- a/car-apps-common/src/com/android/car/apps/common/imaging/ImageBinder.java
+++ b/car-apps-common/src/com/android/car/apps/common/imaging/ImageBinder.java
@@ -158,7 +158,7 @@
getImageFetcher(context).cancelRequest(mCurrentKey, mFetchReceiver);
onRequestFinished();
}
- setDrawable(getLoadingDrawable(context));
+ setDrawable(mPlaceholderType != PlaceholderType.NONE ? getLoadingDrawable(context) : null);
}
private void onRequestFinished() {
diff --git a/car-apps-common/src/com/android/car/apps/common/imaging/LocalImageFetcher.java b/car-apps-common/src/com/android/car/apps/common/imaging/LocalImageFetcher.java
index 185ec67..dc10caf 100644
--- a/car-apps-common/src/com/android/car/apps/common/imaging/LocalImageFetcher.java
+++ b/car-apps-common/src/com/android/car/apps/common/imaging/LocalImageFetcher.java
@@ -241,7 +241,8 @@
}
};
- private @ImageDecoder.Allocator int mAllocatorMode = ImageDecoder.ALLOCATOR_HARDWARE;
+ // ALLOCATOR_HARDWARE causes crashes on some emulators (in media center's queue).
+ private @ImageDecoder.Allocator int mAllocatorMode = ImageDecoder.ALLOCATOR_SOFTWARE;
@Override
protected Drawable doInBackground(Void... voids) {
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
index 829b644..dea0d6a 100644
--- 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
@@ -17,7 +17,6 @@
package com.android.car.media.common.browse;
import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.media.MediaBrowserCompat;
@@ -53,7 +52,7 @@
private ChildrenSubscription mSubscription;
- BrowsedMediaItems(@NonNull MediaBrowserCompat mediaBrowser, @Nullable String parentId) {
+ BrowsedMediaItems(@NonNull MediaBrowserCompat mediaBrowser, @NonNull String parentId) {
mBrowser = mediaBrowser;
mParentId = parentId;
}
@@ -61,10 +60,7 @@
@Override
protected void onActive() {
super.onActive();
- String rootNode = mBrowser.getRoot();
- String itemId = mParentId != null ? mParentId : rootNode;
-
- mSubscription = new ChildrenSubscription(itemId);
+ mSubscription = new ChildrenSubscription(mParentId);
mSubscription.start(CHILDREN_SUBSCRIPTION_RETRIES, CHILDREN_SUBSCRIPTION_RETRY_TIME_MS);
}
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
index 74143c1..5a4626a 100644
--- 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
@@ -112,7 +112,7 @@
* {@link #getBrowsedMediaItems()}.
*/
@UiThread
- void setCurrentBrowseId(@Nullable String browseId);
+ void setCurrentBrowseId(@NonNull String browseId);
/**
* Set the current item to be searched for. If available, the list of items will be emitted
@@ -137,61 +137,16 @@
*/
@NonNull
public static MediaBrowserViewModel.WithMutableBrowseId getInstanceWithMediaBrowser(
+ @NonNull String key,
@NonNull ViewModelProvider viewModelProvider,
@NonNull LiveData<MediaBrowserCompat> mediaBrowser) {
- MediaBrowserViewModelImpl viewModel = viewModelProvider.get(
- MediaBrowserViewModelImpl.class);
+ MutableMediaBrowserViewModel viewModel =
+ viewModelProvider.get(key, MutableMediaBrowserViewModel.class);
initMediaBrowser(mediaBrowser, viewModel);
return viewModel;
}
/**
- * Fetch an initialized {@link MediaBrowserViewModel.WithMutableBrowseId}. It will get its
- * media browser from the {@link MediaSourceViewModel} provided by {@code
- * viewModelProvider}.
- *
- *
- * @param mediaSourceVM the {@link MediaSourceViewModel} singleton.
- * @param viewModelProvider the ViewModelProvider to load ViewModels from.
- * @param key a key to decide which instance of the ViewModel to fetch.
- * Subsequent calls with the same key will return the same
- * instance.
- * @return an initialized MediaBrowserViewModel.WithMutableBrowseId for the given key.
- * @see ViewModelProvider#get(String, Class)
- */
- @NonNull
- public static MediaBrowserViewModel.WithMutableBrowseId getInstanceForKey(
- MediaSourceViewModel mediaSourceVM, @NonNull ViewModelProvider viewModelProvider,
- @NonNull String key) {
- MediaBrowserViewModelImpl viewModel = viewModelProvider.get(key,
- MediaBrowserViewModelImpl.class);
- initMediaBrowser(mediaSourceVM.getConnectedMediaBrowser(), 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 {@code browseId}.
- *
- *
- * @param mediaSourceVM the {@link MediaSourceViewModel} singleton.
- * @param viewModelProvider the ViewModelProvider to load ViewModels from.
- * @param browseId the browseId to browse. This will also serve as the key for
- * fetching the ViewModel.
- * @return an initialized MediaBrowserViewModel configured to browse the specified browseId.
- */
- @NonNull
- public static MediaBrowserViewModel getInstanceForBrowseId(
- MediaSourceViewModel mediaSourceVM, @NonNull ViewModelProvider viewModelProvider,
- @NonNull String browseId) {
- MediaBrowserViewModel.WithMutableBrowseId viewModel =
- getInstanceForKey(mediaSourceVM, viewModelProvider, browseId);
- viewModel.setCurrentBrowseId(browseId);
- 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.
@@ -203,9 +158,9 @@
@NonNull
public static MediaBrowserViewModel getInstanceForBrowseRoot(
MediaSourceViewModel mediaSourceVM, @NonNull ViewModelProvider viewModelProvider) {
- MediaBrowserViewModel.WithMutableBrowseId viewModel =
- getInstanceForKey(mediaSourceVM, viewModelProvider, KEY_BROWSER_ROOT);
- viewModel.setCurrentBrowseId(null);
+ RootMediaBrowserViewModel viewModel =
+ viewModelProvider.get(KEY_BROWSER_ROOT, RootMediaBrowserViewModel.class);
+ initMediaBrowser(mediaSourceVM.getConnectedMediaBrowser(), viewModel);
return viewModel;
}
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 71a1e56..7d47ba7 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
@@ -25,7 +25,6 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.annotation.UiThread;
import android.app.Application;
import android.os.Bundle;
import android.support.v4.media.MediaBrowserCompat;
@@ -49,14 +48,15 @@
* obtained via {@link MediaBrowserViewModel.Factory}
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class MediaBrowserViewModelImpl extends AndroidViewModel implements
- MediaBrowserViewModel.WithMutableBrowseId {
+class MediaBrowserViewModelImpl extends AndroidViewModel implements MediaBrowserViewModel {
+
+ private final boolean mIsRoot;
private final SwitchingLiveData<MediaBrowserCompat> mMediaBrowserSwitch =
SwitchingLiveData.newInstance();
- private final MutableLiveData<String> mCurrentBrowseId = new MutableLiveData<>();
- private final MutableLiveData<String> mCurrentSearchQuery = dataOf(null);
+ final MutableLiveData<String> mCurrentBrowseId = dataOf(null);
+ final MutableLiveData<String> mCurrentSearchQuery = dataOf(null);
private final LiveData<MediaBrowserCompat> mConnectedMediaBrowser =
map(mMediaBrowserSwitch.asLiveData(), MediaBrowserViewModelImpl::requireConnected);
@@ -67,9 +67,11 @@
private final LiveData<String> mPackageName;
- public MediaBrowserViewModelImpl(@NonNull Application application) {
+ MediaBrowserViewModelImpl(@NonNull Application application, boolean isRoot) {
super(application);
+ mIsRoot = isRoot;
+
mPackageName = map(mConnectedMediaBrowser,
mediaBrowser -> {
if (mediaBrowser == null) return null;
@@ -78,10 +80,14 @@
mBrowsedMediaItems =
loadingSwitchMap(pair(mConnectedMediaBrowser, mCurrentBrowseId),
- split((mediaBrowser, browseId) ->
- mediaBrowser == null
- ? null
- : new BrowsedMediaItems(mediaBrowser, browseId)));
+ 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) ->
@@ -143,26 +149,6 @@
return mMediaBrowserSwitch.getSource();
}
- /**
- * Set the current item to be browsed. If available, the list of items will be emitted by {@link
- * #getBrowsedMediaItems()}.
- */
- @UiThread
- @Override
- public void setCurrentBrowseId(@Nullable String browseId) {
- mCurrentBrowseId.setValue(browseId);
- }
-
- /**
- * Set the current item to be searched for. If available, the list of items will be emitted
- * by {@link #getBrowsedMediaItems()}.
- */
- @UiThread
- @Override
- public void search(@Nullable String query) {
- mCurrentSearchQuery.setValue(query);
- }
-
@Override
public LiveData<String> getPackageName() {
return mPackageName;
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
new file mode 100644
index 0000000..482efdd
--- /dev/null
+++ b/car-media-common/src/com/android/car/media/common/browse/MutableMediaBrowserViewModel.java
@@ -0,0 +1,45 @@
+/*
+ * 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.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UiThread;
+import android.app.Application;
+
+import androidx.annotation.RestrictTo;
+
+/** 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
new file mode 100644
index 0000000..1edb484
--- /dev/null
+++ b/car-media-common/src/com/android/car/media/common/browse/RootMediaBrowserViewModel.java
@@ -0,0 +1,30 @@
+/*
+ * 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.annotation.NonNull;
+import android.app.Application;
+
+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/source/MediaSourceViewModel.java b/car-media-common/src/com/android/car/media/common/source/MediaSourceViewModel.java
index d25ebe9..1d02209 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
@@ -217,20 +217,13 @@
return;
}
- // Reset dependent values to avoid propagating inconsistencies.
- mMediaController.setValue(null);
- mConnectedMediaBrowser.setValue(null);
- mBrowserConnector.connectTo(null);
-
// Broadcast the new source
mPrimaryMediaSource.setValue(newMediaSource);
// Recompute dependent values
- if (newMediaSource == null) {
- return;
+ if (newMediaSource != null) {
+ ComponentName browseService = newMediaSource.getBrowseServiceComponentName();
+ mBrowserConnector.connectTo(browseService);
}
-
- ComponentName browseService = newMediaSource.getBrowseServiceComponentName();
- mBrowserConnector.connectTo(browseService);
}
}
diff --git a/car-telephony-common/res/values/strings.xml b/car-telephony-common/res/values/strings.xml
index e4f7b52..24a3300 100644
--- a/car-telephony-common/res/values/strings.xml
+++ b/car-telephony-common/res/values/strings.xml
@@ -42,4 +42,9 @@
<!-- Status label for phone state. … is an ellipsis. [CHAR LIMIT=25] -->
<string name="call_state_call_ending">Disconnecting…</string>
+ <!-- String format used to format a address Uri. -->
+ <string name="address_uri_format" translatable="false">geo:0,0?q=%s</string>
+ <!-- String format used to format a navigation Uri. -->
+ <string name="navigation_uri_format" translatable="false">https://maps.google.com/maps?daddr=%s&nav=1</string>
+
</resources>
\ No newline at end of file
diff --git a/car-telephony-common/src/com/android/car/telephony/common/Contact.java b/car-telephony-common/src/com/android/car/telephony/common/Contact.java
index d61fbf9..f2b45f8 100644
--- a/car-telephony-common/src/com/android/car/telephony/common/Contact.java
+++ b/car-telephony-common/src/com/android/car/telephony/common/Contact.java
@@ -159,11 +159,6 @@
private String mLookupKey;
/**
- * All phone numbers of this contact mapping to the unique primary key for the raw data entry.
- */
- private List<PhoneNumber> mPhoneNumbers = new ArrayList<>();
-
- /**
* A URI that can be used to retrieve a thumbnail of the contact's photo.
*/
@Nullable
@@ -198,6 +193,17 @@
private boolean mIsVoiceMail;
/**
+ * All phone numbers of this contact mapping to the unique primary key for the raw data entry.
+ */
+ private final List<PhoneNumber> mPhoneNumbers = new ArrayList<>();
+
+ /**
+ * All postal addresses of this contact mapping to the unique primary key for the raw data
+ * entry
+ */
+ private final List<PostalAddress> mPostalAddresses = new ArrayList<>();
+
+ /**
* Parses a contact entry for a Cursor loaded from the Contact Database. A new contact will be
* created and returned.
*/
@@ -226,7 +232,8 @@
contact.loadBasicInfo(cursor);
}
- if (!accountName.equals(contact.mAccountName) || !lookupKey.equals(contact.mLookupKey)) {
+ if (!TextUtils.equals(accountName, contact.mAccountName)
+ || !TextUtils.equals(lookupKey, contact.mLookupKey)) {
Log.w(TAG, "A wrong contact is passed in. A new contact will be created.");
contact = new Contact();
contact.loadBasicInfo(cursor);
@@ -243,6 +250,9 @@
case ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE:
contact.addPhoneNumber(context, cursor);
break;
+ case ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE:
+ contact.addPostalAddress(cursor);
+ break;
default:
Log.d(TAG,
String.format("This mimetype %s will not be loaded right now.", mimeType));
@@ -340,6 +350,15 @@
}
}
+ /**
+ * Loads the data whose mimetype is
+ * {@link ContactsContract.CommonDataKinds.StructuredPostal#CONTENT_ITEM_TYPE}.
+ */
+ private void addPostalAddress(Cursor cursor) {
+ PostalAddress postalAddress = PostalAddress.fromCursor(cursor);
+ mPostalAddresses.add(postalAddress);
+ }
+
@Override
public boolean equals(Object obj) {
return obj instanceof Contact && mLookupKey.equals(((Contact) obj).mLookupKey)
@@ -465,6 +484,13 @@
}
/**
+ * Return all postal addresses associated with this contact.
+ */
+ public List<PostalAddress> getPostalAddresses() {
+ return mPostalAddresses;
+ }
+
+ /**
* Returns if this Contact represents a voice mail number.
*/
public boolean isVoicemail() {
@@ -544,6 +570,11 @@
for (PhoneNumber phoneNumber : mPhoneNumbers) {
dest.writeParcelable(phoneNumber, flags);
}
+
+ dest.writeInt(mPostalAddresses.size());
+ for (PostalAddress postalAddress : mPostalAddresses) {
+ dest.writeParcelable(postalAddress, flags);
+ }
}
public static final Creator<Contact> CREATOR = new Creator<Contact>() {
@@ -581,7 +612,6 @@
contact.mIsVoiceMail = source.readBoolean();
contact.mPrimaryPhoneNumber = source.readParcelable(PhoneNumber.class.getClassLoader());
int phoneNumberListLength = source.readInt();
- contact.mPhoneNumbers = new ArrayList<>();
for (int i = 0; i < phoneNumberListLength; i++) {
PhoneNumber phoneNumber = source.readParcelable(PhoneNumber.class.getClassLoader());
contact.mPhoneNumbers.add(phoneNumber);
@@ -590,6 +620,12 @@
}
}
+ int postalAddressListLength = source.readInt();
+ for (int i = 0; i < postalAddressListLength; i++) {
+ PostalAddress address = source.readParcelable(PostalAddress.class.getClassLoader());
+ contact.mPostalAddresses.add(address);
+ }
+
return contact;
}
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 db95960..501fa02 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
@@ -22,11 +22,13 @@
import android.text.TextUtils;
import android.util.Log;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
@@ -39,7 +41,6 @@
*/
public class InMemoryPhoneBook implements Observer<List<Contact>> {
private static final String TAG = "CD.InMemoryPhoneBook";
- private static final String KEY_FORMAT = "%s %s";
private static InMemoryPhoneBook sInMemoryPhoneBook;
private final Context mContext;
@@ -49,9 +50,10 @@
*/
private final Map<I18nPhoneNumberWrapper, Contact> mPhoneNumberContactMap = new HashMap<>();
/**
- * A map to look up contact by lookup key.
+ * A map to look up contact by account name and lookup key. Each entry presents a map of lookup
+ * key to contacts for one account.
*/
- private final Map<String, Contact> mLookupKeyContactMap = new HashMap<>();
+ private final Map<String, Map<String, Contact>> mLookupKeyContactMap = new HashMap<>();
private boolean mIsLoaded = false;
/**
@@ -103,10 +105,12 @@
ContactsContract.Data.CONTENT_URI,
null,
ContactsContract.Data.MIMETYPE + " = ? OR "
+ + ContactsContract.Data.MIMETYPE + " = ? OR "
+ ContactsContract.Data.MIMETYPE + " = ?",
new String[]{
ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE,
- ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE},
+ ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE,
+ ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE},
ContactsContract.Contacts.DISPLAY_NAME + " ASC ");
mContactListAsyncQueryLiveData = new AsyncQueryLiveData<List<Contact>>(mContext,
QueryParam.of(contactListQueryParam), Executors.newSingleThreadExecutor()) {
@@ -158,11 +162,11 @@
}
/**
- * Looks up a {@link Contact} by the given lookup key and account name. Returns null if can't
- * find the contact entry.
+ * Looks up a {@link Contact} by the given lookup key and account name. Account name could be
+ * null for locally added contacts. Returns null if can't find the contact entry.
*/
@Nullable
- public Contact lookupContactByKey(String lookupKey, String accountName) {
+ public Contact lookupContactByKey(String lookupKey, @Nullable String accountName) {
if (!isLoaded()) {
Log.w(TAG, "looking up a contact while loading.");
}
@@ -170,16 +174,41 @@
Log.w(TAG, "looking up an empty lookup key.");
return null;
}
- if (TextUtils.isEmpty(accountName)) {
- Log.w(TAG, "looking up an empty lookup account");
- return null;
+ if (mLookupKeyContactMap.containsKey(accountName)) {
+ return mLookupKeyContactMap.get(accountName).get(lookupKey);
}
- return mLookupKeyContactMap.get(getContactKey(lookupKey, accountName));
+ return null;
+ }
+
+ /**
+ * Iterates all the accounts and returns a list of contacts that match the lookup key. This API
+ * is discouraged to use whenever the account name is available where {@link
+ * #lookupContactByKey(String, String)} should be used instead.
+ */
+ @NonNull
+ public List<Contact> lookupContactByKey(String lookupKey) {
+ if (!isLoaded()) {
+ Log.w(TAG, "looking up a contact while loading.");
+ }
+
+ if (TextUtils.isEmpty(lookupKey)) {
+ Log.w(TAG, "looking up an empty lookup key.");
+ return Collections.emptyList();
+ }
+ List<Contact> results = new ArrayList<>();
+ // Iterate all the accounts to get all the match contacts with given lookup key.
+ for (Map<String, Contact> subMap : mLookupKeyContactMap.values()) {
+ if (subMap.containsKey(lookupKey)) {
+ results.add(subMap.get(lookupKey));
+ }
+ }
+
+ return results;
}
private List<Contact> onCursorLoaded(Cursor cursor) {
- Map<String, Contact> contactMap = new LinkedHashMap<>();
+ Map<String, Map<String, Contact>> contactMap = new LinkedHashMap<>();
List<Contact> contactList = new ArrayList<>();
while (cursor.moveToNext()) {
@@ -188,12 +217,18 @@
int lookupKeyColumn = cursor.getColumnIndex(ContactsContract.Data.LOOKUP_KEY);
String accountName = cursor.getString(accountNameColumn);
String lookupKey = cursor.getString(lookupKeyColumn);
- String key = getContactKey(lookupKey, accountName);
- contactMap.put(key, Contact.fromCursor(mContext, cursor, contactMap.get(key)));
+ if (!contactMap.containsKey(accountName)) {
+ contactMap.put(accountName, new HashMap<>());
+ }
+
+ Map<String, Contact> subMap = contactMap.get(accountName);
+ subMap.put(lookupKey, Contact.fromCursor(mContext, cursor, subMap.get(lookupKey)));
}
- contactList.addAll(contactMap.values());
+ for (Map<String, Contact> subMap : contactMap.values()) {
+ contactList.addAll(subMap.values());
+ }
mLookupKeyContactMap.clear();
mLookupKeyContactMap.putAll(contactMap);
@@ -211,13 +246,4 @@
Log.d(TAG, "Contacts loaded:" + (contacts == null ? 0 : contacts.size()));
mIsLoaded = true;
}
-
- /**
- * Formats a key to identify a contact based the lookup key and the account name.
- */
- private String getContactKey(String lookupKey, String accountName) {
- String key = String.format(KEY_FORMAT, lookupKey, accountName);
- Log.d(TAG, "Contact key is: " + key);
- return key;
- }
}
diff --git a/car-telephony-common/src/com/android/car/telephony/common/PostalAddress.java b/car-telephony-common/src/com/android/car/telephony/common/PostalAddress.java
new file mode 100644
index 0000000..7d0baec
--- /dev/null
+++ b/car-telephony-common/src/com/android/car/telephony/common/PostalAddress.java
@@ -0,0 +1,150 @@
+/*
+ * 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.telephony.common;
+
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.ContactsContract;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Encapsulates data about an address entry. Typically loaded from the local Address store.
+ */
+public class PostalAddress implements Parcelable {
+ private static final String TAG = "CD.PostalAddress";
+
+ /**
+ * The formatted address.
+ */
+ private String mFormattedAddress;
+
+ /**
+ * The address type. See more at {@link ContactsContract.CommonDataKinds.StructuredPostal#TYPE}
+ */
+ private int mType;
+
+ /**
+ * The user defined label. See more at
+ * {@link ContactsContract.CommonDataKinds.StructuredPostal#LABEL}
+ */
+ @Nullable
+ private String mLabel;
+
+ /**
+ * Parses a PostalAddress entry for a Cursor loaded from the Address Database.
+ */
+ public static PostalAddress fromCursor(Cursor cursor) {
+ int formattedAddressColumn = cursor.getColumnIndex(
+ ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS);
+ int addressTypeColumn = cursor.getColumnIndex(
+ ContactsContract.CommonDataKinds.StructuredPostal.TYPE);
+ int labelColumn = cursor.getColumnIndex(
+ ContactsContract.CommonDataKinds.StructuredPostal.LABEL);
+
+ PostalAddress postalAddress = new PostalAddress();
+ postalAddress.mFormattedAddress = cursor.getString(formattedAddressColumn);
+ postalAddress.mType = cursor.getInt(addressTypeColumn);
+ postalAddress.mLabel = cursor.getString(labelColumn);
+
+ return postalAddress;
+ }
+
+ /**
+ * Returns {@link #mFormattedAddress}
+ */
+ public String getFormattedAddress() {
+ return mFormattedAddress;
+ }
+
+ /**
+ * Returns {@link #mType}
+ */
+ public int getType() {
+ return mType;
+ }
+
+ /**
+ * Returns {@link #mLabel}
+ */
+ @Nullable
+ public String getLabel() {
+ return mLabel;
+ }
+
+ /**
+ * Returns a human readable string label. For example, Home, Work, etc.
+ */
+ public CharSequence getReadableLabel(Resources res) {
+ return ContactsContract.CommonDataKinds.StructuredPostal.getTypeLabel(res, mType, mLabel);
+ }
+
+ /**
+ * Returns the address Uri for {@link #mFormattedAddress}.
+ */
+ public Uri getAddressUri(Resources res) {
+ String address = String.format(res.getString(R.string.address_uri_format),
+ Uri.encode(mFormattedAddress));
+ Log.d(TAG, "The address is: " + address);
+ return Uri.parse(address);
+ }
+
+ /**
+ * Returns the navigation Uri for {@link #mFormattedAddress}.
+ */
+ public Uri getNavigationUri(Resources res) {
+ String address = String.format(res.getString(R.string.navigation_uri_format),
+ Uri.encode(mFormattedAddress));
+ Log.d(TAG, "The address is: " + address);
+ return Uri.parse(address);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mType);
+ dest.writeString(mLabel);
+ dest.writeString(mFormattedAddress);
+ }
+
+ /**
+ * Create {@link PostalAddress} object from saved parcelable.
+ */
+ public static Creator<PostalAddress> CREATOR = new Creator<PostalAddress>() {
+ @Override
+ public PostalAddress createFromParcel(Parcel source) {
+ PostalAddress postalAddress = new PostalAddress();
+ postalAddress.mType = source.readInt();
+ postalAddress.mLabel = source.readString();
+ postalAddress.mFormattedAddress = source.readString();
+ return postalAddress;
+ }
+
+ @Override
+ public PostalAddress[] newArray(int size) {
+ return new PostalAddress[size];
+ }
+ };
+}
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 00c1e07..c9c2f8c 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
@@ -362,8 +362,8 @@
}
/**
- * Sets a Contact avatar onto the provided {@code icon}. The first letter or both letters
- * of the contact's initials.
+ * Sets a Contact avatar onto the provided {@code icon}. The first letter or both letters of the
+ * contact's initials.
*/
public static void setContactBitmapAsync(
Context context,
@@ -373,9 +373,9 @@
}
/**
- * Sets a Contact avatar onto the provided {@code icon}. The first letter or both letters
- * of the contact's initials or {@code fallbackDisplayName} will be used as a fallback resource
- * if avatar loading fails.
+ * Sets a Contact avatar onto the provided {@code icon}. The first letter or both letters of the
+ * contact's initials or {@code fallbackDisplayName} will be used as a fallback resource if
+ * avatar loading fails.
*/
public static void setContactBitmapAsync(
Context context,
@@ -383,9 +383,9 @@
@Nullable final Contact contact,
@Nullable final String fallbackDisplayName) {
Uri avatarUri = contact != null ? contact.getAvatarUri() : null;
- String initials = contact != null
- ? contact.getInitials() : fallbackDisplayName.substring(0, 1);
- String identifier = TextUtils.isEmpty(initials) ? fallbackDisplayName : initials;
+ String initials = contact != null ? contact.getInitials()
+ : (fallbackDisplayName == null ? null : getInitials(fallbackDisplayName, null));
+ String identifier = contact == null ? fallbackDisplayName : contact.getDisplayName();
setContactBitmapAsync(context, icon, avatarUri, initials, identifier);
}
@@ -416,10 +416,10 @@
/**
* Create a {@link LetterTileDrawable} for the given initials.
*
- * @param initials is the letters that will be drawn on the canvas. If it is null, then
- * an avatar anonymous icon will be drawn
- * @param identifier will decide the color for the drawable. If null, a default color will
- * be used.
+ * @param initials is the letters that will be drawn on the canvas. If it is null, then an
+ * avatar anonymous icon will be drawn
+ * @param identifier will decide the color for the drawable. If null, a default color will be
+ * used.
*/
public static LetterTileDrawable createLetterTile(
Context context,
@@ -506,7 +506,13 @@
}
}
- static String getInitials(String name, String nameAlt) {
+ /**
+ * Returns the initials based on the name and nameAlt.
+ *
+ * @param name should be the display name of a contact.
+ * @param nameAlt should be alternative display name of a contact.
+ */
+ public static String getInitials(String name, String nameAlt) {
StringBuilder initials = new StringBuilder();
if (!TextUtils.isEmpty(name) && Character.isLetter(name.charAt(0))) {
initials.append(Character.toUpperCase(name.charAt(0)));
diff --git a/car-ui-lib/build.gradle b/car-ui-lib/build.gradle
index 7a8596f..aa720d4 100644
--- a/car-ui-lib/build.gradle
+++ b/car-ui-lib/build.gradle
@@ -23,7 +23,7 @@
}
dependencies {
- classpath 'com.android.tools.build:gradle:3.5.0'
+ classpath 'com.android.tools.build:gradle:3.5.1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
diff --git a/car-ui-lib/res/layout/car_ui_alert_dialog_title_with_subtitle.xml b/car-ui-lib/res/layout/car_ui_alert_dialog_title_with_subtitle.xml
new file mode 100644
index 0000000..389e511
--- /dev/null
+++ b/car-ui-lib/res/layout/car_ui_alert_dialog_title_with_subtitle.xml
@@ -0,0 +1,51 @@
+<?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.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/title_template"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@style/Widget.CarUi.AlertDialog.HeaderContainer">
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="@dimen/car_ui_dialog_icon_size"
+ android:layout_height="@dimen/car_ui_dialog_icon_size"
+ style="@style/Widget.CarUi.AlertDialog.Icon"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@style/Widget.CarUi.AlertDialog.TitleContainer">
+ <com.android.internal.widget.DialogTitle
+ android:id="@+id/alertTitle"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAlignment="viewStart"
+ style="?android:attr/windowTitleStyle" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:id="@+id/alertSubtitle"
+ android:textAppearance="@style/TextAppearance.CarUi.AlertDialog.Subtitle"/>
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/car-ui-lib/res/layout/car_ui_list_preference_dialog.xml b/car-ui-lib/res/layout/car_ui_list_preference.xml
similarity index 100%
rename from car-ui-lib/res/layout/car_ui_list_preference_dialog.xml
rename to car-ui-lib/res/layout/car_ui_list_preference.xml
diff --git a/car-ui-lib/res/layout/car_ui_preference.xml b/car-ui-lib/res/layout/car_ui_preference.xml
index 2c2f4cf..03e101d 100644
--- a/car-ui-lib/res/layout/car_ui_preference.xml
+++ b/car-ui-lib/res/layout/car_ui_preference.xml
@@ -35,7 +35,7 @@
android:layout_marginEnd="@dimen/car_ui_preference_icon_margin_end"
android:layout_marginTop="@dimen/car_ui_preference_content_margin_top"
android:scaleType="fitCenter"
- android:tint="@color/car_ui_preference_icon_color"/>
+ style="@style/Preference.CarUi.Icon"/>
<LinearLayout
android:layout_width="match_parent"
diff --git a/car-ui-lib/res/layout/car_ui_radio_button_item.xml b/car-ui-lib/res/layout/car_ui_radio_button_item.xml
index e6733b1..f6e58ee 100644
--- a/car-ui-lib/res/layout/car_ui_radio_button_item.xml
+++ b/car-ui-lib/res/layout/car_ui_radio_button_item.xml
@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
+<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2019 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,10 +13,51 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-
-
-<RadioButton
+<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/radio_button"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"/>
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/car_ui_list_item_radio_button_height">
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/car_ui_radio_button_start_guideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_begin="@dimen/car_ui_list_item_radio_button_start_inset" />
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/car_ui_radio_button_end_guideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_end="@dimen/car_ui_list_item_radio_button_end_inset" />
+
+ <FrameLayout
+ android:id="@+id/radio_button_container"
+ android:layout_width="@dimen/car_ui_list_item_radio_button_icon_container_width"
+ android:layout_height="0dp"
+ android:background="?android:attr/selectableItemBackground"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="@+id/car_ui_radio_button_start_guideline"
+ app:layout_constraintTop_toTopOf="parent">
+
+ <RadioButton
+ android:id="@+id/radio_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center" />
+ </FrameLayout>
+
+ <TextView
+ android:id="@+id/text"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/car_ui_list_item_text_start_margin"
+ android:textAppearance="@style/TextAppearance.CarUi.ListItem"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="@+id/car_ui_radio_button_end_guideline"
+ app:layout_constraintStart_toEndOf="@+id/radio_button_container"
+ app:layout_constraintTop_toTopOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/car-ui-lib/res/layout/car_ui_toolbar.xml b/car-ui-lib/res/layout/car_ui_toolbar.xml
index f22318d..2d54431 100644
--- a/car-ui-lib/res/layout/car_ui_toolbar.xml
+++ b/car-ui-lib/res/layout/car_ui_toolbar.xml
@@ -150,16 +150,28 @@
android:layout_width="match_parent"
android:layout_height="@dimen/car_ui_toolbar_separator_height"
style="@style/Widget.CarUi.Toolbar.SeparatorView"
- app:layout_constraintTop_toBottomOf="@id/car_ui_toolbar_bottom_guideline"
+ app:layout_constraintBottom_toTopOf="@id/car_ui_toolbar_bottom_guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
+ <ProgressBar
+ android:id="@+id/car_ui_toolbar_progress_bar"
+ style="@style/Widget.CarUi.Toolbar.ProgressBar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:layout_constraintBottom_toTopOf="@id/car_ui_toolbar_row_separator"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ android:indeterminate="true"
+ android:visibility="gone"/>
+
<View
android:id="@+id/car_ui_toolbar_bottom_styleable"
android:layout_width="match_parent"
android:layout_height="@dimen/car_ui_toolbar_bottom_view_height"
style="@style/Widget.CarUi.Toolbar.BottomView"
- app:layout_constraintTop_toBottomOf="@+id/car_ui_toolbar_row_separator"
+ app:layout_constraintBottom_toTopOf="@+id/car_ui_toolbar_progress_bar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
+
</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/car-ui-lib/res/layout/car_ui_toolbar_two_row.xml b/car-ui-lib/res/layout/car_ui_toolbar_two_row.xml
index fe9eadc..bc334bf 100644
--- a/car-ui-lib/res/layout/car_ui_toolbar_two_row.xml
+++ b/car-ui-lib/res/layout/car_ui_toolbar_two_row.xml
@@ -164,4 +164,15 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
+ <ProgressBar
+ android:id="@+id/car_ui_toolbar_progress_bar"
+ style="@style/Widget.CarUi.Toolbar.ProgressBar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:layout_constraintBottom_toTopOf="@id/car_ui_toolbar_bottom_styleable"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ android:indeterminate="true"
+ android:visibility="gone"/>
+
</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/car-ui-lib/res/values-w1280dp/dimens.xml b/car-ui-lib/res/values-w1280dp/dimens.xml
new file mode 100644
index 0000000..a06df2b
--- /dev/null
+++ b/car-ui-lib/res/values-w1280dp/dimens.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <!-- Keylines -->
+ <dimen name="car_ui_keyline_1">32dp</dimen>
+ <dimen name="car_ui_keyline_2">108dp</dimen>
+ <dimen name="car_ui_keyline_3">128dp</dimen>
+ <dimen name="car_ui_keyline_4">168dp</dimen>
+</resources>
diff --git a/car-ui-lib/res/values/attrs.xml b/car-ui-lib/res/values/attrs.xml
index f6a60c9..71ad091 100644
--- a/car-ui-lib/res/values/attrs.xml
+++ b/car-ui-lib/res/values/attrs.xml
@@ -46,6 +46,10 @@
<attr name="id" format="reference"/>
<!-- Show/hide the MenuItem -->
<attr name="visible" format="boolean"/>
+ <!-- Set this to true to make a search MenuItem. This will override every other property except id, visible, and onclick. -->
+ <attr name="search" format="boolean"/>
+ <!-- Set this to true to make a settings MenuItem. This will override every other property except id, visible, and onclick. -->
+ <attr name="settings" format="boolean"/>
<!-- Title -->
<attr name="title"/>
<!-- Icon -->
@@ -109,6 +113,14 @@
</attr>
</declare-styleable>
+ <declare-styleable name="CarUiPreference">
+ <!-- Toggle for showing chevron -->
+ <attr name="showChevron" format="boolean" />
+ </declare-styleable>
+
+ <!-- Theme attribute to specify a default style for all CarUiPreferences -->
+ <attr name="carUiPreferenceStyle" format="reference" />
+
<!-- Theme attribute to specify a default style for all CarUiRecyclerViews -->
<attr name="carUiRecyclerViewStyle" format="reference" />
diff --git a/car-ui-lib/res/values/dimens.xml b/car-ui-lib/res/values/dimens.xml
index 1ffa471..1679a1d 100644
--- a/car-ui-lib/res/values/dimens.xml
+++ b/car-ui-lib/res/values/dimens.xml
@@ -40,6 +40,12 @@
<dimen name="car_ui_body2_size">28sp</dimen>
<dimen name="car_ui_body3_size">24sp</dimen>
+ <!-- Keylines -->
+ <dimen name="car_ui_keyline_1">24dp</dimen>
+ <dimen name="car_ui_keyline_2">96dp</dimen>
+ <dimen name="car_ui_keyline_3">112dp</dimen>
+ <dimen name="car_ui_keyline_4">148dp</dimen>
+
<!-- Tabs -->
<!-- Exact size of the tab textbox. Use @dimen/wrap_content if this must be flexible -->
@@ -172,6 +178,8 @@
<dimen name="car_ui_dialog_edittext_margin_bottom">10dp</dimen>
<dimen name="car_ui_dialog_edittext_margin_start">22dp</dimen>
<dimen name="car_ui_dialog_edittext_margin_end">22dp</dimen>
+ <dimen name="car_ui_dialog_icon_size">56dp</dimen>
+ <dimen name="car_ui_dialog_title_margin">@dimen/car_ui_keyline_1</dimen>
<!-- List item -->
@@ -187,4 +195,9 @@
<dimen name="car_ui_list_item_icon_container_width">112dp</dimen>
<dimen name="car_ui_list_item_action_divider_width">1dp</dimen>
<dimen name="car_ui_list_item_action_divider_height">60dp</dimen>
+
+ <dimen name="car_ui_list_item_radio_button_height">@dimen/car_ui_list_item_height</dimen>
+ <dimen name="car_ui_list_item_radio_button_start_inset">@dimen/car_ui_list_item_start_inset</dimen>
+ <dimen name="car_ui_list_item_radio_button_end_inset">@dimen/car_ui_list_item_end_inset</dimen>
+ <dimen name="car_ui_list_item_radio_button_icon_container_width">@dimen/car_ui_list_item_icon_container_width</dimen>
</resources>
diff --git a/car-ui-lib/res/values/styles.xml b/car-ui-lib/res/values/styles.xml
index 5881a63..cb11c1e 100644
--- a/car-ui-lib/res/values/styles.xml
+++ b/car-ui-lib/res/values/styles.xml
@@ -34,6 +34,10 @@
<item name="android:paddingEnd">@dimen/car_ui_toolbar_title_logo_padding</item>
</style>
+ <style name="Widget.CarUi.Toolbar.ProgressBar"
+ parent="@android:style/Widget.DeviceDefault.ProgressBar.Horizontal">
+ </style>
+
<style name="Widget.CarUi.Toolbar.NavIcon">
<item name="android:tint">@color/car_ui_toolbar_nav_icon_color</item>
<item name="android:src">@drawable/car_ui_icon_arrow_back</item>
@@ -110,6 +114,26 @@
<item name="android:scrollbars">none</item>
</style>
+ <style name="Widget.CarUi.AlertDialog"/>
+
+ <style name="Widget.CarUi.AlertDialog.HeaderContainer">
+ <item name="android:orientation">horizontal</item>
+ <item name="android:gravity">center_vertical|start</item>
+ <item name="android:paddingTop">18dp</item>
+ <item name="android:paddingBottom">18dp</item>
+ </style>
+
+ <style name="Widget.CarUi.AlertDialog.TitleContainer">
+ <item name="android:layout_marginStart">@dimen/car_ui_dialog_title_margin</item>
+ <item name="android:layout_marginEnd">@dimen/car_ui_dialog_title_margin</item>
+ <item name="android:orientation">vertical</item>
+ </style>
+
+ <style name="Widget.CarUi.AlertDialog.Icon">
+ <item name="android:layout_marginStart">@dimen/car_ui_dialog_title_margin</item>
+ <item name="android:scaleType">fitCenter</item>
+ </style>
+
<style name="Preference.CarUi.ListPreference" parent="android:Theme.DeviceDefault.Dialog">
<item name="android:windowNoTitle">true</item>
<!-- Set this to true if you want Full Screen without status bar -->
@@ -147,11 +171,15 @@
<item name="android:layout">@layout/car_ui_preference_dropdown</item>
</style>
+ <style name="Preference.CarUi.Icon"/>
+
<style name="Preference.CarUi.Information">
<item name="android:enabled">false</item>
<item name="android:shouldDisableView">false</item>
</style>
+ <style name="Preference.CarUi.Preference"/>
+
<style name="Preference.CarUi.PreferenceScreen"/>
<style name="Preference.CarUi.SeekBarPreference">
@@ -207,13 +235,12 @@
</style>
<style name="TextAppearance.CarUi.PreferenceEditTextDialogMessage">
- <item name="android:textColor">
- @color/car_ui_preference_edit_text_dialog_message_text_color
- </item>
- <item name="android:textSize">@dimen/car_ui_preference_edit_text_dialog_message_text_size
- </item>
+ <item name="android:textColor">@color/car_ui_preference_edit_text_dialog_message_text_color</item>
+ <item name="android:textSize">@dimen/car_ui_preference_edit_text_dialog_message_text_size</item>
</style>
+ <style name="TextAppearance.CarUi.AlertDialog.Subtitle" parent="android:TextAppearance.DeviceDefault"/>
+
<style name="TextAppearance.CarUi.Widget" parent="android:TextAppearance.DeviceDefault.Widget"/>
<style name="TextAppearance.CarUi.Widget.Toolbar"/>
diff --git a/car-ui-lib/res/values/themes.xml b/car-ui-lib/res/values/themes.xml
index 70e6872..d40e13c 100644
--- a/car-ui-lib/res/values/themes.xml
+++ b/car-ui-lib/res/values/themes.xml
@@ -205,13 +205,10 @@
</style>
<style name="CarUiPreferenceTheme">
- <item name="checkBoxPreferenceStyle">@style/Preference.CarUi.CheckBoxPreference
- </item>
+ <item name="checkBoxPreferenceStyle">@style/Preference.CarUi.CheckBoxPreference</item>
<item name="dialogPreferenceStyle">@style/Preference.CarUi.DialogPreference</item>
<item name="dropdownPreferenceStyle">@style/Preference.CarUi.DropDown</item>
- <item name="editTextPreferenceStyle">
- @style/Preference.CarUi.DialogPreference.EditTextPreference
- </item>
+ <item name="editTextPreferenceStyle">@style/Preference.CarUi.DialogPreference.EditTextPreference</item>
<item name="preferenceCategoryStyle">@style/Preference.CarUi.Category</item>
<item name="preferenceFragmentCompatStyle">@style/PreferenceFragment.CarUi</item>
<item name="preferenceFragmentListStyle">@style/PreferenceFragmentList.CarUi</item>
diff --git a/car-ui-lib/src/com/android/car/ui/AlertDialogBuilder.java b/car-ui-lib/src/com/android/car/ui/AlertDialogBuilder.java
index 78d8d87..25431d8 100644
--- a/car-ui-lib/src/com/android/car/ui/AlertDialogBuilder.java
+++ b/car-ui-lib/src/com/android/car/ui/AlertDialogBuilder.java
@@ -27,7 +27,9 @@
import android.view.View;
import android.widget.AdapterView;
import android.widget.EditText;
+import android.widget.ImageView;
import android.widget.ListAdapter;
+import android.widget.TextView;
import androidx.annotation.ArrayRes;
import androidx.annotation.AttrRes;
@@ -44,7 +46,9 @@
private boolean mPositiveButtonSet;
private boolean mNeutralButtonSet;
private boolean mNegativeButtonSet;
- private String mDefaultButtonText;
+ private CharSequence mTitle;
+ private CharSequence mSubtitle;
+ private Drawable mIcon;
public AlertDialogBuilder(Context context) {
// Resource id specified as 0 uses the parent contexts resolved value for alertDialogTheme.
@@ -54,7 +58,6 @@
public AlertDialogBuilder(Context context, int themeResId) {
mBuilder = new AlertDialog.Builder(context, themeResId);
mContext = context;
- mDefaultButtonText = context.getString(R.string.car_ui_alert_dialog_default_button);
}
public Context getContext() {
@@ -67,8 +70,7 @@
* @return This Builder object to allow for chaining of calls to set methods
*/
public AlertDialogBuilder setTitle(@StringRes int titleId) {
- mBuilder.setTitle(titleId);
- return this;
+ return setTitle(mContext.getText(titleId));
}
/**
@@ -77,11 +79,31 @@
* @return This Builder object to allow for chaining of calls to set methods
*/
public AlertDialogBuilder setTitle(CharSequence title) {
+ mTitle = title;
mBuilder.setTitle(title);
return this;
}
/**
+ * Sets a subtitle to be displayed in the {@link Dialog}.
+ *
+ * @return This Builder object to allow for chaining of calls to set methods
+ */
+ public AlertDialogBuilder setSubtitle(@StringRes int subtitle) {
+ return setSubtitle(mContext.getString(subtitle));
+ }
+
+ /**
+ * Sets a subtitle to be displayed in the {@link Dialog}.
+ *
+ * @return This Builder object to allow for chaining of calls to set methods
+ */
+ public AlertDialogBuilder setSubtitle(CharSequence subtitle) {
+ mSubtitle = subtitle;
+ return this;
+ }
+
+ /**
* Set the message to display using the given resource id.
*
* @return This Builder object to allow for chaining of calls to set methods
@@ -109,8 +131,7 @@
* @return This Builder object to allow for chaining of calls to set methods
*/
public AlertDialogBuilder setIcon(@DrawableRes int iconId) {
- mBuilder.setIcon(iconId);
- return this;
+ return setIcon(mContext.getDrawable(iconId));
}
/**
@@ -124,6 +145,7 @@
* methods
*/
public AlertDialogBuilder setIcon(Drawable icon) {
+ mIcon = icon;
mBuilder.setIcon(icon);
return this;
}
@@ -510,10 +532,8 @@
*/
public AlertDialogBuilder setEditBox(String prompt, TextWatcher textChangedListener,
InputFilter[] inputFilters, int inputType) {
- LayoutInflater layoutInflater =
- (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- View contentView = layoutInflater.inflate(R.layout.car_ui_alert_dialog_edit_text,
- null);
+ View contentView = LayoutInflater.from(mContext).inflate(
+ R.layout.car_ui_alert_dialog_edit_text, null);
EditText editText = contentView.requireViewById(R.id.textbox);
editText.setText(prompt);
@@ -548,6 +568,33 @@
return setEditBox(prompt, textChangedListener, inputFilters, 0);
}
+
+ /** Final steps common to both {@link #create()} and {@link #show()} */
+ private void prepareDialog() {
+ if (mSubtitle != null) {
+
+ View customTitle = LayoutInflater.from(mContext).inflate(
+ R.layout.car_ui_alert_dialog_title_with_subtitle, null);
+
+ TextView mTitleView = customTitle.requireViewById(R.id.alertTitle);
+ TextView mSubtitleView = customTitle.requireViewById(R.id.alertSubtitle);
+ ImageView mIconView = customTitle.requireViewById(R.id.icon);
+
+ mTitleView.setText(mTitle);
+ mSubtitleView.setText(mSubtitle);
+ mIconView.setImageDrawable(mIcon);
+ mIconView.setVisibility(mIcon != null ? View.VISIBLE : View.GONE);
+ mBuilder.setCustomTitle(customTitle);
+ }
+
+ if (!mNeutralButtonSet && !mNegativeButtonSet && !mPositiveButtonSet) {
+ String mDefaultButtonText = mContext.getString(
+ R.string.car_ui_alert_dialog_default_button);
+ mBuilder.setNegativeButton(mDefaultButtonText, (dialog, which) -> {
+ });
+ }
+ }
+
/**
* Creates an {@link AlertDialog} with the arguments supplied to this
* builder.
@@ -557,6 +604,7 @@
* create and display the dialog.
*/
public AlertDialog create() {
+ prepareDialog();
return mBuilder.create();
}
@@ -571,12 +619,7 @@
* </pre>
*/
public AlertDialog show() {
- if (mNeutralButtonSet || mNegativeButtonSet || mPositiveButtonSet) {
- return mBuilder.show();
- }
-
- mBuilder.setNegativeButton(mDefaultButtonText, (dialog, which) -> {
- });
+ prepareDialog();
return mBuilder.show();
}
}
diff --git a/car-ui-lib/src/com/android/car/ui/preference/CarUiDialogFragment.java b/car-ui-lib/src/com/android/car/ui/preference/CarUiDialogFragment.java
new file mode 100644
index 0000000..6a848b8
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/preference/CarUiDialogFragment.java
@@ -0,0 +1,234 @@
+/*
+ * 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.ui.preference;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.TextView;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.LayoutRes;
+import androidx.annotation.NonNull;
+import androidx.fragment.app.DialogFragment;
+import androidx.preference.DialogPreference;
+
+/**
+ * Abstract base class which presents a dialog associated with a {@link
+ * androidx.preference.DialogPreference}. Since the preference object may not be available during
+ * fragment re-creation, the necessary information for displaying the dialog is read once during
+ * the initial call to {@link #onCreate(Bundle)} and saved/restored in the saved instance state.
+ * Custom subclasses should also follow this pattern.
+ *
+ * <p>Note: this is borrowed as-is from {@link androidx.preference.PreferenceDialogFragmentCompat}
+ * with updates to formatting to match the project style and the removal of the {@link
+ * DialogPreference.TargetFragment} interface requirement. See {@link PreferenceDialogFragment}
+ * for a version of this class with the check preserved. Automotive applications should use
+ * children of this fragment in order to launch the system themed platform {@link AlertDialog}
+ * instead of the one in the support library.
+ */
+
+public abstract class CarUiDialogFragment extends DialogFragment implements
+ DialogInterface.OnClickListener {
+
+ private static final String SAVE_STATE_TITLE = "CarUiDialogFragment.title";
+ private static final String SAVE_STATE_POSITIVE_TEXT = "CarUiDialogFragment.positiveText";
+ private static final String SAVE_STATE_NEGATIVE_TEXT = "CarUiDialogFragment.negativeText";
+ private static final String SAVE_STATE_MESSAGE = "CarUiDialogFragment.message";
+ private static final String SAVE_STATE_LAYOUT = "CarUiDialogFragment.layout";
+ private static final String SAVE_STATE_ICON = "CarUiDialogFragment.icon";
+
+ protected CharSequence mDialogTitle;
+ protected CharSequence mPositiveButtonText;
+ protected CharSequence mNegativeButtonText;
+ protected CharSequence mDialogMessage;
+ @LayoutRes
+ protected int mDialogLayoutRes;
+
+ protected BitmapDrawable mDialogIcon;
+
+ /** Which button was clicked. */
+ private int mWhichButtonClicked;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (savedInstanceState != null) {
+ mDialogTitle = savedInstanceState.getCharSequence(SAVE_STATE_TITLE);
+ mPositiveButtonText = savedInstanceState.getCharSequence(SAVE_STATE_POSITIVE_TEXT);
+ mNegativeButtonText = savedInstanceState.getCharSequence(SAVE_STATE_NEGATIVE_TEXT);
+ mDialogMessage = savedInstanceState.getCharSequence(SAVE_STATE_MESSAGE);
+ mDialogLayoutRes = savedInstanceState.getInt(SAVE_STATE_LAYOUT, 0);
+ Bitmap bitmap = savedInstanceState.getParcelable(SAVE_STATE_ICON);
+ if (bitmap != null) {
+ mDialogIcon = new BitmapDrawable(getResources(), bitmap);
+ }
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ outState.putCharSequence(SAVE_STATE_TITLE, mDialogTitle);
+ outState.putCharSequence(SAVE_STATE_POSITIVE_TEXT, mPositiveButtonText);
+ outState.putCharSequence(SAVE_STATE_NEGATIVE_TEXT, mNegativeButtonText);
+ outState.putCharSequence(SAVE_STATE_MESSAGE, mDialogMessage);
+ outState.putInt(SAVE_STATE_LAYOUT, mDialogLayoutRes);
+ if (mDialogIcon != null) {
+ outState.putParcelable(SAVE_STATE_ICON, mDialogIcon.getBitmap());
+ }
+ }
+
+ @Override
+ @NonNull
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ Context context = getActivity();
+ mWhichButtonClicked = DialogInterface.BUTTON_NEGATIVE;
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context)
+ .setTitle(mDialogTitle)
+ .setIcon(mDialogIcon)
+ .setPositiveButton(mPositiveButtonText, this)
+ .setNegativeButton(mNegativeButtonText, this);
+
+ View contentView = onCreateDialogView(context);
+ if (contentView != null) {
+ onBindDialogView(contentView);
+ builder.setView(contentView);
+ } else {
+ builder.setMessage(mDialogMessage);
+ }
+
+ onPrepareDialogBuilder(builder);
+
+ // Create the dialog
+ Dialog dialog = builder.create();
+ if (needInputMethod()) {
+ // Request input only after the dialog is shown. This is to prevent an issue where the
+ // dialog view collapsed the content on small displays.
+ dialog.setOnShowListener(d -> requestInputMethod(dialog));
+ }
+
+ return dialog;
+ }
+
+ /**
+ * Prepares the dialog builder to be shown when the preference is clicked. Use this to set
+ * custom properties on the dialog.
+ *
+ * <p>Do not {@link AlertDialog.Builder#create()} or {@link AlertDialog.Builder#show()}.
+ */
+ protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
+ }
+
+ /**
+ * Returns whether the preference needs to display a soft input method when the dialog is
+ * displayed. Default is false. Subclasses should override this method if they need the soft
+ * input method brought up automatically.
+ *
+ * <p>Note: Ensure your subclass manually requests focus (ideally in {@link
+ * #onBindDialogView(View)}) for the input field in order to
+ * correctly attach the input method to the field.
+ */
+ protected boolean needInputMethod() {
+ return false;
+ }
+
+ /**
+ * Sets the required flags on the dialog window to enable input method window to show up.
+ */
+ private void requestInputMethod(Dialog dialog) {
+ Window window = dialog.getWindow();
+ window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
+ }
+
+ /**
+ * Creates the content view for the dialog (if a custom content view is required). By default,
+ * it inflates the dialog layout resource if it is set.
+ *
+ * @return the content View for the dialog.
+ * @see DialogPreference#setLayoutResource(int)
+ */
+ protected View onCreateDialogView(Context context) {
+ int resId = mDialogLayoutRes;
+ if (resId == 0) {
+ return null;
+ }
+
+ LayoutInflater inflater = LayoutInflater.from(context);
+ return inflater.inflate(resId, null);
+ }
+
+ /**
+ * Binds views in the content View of the dialog to data.
+ *
+ * <p>Make sure to call through to the superclass implementation.
+ *
+ * @param view the content View of the dialog, if it is custom.
+ */
+ @CallSuper
+ protected void onBindDialogView(View view) {
+ View dialogMessageView = view.findViewById(android.R.id.message);
+
+ if (dialogMessageView != null) {
+ CharSequence message = mDialogMessage;
+ int newVisibility = View.GONE;
+
+ if (!TextUtils.isEmpty(message)) {
+ if (dialogMessageView instanceof TextView) {
+ ((TextView) dialogMessageView).setText(message);
+ }
+
+ newVisibility = View.VISIBLE;
+ }
+
+ if (dialogMessageView.getVisibility() != newVisibility) {
+ dialogMessageView.setVisibility(newVisibility);
+ }
+ }
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ mWhichButtonClicked = which;
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ super.onDismiss(dialog);
+ onDialogClosed(mWhichButtonClicked == DialogInterface.BUTTON_POSITIVE);
+ }
+
+ /**
+ * Called when the dialog is dismissed.
+ *
+ * @param positiveResult {@code true} if the dialog was dismissed with {@link
+ * DialogInterface#BUTTON_POSITIVE}.
+ */
+ protected abstract void onDialogClosed(boolean positiveResult);
+}
diff --git a/car-ui-lib/src/com/android/car/ui/preference/CarUiEditTextPreference.java b/car-ui-lib/src/com/android/car/ui/preference/CarUiEditTextPreference.java
index 079ab39..4480714 100644
--- a/car-ui-lib/src/com/android/car/ui/preference/CarUiEditTextPreference.java
+++ b/car-ui-lib/src/com/android/car/ui/preference/CarUiEditTextPreference.java
@@ -30,6 +30,7 @@
public class CarUiEditTextPreference extends EditTextPreference {
private final Context mContext;
+ private boolean mShowChevron = true;
public CarUiEditTextPreference(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
@@ -56,13 +57,17 @@
public void onAttached() {
super.onAttached();
- boolean showChevron = mContext.getResources().getBoolean(
+ boolean allowChevron = mContext.getResources().getBoolean(
R.bool.car_ui_preference_show_chevron);
- if (!showChevron) {
+ if (!allowChevron || !mShowChevron) {
return;
}
setWidgetLayoutResource(R.layout.car_ui_preference_chevron);
}
+
+ public void setShowChevron(boolean showChevron) {
+ mShowChevron = showChevron;
+ }
}
diff --git a/car-ui-lib/src/com/android/car/ui/preference/CarUiPreference.java b/car-ui-lib/src/com/android/car/ui/preference/CarUiPreference.java
index 19b5d19..5db22cf 100644
--- a/car-ui-lib/src/com/android/car/ui/preference/CarUiPreference.java
+++ b/car-ui-lib/src/com/android/car/ui/preference/CarUiPreference.java
@@ -17,6 +17,7 @@
package com.android.car.ui.preference;
import android.content.Context;
+import android.content.res.TypedArray;
import android.util.AttributeSet;
import androidx.preference.Preference;
@@ -29,37 +30,47 @@
*/
public class CarUiPreference extends Preference {
- private final Context mContext;
+ private Context mContext;
+ private boolean mShowChevron;
public CarUiPreference(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
- mContext = context;
+ init(context, attrs, defStyleAttr, defStyleRes);
}
public CarUiPreference(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- mContext = context;
+ this(context, attrs, defStyleAttr, R.style.Preference_CarUi_Preference);
}
public CarUiPreference(Context context, AttributeSet attrs) {
- super(context, attrs);
- mContext = context;
+ this(context, attrs, R.attr.carUiPreferenceStyle);
}
public CarUiPreference(Context context) {
- super(context);
+ this(context, null);
+ }
+
+ public void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
mContext = context;
+
+ TypedArray a = getContext().obtainStyledAttributes(
+ attrs,
+ R.styleable.CarUiPreference,
+ defStyleAttr,
+ defStyleRes);
+
+ mShowChevron = a.getBoolean(R.styleable.CarUiPreference_showChevron, true);
}
@Override
public void onAttached() {
super.onAttached();
- boolean showChevron = mContext.getResources().getBoolean(
+ boolean allowChevron = mContext.getResources().getBoolean(
R.bool.car_ui_preference_show_chevron);
- if (!showChevron || getWidgetLayoutResource() != 0) {
+ if (!allowChevron || !mShowChevron) {
return;
}
@@ -68,4 +79,8 @@
setWidgetLayoutResource(R.layout.car_ui_preference_chevron);
}
}
+
+ public void setShowChevron(boolean showChevron) {
+ mShowChevron = showChevron;
+ }
}
diff --git a/car-ui-lib/src/com/android/car/ui/preference/CarUiRecyclerViewRadioButtonAdapter.java b/car-ui-lib/src/com/android/car/ui/preference/CarUiRecyclerViewRadioButtonAdapter.java
index 7c6ab9e..41e1351 100644
--- a/car-ui-lib/src/com/android/car/ui/preference/CarUiRecyclerViewRadioButtonAdapter.java
+++ b/car-ui-lib/src/com/android/car/ui/preference/CarUiRecyclerViewRadioButtonAdapter.java
@@ -20,7 +20,9 @@
import android.view.View;
import android.view.ViewGroup;
import android.widget.RadioButton;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.android.car.ui.R;
@@ -55,6 +57,7 @@
mSelectedPosition = position;
}
+ @NonNull
@Override
public CarUiRecyclerViewRadioButtonAdapter.ViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
@@ -70,7 +73,7 @@
//since only one radio button is allowed to be selected,
// this condition un-checks previous selections
holder.mRadioButton.setChecked(mSelectedPosition == position);
- holder.mRadioButton.setText(entry);
+ holder.mTextView.setText(entry);
}
@Override
@@ -89,15 +92,17 @@
}
/** The viewholder class for recyclerview containing radio buttons. */
- public class ViewHolder extends RecyclerView.ViewHolder {
+ class ViewHolder extends RecyclerView.ViewHolder {
- public RadioButton mRadioButton;
+ RadioButton mRadioButton;
+ TextView mTextView;
- public ViewHolder(View view) {
+ ViewHolder(View view) {
super(view);
- mRadioButton = (RadioButton) view.findViewById(R.id.radio_button);
+ mRadioButton = view.findViewById(R.id.radio_button);
+ mTextView = view.findViewById(R.id.text);
- mRadioButton.setOnClickListener(v -> {
+ view.setOnClickListener(v -> {
mSelectedPosition = getAdapterPosition();
notifyDataSetChanged();
if (mOnRadioButtonClickedListener != null) {
diff --git a/car-ui-lib/src/com/android/car/ui/preference/EditTextPreferenceDialogFragment.java b/car-ui-lib/src/com/android/car/ui/preference/EditTextPreferenceDialogFragment.java
index f2d7c3e..97a58d2 100644
--- a/car-ui-lib/src/com/android/car/ui/preference/EditTextPreferenceDialogFragment.java
+++ b/car-ui-lib/src/com/android/car/ui/preference/EditTextPreferenceDialogFragment.java
@@ -18,8 +18,12 @@
import android.app.AlertDialog;
import android.os.Bundle;
+import android.text.InputType;
+import android.view.KeyEvent;
import android.view.View;
+import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
+import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.preference.EditTextPreference;
@@ -32,13 +36,14 @@
* implementations in order to launch the system themed platform {@link AlertDialog} instead of the
* one in the support library.
*/
-public class EditTextPreferenceDialogFragment extends PreferenceDialogFragment {
+public class EditTextPreferenceDialogFragment extends PreferenceDialogFragment implements
+ TextView.OnEditorActionListener {
private static final String SAVE_STATE_TEXT = "EditTextPreferenceDialogFragment.text";
private EditText mEditText;
-
private CharSequence mText;
+ private boolean mAllowEnterToSubmit = true;
/**
* Returns a new instance of {@link EditTextPreferenceDialogFragment} for the {@link
@@ -82,6 +87,10 @@
mEditText.requestFocus();
mEditText.setText(mText);
+ mEditText.setInputType(InputType.TYPE_CLASS_TEXT);
+ mEditText.setImeOptions(EditorInfo.IME_ACTION_DONE);
+ mEditText.setOnEditorActionListener(this);
+
// Place cursor at the end
mEditText.setSelection(mEditText.getText().length());
}
@@ -105,4 +114,26 @@
}
}
+ /** Allows enabling and disabling the ability to press enter to dismiss the dialog. */
+ public void setAllowEnterToSubmit(boolean isAllowed) {
+ mAllowEnterToSubmit = isAllowed;
+ }
+
+ /** Allows verifying if enter to submit is currently enabled. */
+ public boolean getAllowEnterToSubmit() {
+ return mAllowEnterToSubmit;
+ }
+
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (actionId == EditorInfo.IME_ACTION_DONE && mAllowEnterToSubmit) {
+ CharSequence newValue = v.getText();
+
+ getEditTextPreference().callChangeListener(newValue);
+ dismiss();
+
+ return true;
+ }
+ return false;
+ }
}
diff --git a/car-ui-lib/src/com/android/car/ui/preference/ListPreferenceDialogFragment.java b/car-ui-lib/src/com/android/car/ui/preference/ListPreferenceDialogFragment.java
index 9395fbf..4af62ef 100644
--- a/car-ui-lib/src/com/android/car/ui/preference/ListPreferenceDialogFragment.java
+++ b/car-ui-lib/src/com/android/car/ui/preference/ListPreferenceDialogFragment.java
@@ -107,7 +107,7 @@
setStyle(DialogFragment.STYLE_NORMAL, R.style.Preference_CarUi_ListPreference);
LayoutInflater inflater = LayoutInflater.from(getContext());
- mDialogView = inflater.inflate(R.layout.car_ui_list_preference_dialog, null);
+ mDialogView = inflater.inflate(R.layout.car_ui_list_preference, null);
Toolbar toolbar = mDialogView.findViewById(R.id.toolbar);
toolbar.registerOnBackListener(this);
diff --git a/car-ui-lib/src/com/android/car/ui/preference/ListPreferenceFragment.java b/car-ui-lib/src/com/android/car/ui/preference/ListPreferenceFragment.java
new file mode 100644
index 0000000..d0653dd
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/preference/ListPreferenceFragment.java
@@ -0,0 +1,178 @@
+/*
+ * 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.ui.preference;
+
+import static com.android.car.ui.preference.PreferenceDialogFragment.ARG_KEY;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.preference.DialogPreference;
+import androidx.preference.ListPreference;
+import androidx.preference.Preference;
+
+import com.android.car.ui.R;
+import com.android.car.ui.recyclerview.CarUiRecyclerView;
+import com.android.car.ui.toolbar.Toolbar;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A fragment that provides a layout with a list of options associated with a {@link
+ * ListPreference}.
+ */
+public class ListPreferenceFragment extends Fragment implements
+ CarUiRecyclerViewRadioButtonAdapter.OnRadioButtonClickedListener {
+
+ private CarUiRecyclerView mCarUiRecyclerView;
+ private ListPreference mPreference;
+ private int mClickedDialogEntryIndex;
+ private CharSequence[] mEntryValues;
+
+ /**
+ * Returns a new instance of {@link ListPreferenceFragment} for the {@link ListPreference} with
+ * the given {@code key}.
+ */
+ public static ListPreferenceFragment newInstance(String key) {
+ ListPreferenceFragment fragment = new ListPreferenceFragment();
+ Bundle b = new Bundle(/* capacity= */ 1);
+ b.putString(ARG_KEY, key);
+ fragment.setArguments(b);
+ return fragment;
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ @NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.car_ui_list_preference, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ mCarUiRecyclerView = view.findViewById(R.id.radio_list);
+ final Toolbar toolbar = view.findViewById(R.id.toolbar);
+ if (mCarUiRecyclerView == null) {
+ throw new IllegalStateException(
+ "ListPreference layout did not contain recycler view with expected id.");
+ }
+
+ if (toolbar == null) {
+ throw new IllegalStateException(
+ "ListPreference layout did not contain toolbar with expected id.");
+ }
+
+ mCarUiRecyclerView.setPadding(0, toolbar.getHeight(), 0, 0);
+ toolbar.registerToolbarHeightChangeListener(newHeight -> {
+ if (mCarUiRecyclerView.getPaddingTop() == newHeight) {
+ return;
+ }
+
+ int oldHeight = mCarUiRecyclerView.getPaddingTop();
+ mCarUiRecyclerView.setPadding(0, newHeight, 0, 0);
+ mCarUiRecyclerView.scrollBy(0, oldHeight - newHeight);
+ });
+
+ mCarUiRecyclerView.setClipToPadding(false);
+ ListPreference preference = getListPreference();
+ toolbar.setTitle(mPreference.getTitle());
+
+ CharSequence[] entries = preference.getEntries();
+ mEntryValues = preference.getEntryValues();
+
+ if (entries == null || mEntryValues == null) {
+ throw new IllegalStateException(
+ "ListPreference requires an entries array and an entryValues array.");
+ }
+
+ if (entries.length != mEntryValues.length) {
+ throw new IllegalStateException(
+ "ListPreference entries array length does not match entryValues array length.");
+ }
+
+ mClickedDialogEntryIndex = preference.findIndexOfValue(preference.getValue());
+ List<String> entryStrings = new ArrayList<>(entries.length);
+ for (CharSequence entry : entries) {
+ entryStrings.add(entry.toString());
+ }
+
+ CarUiRecyclerViewRadioButtonAdapter adapter = new CarUiRecyclerViewRadioButtonAdapter(
+ entryStrings, mClickedDialogEntryIndex);
+ mCarUiRecyclerView.setAdapter(adapter);
+ adapter.registerListener(this);
+ }
+
+ private ListPreference getListPreference() {
+ if (mPreference == null && getArguments() != null) {
+ String key = getArguments().getString(ARG_KEY);
+ DialogPreference.TargetFragment fragment =
+ (DialogPreference.TargetFragment) getTargetFragment();
+
+ if (key == null) {
+ throw new IllegalStateException(
+ "ListPreference key not found in Fragment arguments");
+ }
+
+ if (fragment == null) {
+ throw new IllegalStateException(
+ "Target fragment must be registered before displaying ListPreference "
+ + "screen.");
+ }
+
+ Preference preference = fragment.findPreference(key);
+
+ if (!(preference instanceof ListPreference)) {
+ throw new IllegalStateException(
+ "Cannot use ListPreferenceFragment with a preference that is not of type "
+ + "ListPreference");
+ }
+
+ mPreference = (ListPreference) preference;
+ }
+ return mPreference;
+ }
+
+ @Override
+ public void onClick(int position) {
+ if (position < 0 || position > mEntryValues.length - 1) {
+ throw new IllegalStateException(
+ "Clicked preference has invalid index.");
+ }
+
+ mClickedDialogEntryIndex = position;
+ String value = mEntryValues[mClickedDialogEntryIndex].toString();
+ ListPreference preference = getListPreference();
+ if (preference.callChangeListener(value)) {
+ preference.setValue(value);
+ }
+
+ if (getActivity() == null) {
+ throw new IllegalStateException(
+ "ListPreference fragment is not attached to an Activity.");
+ }
+
+ getActivity().getSupportFragmentManager().popBackStack();
+ }
+}
diff --git a/car-ui-lib/src/com/android/car/ui/preference/PreferenceDialogFragment.java b/car-ui-lib/src/com/android/car/ui/preference/PreferenceDialogFragment.java
index 9e87838..8b2e79e 100644
--- a/car-ui-lib/src/com/android/car/ui/preference/PreferenceDialogFragment.java
+++ b/car-ui-lib/src/com/android/car/ui/preference/PreferenceDialogFragment.java
@@ -17,25 +17,13 @@
package com.android.car.ui.preference;
import android.app.AlertDialog;
-import android.app.Dialog;
-import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
-import android.text.TextUtils;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.Window;
-import android.view.WindowManager;
-import android.widget.TextView;
-import androidx.annotation.CallSuper;
-import androidx.annotation.LayoutRes;
-import androidx.annotation.NonNull;
-import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.preference.DialogPreference;
import androidx.preference.PreferenceFragmentCompat;
@@ -47,37 +35,20 @@
* the initial call to {@link #onCreate(Bundle)} and saved/restored in the saved instance state.
* Custom subclasses should also follow this pattern.
*
- * <p>Note: this is borrowed as-is from androidx.preference.PreferenceDialogFragmentCompat with
- * updates to formatting to match the project style. Automotive applications should use children of
- * this fragment in order to launch the system themed platform {@link AlertDialog} instead of the
- * one in the support library.
+ * <p>Note: this has the same functionality and interface as {@link
+ * androidx.preference.PreferenceDialogFragmentCompat} with updates to formatting to match the
+ * project style. This class preserves the {@link DialogPreference.TargetFragment} interface
+ * requirement that was removed in {@link CarUiDialogFragment}. Automotive applications should use
+ * children of this fragment in order to launch the system themed platform {@link AlertDialog}
+ * instead of the one in the support library.
*/
-public abstract class PreferenceDialogFragment extends DialogFragment implements
+public abstract class PreferenceDialogFragment extends CarUiDialogFragment implements
DialogInterface.OnClickListener {
protected static final String ARG_KEY = "key";
- private static final String SAVE_STATE_TITLE = "PreferenceDialogFragment.title";
- private static final String SAVE_STATE_POSITIVE_TEXT = "PreferenceDialogFragment.positiveText";
- private static final String SAVE_STATE_NEGATIVE_TEXT = "PreferenceDialogFragment.negativeText";
- private static final String SAVE_STATE_MESSAGE = "PreferenceDialogFragment.message";
- private static final String SAVE_STATE_LAYOUT = "PreferenceDialogFragment.layout";
- private static final String SAVE_STATE_ICON = "PreferenceDialogFragment.icon";
-
private DialogPreference mPreference;
- private CharSequence mDialogTitle;
- private CharSequence mPositiveButtonText;
- private CharSequence mNegativeButtonText;
- private CharSequence mDialogMessage;
- @LayoutRes
- private int mDialogLayoutRes;
-
- private BitmapDrawable mDialogIcon;
-
- /** Which button was clicked. */
- private int mWhichButtonClicked;
-
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -111,66 +82,9 @@
icon.draw(canvas);
mDialogIcon = new BitmapDrawable(getResources(), bitmap);
}
- } else {
- mDialogTitle = savedInstanceState.getCharSequence(SAVE_STATE_TITLE);
- mPositiveButtonText = savedInstanceState.getCharSequence(SAVE_STATE_POSITIVE_TEXT);
- mNegativeButtonText = savedInstanceState.getCharSequence(SAVE_STATE_NEGATIVE_TEXT);
- mDialogMessage = savedInstanceState.getCharSequence(SAVE_STATE_MESSAGE);
- mDialogLayoutRes = savedInstanceState.getInt(SAVE_STATE_LAYOUT, 0);
- Bitmap bitmap = savedInstanceState.getParcelable(SAVE_STATE_ICON);
- if (bitmap != null) {
- mDialogIcon = new BitmapDrawable(getResources(), bitmap);
- }
}
}
- @Override
- public void onSaveInstanceState(@NonNull Bundle outState) {
- super.onSaveInstanceState(outState);
-
- outState.putCharSequence(SAVE_STATE_TITLE, mDialogTitle);
- outState.putCharSequence(SAVE_STATE_POSITIVE_TEXT, mPositiveButtonText);
- outState.putCharSequence(SAVE_STATE_NEGATIVE_TEXT, mNegativeButtonText);
- outState.putCharSequence(SAVE_STATE_MESSAGE, mDialogMessage);
- outState.putInt(SAVE_STATE_LAYOUT, mDialogLayoutRes);
- if (mDialogIcon != null) {
- outState.putParcelable(SAVE_STATE_ICON, mDialogIcon.getBitmap());
- }
- }
-
- @Override
- @NonNull
- public Dialog onCreateDialog(Bundle savedInstanceState) {
- Context context = getActivity();
- mWhichButtonClicked = DialogInterface.BUTTON_NEGATIVE;
-
- AlertDialog.Builder builder = new AlertDialog.Builder(context)
- .setTitle(mDialogTitle)
- .setIcon(mDialogIcon)
- .setPositiveButton(mPositiveButtonText, this)
- .setNegativeButton(mNegativeButtonText, this);
-
- View contentView = onCreateDialogView(context);
- if (contentView != null) {
- onBindDialogView(contentView);
- builder.setView(contentView);
- } else {
- builder.setMessage(mDialogMessage);
- }
-
- onPrepareDialogBuilder(builder);
-
- // Create the dialog
- Dialog dialog = builder.create();
- if (needInputMethod()) {
- // Request input only after the dialog is shown. This is to prevent an issue where the
- // dialog view collapsed the content on small displays.
- dialog.setOnShowListener(d -> requestInputMethod(dialog));
- }
-
- return dialog;
- }
-
/**
* Get the preference that requested this dialog. Available after {@link #onCreate(Bundle)} has
* been called on the {@link PreferenceFragmentCompat} which launched this dialog.
@@ -186,99 +100,4 @@
}
return mPreference;
}
-
- /**
- * Prepares the dialog builder to be shown when the preference is clicked. Use this to set
- * custom properties on the dialog.
- *
- * <p>Do not {@link AlertDialog.Builder#create()} or {@link AlertDialog.Builder#show()}.
- */
- protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
- }
-
- /**
- * Returns whether the preference needs to display a soft input method when the dialog is
- * displayed. Default is false. Subclasses should override this method if they need the soft
- * input method brought up automatically.
- *
- * <p>Note: Ensure your subclass manually requests focus (ideally in {@link
- * #onBindDialogView(View)}) for the input field in order to
- * correctly attach the input method to the field.
- */
- protected boolean needInputMethod() {
- return false;
- }
-
- /**
- * Sets the required flags on the dialog window to enable input method window to show up.
- */
- private void requestInputMethod(Dialog dialog) {
- Window window = dialog.getWindow();
- window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
- }
-
- /**
- * Creates the content view for the dialog (if a custom content view is required). By default,
- * it inflates the dialog layout resource if it is set.
- *
- * @return the content View for the dialog.
- * @see DialogPreference#setLayoutResource(int)
- */
- protected View onCreateDialogView(Context context) {
- int resId = mDialogLayoutRes;
- if (resId == 0) {
- return null;
- }
-
- LayoutInflater inflater = LayoutInflater.from(context);
- return inflater.inflate(resId, null);
- }
-
- /**
- * Binds views in the content View of the dialog to data.
- *
- * <p>Make sure to call through to the superclass implementation.
- *
- * @param view the content View of the dialog, if it is custom.
- */
- @CallSuper
- protected void onBindDialogView(View view) {
- View dialogMessageView = view.findViewById(android.R.id.message);
-
- if (dialogMessageView != null) {
- CharSequence message = mDialogMessage;
- int newVisibility = View.GONE;
-
- if (!TextUtils.isEmpty(message)) {
- if (dialogMessageView instanceof TextView) {
- ((TextView) dialogMessageView).setText(message);
- }
-
- newVisibility = View.VISIBLE;
- }
-
- if (dialogMessageView.getVisibility() != newVisibility) {
- dialogMessageView.setVisibility(newVisibility);
- }
- }
- }
-
- @Override
- public void onClick(DialogInterface dialog, int which) {
- mWhichButtonClicked = which;
- }
-
- @Override
- public void onDismiss(DialogInterface dialog) {
- super.onDismiss(dialog);
- onDialogClosed(mWhichButtonClicked == DialogInterface.BUTTON_POSITIVE);
- }
-
- /**
- * Called when the dialog is dismissed.
- *
- * @param positiveResult {@code true} if the dialog was dismissed with {@link
- * DialogInterface#BUTTON_POSITIVE}.
- */
- protected abstract void onDialogClosed(boolean positiveResult);
}
diff --git a/car-ui-lib/src/com/android/car/ui/preference/PreferenceFragment.java b/car-ui-lib/src/com/android/car/ui/preference/PreferenceFragment.java
index 5582ffc..1acb2d2 100644
--- a/car-ui-lib/src/com/android/car/ui/preference/PreferenceFragment.java
+++ b/car-ui-lib/src/com/android/car/ui/preference/PreferenceFragment.java
@@ -16,31 +16,50 @@
package com.android.car.ui.preference;
+import android.content.Context;
import android.os.Bundle;
+import android.util.Log;
+import android.util.Pair;
import android.view.View;
+import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.Fragment;
+import androidx.preference.DialogPreference;
+import androidx.preference.DropDownPreference;
import androidx.preference.EditTextPreference;
import androidx.preference.ListPreference;
import androidx.preference.MultiSelectListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
+import androidx.preference.PreferenceGroup;
+import androidx.preference.PreferenceScreen;
import androidx.recyclerview.widget.RecyclerView;
import com.android.car.ui.R;
import com.android.car.ui.toolbar.Toolbar;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
/**
* A PreferenceFragmentCompat is the entry point to using the Preference library.
*
- * <p>Note: this is borrowed as-is from androidx.preference.PreferenceFragmentCompat with updates to
- * launch Car UI library {@link DialogFragment}. Automotive applications should use children of
- * this fragment in order to launch the system themed {@link DialogFragment}.
+ * <p>Using this fragment will replace regular Preferences with CarUi equivalents. Because of this,
+ * certain properties that cannot be read out of Preferences will be lost upon calling
+ * {@link #setPreferenceScreen(PreferenceScreen)}. These include the preference viewId,
+ * defaultValue, and enabled state.
*/
public abstract class PreferenceFragment extends PreferenceFragmentCompat {
+ private static final String TAG = "CarUiPreferenceFragment";
private static final String DIALOG_FRAGMENT_TAG =
"com.android.car.ui.PreferenceFragment.DIALOG";
@@ -92,11 +111,11 @@
return;
}
- final DialogFragment f;
+ final Fragment f;
if (preference instanceof EditTextPreference) {
f = EditTextPreferenceDialogFragment.newInstance(preference.getKey());
} else if (preference instanceof ListPreference) {
- f = ListPreferenceDialogFragment.newInstance(preference.getKey());
+ f = ListPreferenceFragment.newInstance(preference.getKey());
} else if (preference instanceof MultiSelectListPreference) {
f = MultiSelectListPreferenceDialogFragment.newInstance(preference.getKey());
} else {
@@ -106,7 +125,197 @@
+ ". Make sure to implement onPreferenceDisplayDialog() to handle "
+ "displaying a custom dialog for this Preference.");
}
+
f.setTargetFragment(this, 0);
- f.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
+
+ if (f instanceof DialogFragment) {
+ ((DialogFragment) f).show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
+ } else {
+ if (getActivity() == null) {
+ throw new IllegalStateException(
+ "Preference fragment is not attached to an Activity.");
+ }
+
+ if (getView() == null) {
+ throw new IllegalStateException(
+ "Preference fragment must have a layout.");
+ }
+
+ getActivity().getSupportFragmentManager().beginTransaction()
+ .replace(((ViewGroup) getView().getParent()).getId(), f)
+ .addToBackStack(null)
+ .commit();
+ }
+ }
+
+ /**
+ * This override of setPreferenceScreen replaces preferences with their CarUi versions first.
+ */
+ @Override
+ public void setPreferenceScreen(PreferenceScreen preferenceScreen) {
+ // We do a search of the tree and every time we see a PreferenceGroup we remove
+ // all it's children, replace them with CarUi versions, and then re-add them
+
+ Map<Preference, String> dependencies = new HashMap<>();
+ List<Preference> children = new ArrayList<>();
+
+ // Stack of preferences to process
+ Deque<Preference> stack = new ArrayDeque<>();
+ stack.addFirst(preferenceScreen);
+
+ while (!stack.isEmpty()) {
+ Preference preference = stack.removeFirst();
+
+ if (preference instanceof PreferenceGroup) {
+ PreferenceGroup pg = (PreferenceGroup) preference;
+
+ children.clear();
+ for (int i = 0; i < pg.getPreferenceCount(); i++) {
+ children.add(pg.getPreference(i));
+ }
+
+ pg.removeAll();
+
+ for (Preference child : children) {
+ Preference replacement = getReplacementFor(child);
+
+ dependencies.put(replacement, child.getDependency());
+ pg.addPreference(replacement);
+ stack.addFirst(replacement);
+ }
+ }
+ }
+
+ super.setPreferenceScreen(preferenceScreen);
+
+ // Set the dependencies after all the swapping has been done and they've been
+ // associated with this fragment, or we could potentially fail to find preferences
+ // or use the wrong preferenceManager
+ for (Map.Entry<Preference, String> entry : dependencies.entrySet()) {
+ entry.getKey().setDependency(entry.getValue());
+ }
+ }
+
+ // Mapping from regular preferences to CarUi preferences.
+ // Order is important, subclasses must come before their base classes
+ private static final List<Pair<Class<? extends Preference>, Class<? extends Preference>>>
+ sPreferenceMapping = Arrays.asList(
+ new Pair<>(DropDownPreference.class, CarUiDropDownPreference.class),
+ new Pair<>(ListPreference.class, CarUiListPreference.class),
+ new Pair<>(MultiSelectListPreference.class, CarUiMultiSelectListPreference.class),
+ new Pair<>(EditTextPreference.class, CarUiEditTextPreference.class),
+ new Pair<>(Preference.class, CarUiPreference.class)
+ );
+
+ /**
+ * Gets the CarUi version of the passed in preference. If there is no suitable replacement, this
+ * method will return it's input.
+ *
+ * <p>When given a Preference that extends a replaceable preference, we log a warning instead
+ * of replacing it so that we don't remove any functionality.
+ */
+ private static Preference getReplacementFor(Preference preference) {
+ Class<? extends Preference> clazz = preference.getClass();
+
+ for (Pair<Class<? extends Preference>, Class<? extends Preference>> replacement
+ : sPreferenceMapping) {
+ Class<? extends Preference> source = replacement.first;
+ Class<? extends Preference> target = replacement.second;
+ if (source.isAssignableFrom(clazz)) {
+ if (clazz == source) {
+ try {
+ return copyPreference(preference, (Preference) target
+ .getDeclaredConstructor(Context.class)
+ .newInstance(preference.getContext()));
+ } catch (ReflectiveOperationException e) {
+ throw new RuntimeException(e);
+ }
+ } else if (clazz == target || source == Preference.class) {
+ // Don't warn about subclasses of Preference because there are many legitimate
+ // uses for non-carui Preference subclasses, like Preference groups.
+ return preference;
+ } else {
+ Log.w(TAG, "Subclass of " + source.getSimpleName() + " was used, "
+ + "preventing us from substituting it with " + target.getSimpleName());
+ return preference;
+ }
+ }
+ }
+
+ return preference;
+ }
+
+ /**
+ * Copies all the properties of one preference to another.
+ *
+ * @return the {@code to} parameter
+ */
+ private static Preference copyPreference(Preference from, Preference to) {
+ // viewId and defaultValue don't have getters
+ // isEnabled() is not completely symmetrical with setEnabled(), so we can't use it.
+ to.setTitle(from.getTitle());
+ to.setOnPreferenceClickListener(from.getOnPreferenceClickListener());
+ to.setOnPreferenceChangeListener(from.getOnPreferenceChangeListener());
+ to.setIcon(from.getIcon());
+ to.setFragment(from.getFragment());
+ to.setIntent(from.getIntent());
+ to.setKey(from.getKey());
+ to.setOrder(from.getOrder());
+ to.setSelectable(from.isSelectable());
+ to.setPersistent(from.isPersistent());
+ to.setIconSpaceReserved(from.isIconSpaceReserved());
+ to.setWidgetLayoutResource(from.getWidgetLayoutResource());
+ to.setPreferenceDataStore(from.getPreferenceDataStore());
+ to.setShouldDisableView(from.getShouldDisableView());
+ to.setSingleLineTitle(from.isSingleLineTitle());
+ to.setVisible(from.isVisible());
+ to.setLayoutResource(from.getLayoutResource());
+ to.setCopyingEnabled(from.isCopyingEnabled());
+
+ if (from.getSummaryProvider() != null) {
+ to.setSummaryProvider(from.getSummaryProvider());
+ } else {
+ to.setSummary(from.getSummary());
+ }
+
+ if (from.peekExtras() != null) {
+ to.getExtras().putAll(from.peekExtras());
+ }
+
+ if (from instanceof DialogPreference) {
+ DialogPreference fromDialog = (DialogPreference) from;
+ DialogPreference toDialog = (DialogPreference) to;
+ toDialog.setDialogTitle(fromDialog.getDialogTitle());
+ toDialog.setDialogIcon(fromDialog.getDialogIcon());
+ toDialog.setDialogMessage(fromDialog.getDialogMessage());
+ toDialog.setDialogLayoutResource(fromDialog.getDialogLayoutResource());
+ toDialog.setNegativeButtonText(fromDialog.getNegativeButtonText());
+ toDialog.setPositiveButtonText(fromDialog.getPositiveButtonText());
+ }
+
+ // DropDownPreference extends ListPreference and doesn't add any extra api surface,
+ // so we don't need a case for it
+ if (from instanceof ListPreference) {
+ ListPreference fromList = (ListPreference) from;
+ ListPreference toList = (ListPreference) to;
+ toList.setEntries(fromList.getEntries());
+ toList.setEntryValues(fromList.getEntryValues());
+ toList.setValue(fromList.getValue());
+ } else if (from instanceof EditTextPreference) {
+ EditTextPreference fromText = (EditTextPreference) from;
+ EditTextPreference toText = (EditTextPreference) to;
+ toText.setText(fromText.getText());
+ } else if (from instanceof MultiSelectListPreference) {
+ MultiSelectListPreference fromMulti = (MultiSelectListPreference) from;
+ MultiSelectListPreference toMulti = (MultiSelectListPreference) to;
+ toMulti.setEntries(fromMulti.getEntries());
+ toMulti.setEntryValues(fromMulti.getEntryValues());
+ toMulti.setValues(fromMulti.getValues());
+ }
+
+ // We don't need to add checks for things that we will never replace,
+ // like PreferenceGroup or CheckBoxPreference
+
+ return to;
}
}
diff --git a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiListItemLayoutManager.java b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiListItemLayoutManager.java
index c825052..80572a5 100644
--- a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiListItemLayoutManager.java
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiListItemLayoutManager.java
@@ -133,7 +133,7 @@
}
}
- return (mListItemHeights.get(firstPos)) + heightOfScreen;
+ return mListItemHeights.get(firstPos) + heightOfScreen;
}
/**
diff --git a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiRecyclerView.java b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiRecyclerView.java
index 73f5498..0b608a2 100644
--- a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiRecyclerView.java
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiRecyclerView.java
@@ -132,7 +132,7 @@
int START = 0;
/** Position scrollbar to the right of the screen. */
- int END = 2;
+ int END = 1;
}
/**
diff --git a/car-ui-lib/src/com/android/car/ui/toolbar/MenuItem.java b/car-ui-lib/src/com/android/car/ui/toolbar/MenuItem.java
index fc27679..7bfad8d 100644
--- a/car-ui-lib/src/com/android/car/ui/toolbar/MenuItem.java
+++ b/car-ui-lib/src/com/android/car/ui/toolbar/MenuItem.java
@@ -26,6 +26,8 @@
import com.android.car.ui.R;
import com.android.car.ui.utils.CarUxRestrictionsUtil;
+import java.lang.ref.WeakReference;
+
/**
* Represents a button to display in the {@link Toolbar}.
*
@@ -38,8 +40,8 @@
* itself, or it's overflow menu.
*
* <p>If you require a search or settings button, you should use
- * {@link Builder#createSearch(Context, OnClickListener)} or
- * {@link Builder#createSettings(Context, OnClickListener)}.
+ * {@link Builder#setToSearch()} or
+ * {@link Builder#setToSettings()}.
*
* <p>Some properties can be changed after the creating a MenuItem, but others require being set
* with a {@link Builder}.
@@ -57,7 +59,10 @@
private int mId;
private CarUxRestrictions mCurrentRestrictions;
- private Listener mListener;
+ // This is a WeakReference to allow the Toolbar (and by extension, the whole screen
+ // the toolbar is on) to be garbage-collected if the MenuItem is held past the
+ // lifecycle of the toolbar.
+ private WeakReference<Listener> mListener = new WeakReference<>(null);
private CharSequence mTitle;
private Drawable mIcon;
private OnClickListener mOnClickListener;
@@ -89,14 +94,16 @@
}
private void update() {
- if (mListener != null) {
- mListener.onMenuItemChanged();
+ Listener listener = mListener.get();
+ if (listener != null) {
+ listener.onMenuItemChanged();
}
}
/** Sets the id, which is purely for the client to distinguish MenuItems with. */
public void setId(int id) {
mId = id;
+ update();
}
/** Gets the id, which is purely for the client to distinguish MenuItems with. */
@@ -290,17 +297,15 @@
return mIsSearch;
}
- /**
- * Builder class.
- *
- * <p>Use the static {@link #createSearch(Context, OnClickListener)} or
- * {@link #createSettings(Context, OnClickListener)} if you want one of those specialized
- * buttons.
- */
+ /** Builder class */
public static final class Builder {
- private Context mContext;
+ private final Context mContext;
+ private final CharSequence mSearchTitle;
+ private final CharSequence mSettingsTitle;
+ private final Drawable mSearchIcon;
+ private final Drawable mSettingsIcon;
- private int mId;
+ private int mId = View.NO_ID;
private CharSequence mTitle;
private Drawable mIcon;
private OnClickListener mOnClickListener;
@@ -314,11 +319,18 @@
private boolean mIsActivatable = false;
private boolean mIsActivated = false;
private boolean mIsSearch = false;
+ private boolean mIsSettings = false;
@CarUxRestrictions.CarUxRestrictionsInfo
private int mUxRestrictions = CarUxRestrictions.UX_RESTRICTIONS_BASELINE;
public Builder(Context c) {
- mContext = c;
+ // Must use getApplicationContext to avoid leaking activities when the MenuItem
+ // is held onto for longer than the Activity's lifecycle
+ mContext = c.getApplicationContext();
+ mSearchTitle = mContext.getString(R.string.car_ui_toolbar_menu_item_search_title);
+ mSettingsTitle = mContext.getString(R.string.car_ui_toolbar_menu_item_settings_title);
+ mSearchIcon = mContext.getDrawable(R.drawable.car_ui_icon_search);
+ mSettingsIcon = mContext.getDrawable(R.drawable.car_ui_icon_settings);
}
/** Builds a {@link MenuItem} from the current state of the Builder */
@@ -332,6 +344,30 @@
|| mIsActivatable)) {
throw new IllegalStateException("Unsupported options for a checkable MenuItem");
}
+ if (mIsSearch && mIsSettings) {
+ throw new IllegalStateException("Can't have both a search and settings MenuItem");
+ }
+
+ if (mIsSearch && (!mSearchTitle.equals(mTitle)
+ || !mSearchIcon.equals(mIcon)
+ || mIsCheckable
+ || mIsActivatable
+ || !mIsTinted
+ || mShowIconAndTitle
+ || mDisplayBehavior != DisplayBehavior.ALWAYS)) {
+ throw new IllegalStateException("Invalid search MenuItem");
+ }
+
+ if (mIsSettings && (!mSettingsTitle.equals(mTitle)
+ || !mSettingsIcon.equals(mIcon)
+ || mIsCheckable
+ || mIsActivatable
+ || !mIsTinted
+ || mShowIconAndTitle
+ || mDisplayBehavior != DisplayBehavior.ALWAYS
+ || mUxRestrictions != CarUxRestrictions.UX_RESTRICTIONS_NO_SETUP)) {
+ throw new IllegalStateException("Invalid settings MenuItem");
+ }
return new MenuItem(this);
}
@@ -477,26 +513,20 @@
return this;
}
- /** Sets that this is the search MenuItem, which has special behavior while searching */
- private Builder setSearch() {
- mIsSearch = true;
- return this;
- }
-
/**
* Creates a search MenuItem.
*
* <p>The advantage of using this over creating your own is getting an OEM-styled search
* icon, and this button will always disappear while searching, even when the
* {@link Toolbar Toolbar's} showMenuItemsWhileSearching is true.
+ *
+ * <p>If using this, you should only change the id, visibility, or onClickListener.
*/
- public static MenuItem createSearch(Context c, OnClickListener listener) {
- return new Builder(c)
- .setTitle(R.string.car_ui_toolbar_menu_item_search_title)
- .setIcon(R.drawable.car_ui_icon_search)
- .setOnClickListener(listener)
- .setSearch()
- .build();
+ public Builder setToSearch() {
+ mIsSearch = true;
+ setTitle(mSearchTitle);
+ setIcon(mSearchIcon);
+ return this;
}
/**
@@ -505,13 +535,32 @@
* <p>The advantage of this over creating your own is getting an OEM-styled settings icon,
* and that the MenuItem will be restricted based on
* {@link CarUxRestrictions#UX_RESTRICTIONS_NO_SETUP}
+ *
+ * <p>If using this, you should only change the id, visibility, or onClickListener.
*/
- public static MenuItem createSettings(Context c, OnClickListener listener) {
- return new Builder(c)
- .setTitle(R.string.car_ui_toolbar_menu_item_settings_title)
- .setIcon(R.drawable.car_ui_icon_settings)
+ public Builder setToSettings() {
+ mIsSettings = true;
+ setTitle(mSettingsTitle);
+ setIcon(mSettingsIcon);
+ setUxRestrictions(CarUxRestrictions.UX_RESTRICTIONS_NO_SETUP);
+ return this;
+ }
+
+ /** @deprecated Use {@link #setToSearch()} instead. */
+ @Deprecated
+ public static MenuItem createSearch(Context c, OnClickListener listener) {
+ return MenuItem.builder(c)
+ .setToSearch()
.setOnClickListener(listener)
- .setUxRestrictions(CarUxRestrictions.UX_RESTRICTIONS_NO_SETUP)
+ .build();
+ }
+
+ /** @deprecated Use {@link #setToSettings()} instead. */
+ @Deprecated
+ public static MenuItem createSettings(Context c, OnClickListener listener) {
+ return MenuItem.builder(c)
+ .setToSettings()
+ .setOnClickListener(listener)
.build();
}
}
@@ -545,7 +594,12 @@
void onMenuItemChanged();
}
+ /**
+ * Sets a listener for changes to this MenuItem. Note that the MenuItem will only hold
+ * weak references to the Listener, so that the listener is not held if the MenuItem
+ * outlives the toolbar.
+ */
void setListener(Listener listener) {
- mListener = listener;
+ mListener = new WeakReference<>(listener);
}
}
diff --git a/car-ui-lib/src/com/android/car/ui/toolbar/MenuItemRenderer.java b/car-ui-lib/src/com/android/car/ui/toolbar/MenuItemRenderer.java
index 890fa0b..4254bdf 100644
--- a/car-ui-lib/src/com/android/car/ui/toolbar/MenuItemRenderer.java
+++ b/car-ui-lib/src/com/android/car/ui/toolbar/MenuItemRenderer.java
@@ -110,6 +110,8 @@
return;
}
+ mView.setId(mMenuItem.getId());
+
boolean hasIcon = mMenuItem.getIcon() != null;
boolean hasText = !TextUtils.isEmpty(mMenuItem.getTitle());
boolean textAndIcon = mMenuItem.isShowingIconAndTitle();
@@ -208,9 +210,11 @@
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.CarUiToolbarMenuItem);
try {
- int id = a.getResourceId(R.styleable.CarUiToolbarMenuItem_id, 0);
+ int id = a.getResourceId(R.styleable.CarUiToolbarMenuItem_id, View.NO_ID);
String title = a.getString(R.styleable.CarUiToolbarMenuItem_title);
Drawable icon = a.getDrawable(R.styleable.CarUiToolbarMenuItem_icon);
+ boolean isSearch = a.getBoolean(R.styleable.CarUiToolbarMenuItem_search, false);
+ boolean isSettings = a.getBoolean(R.styleable.CarUiToolbarMenuItem_settings, false);
boolean tinted = a.getBoolean(R.styleable.CarUiToolbarMenuItem_tinted, true);
boolean visible = a.getBoolean(R.styleable.CarUiToolbarMenuItem_visible, true);
boolean showIconAndTitle = a.getBoolean(
@@ -265,6 +269,14 @@
.setShowIconAndTitle(showIconAndTitle)
.setDisplayBehavior(displayBehavior);
+ if (isSearch) {
+ builder.setToSearch();
+ }
+
+ if (isSettings) {
+ builder.setToSettings();
+ }
+
if (checkable || checkedExists) {
builder.setChecked(checked);
}
diff --git a/car-ui-lib/src/com/android/car/ui/toolbar/TabLayout.java b/car-ui-lib/src/com/android/car/ui/toolbar/TabLayout.java
index 9bbf3c8..b30c370 100644
--- a/car-ui-lib/src/com/android/car/ui/toolbar/TabLayout.java
+++ b/car-ui-lib/src/com/android/car/ui/toolbar/TabLayout.java
@@ -328,7 +328,7 @@
textView.setText(mText);
}
- /** Set icon drawable. */
+ /** Set icon drawable. TODO(b/139444064): revise this api.*/
protected void bindIcon(ImageView imageView) {
imageView.setImageDrawable(mIcon);
}
diff --git a/car-ui-lib/src/com/android/car/ui/toolbar/Toolbar.java b/car-ui-lib/src/com/android/car/ui/toolbar/Toolbar.java
index ea55518..c8899fb 100644
--- a/car-ui-lib/src/com/android/car/ui/toolbar/Toolbar.java
+++ b/car-ui-lib/src/com/android/car/ui/toolbar/Toolbar.java
@@ -32,6 +32,7 @@
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
+import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.annotation.DrawableRes;
@@ -147,6 +148,11 @@
private boolean mLogoFillsNavIconSpace;
private boolean mEatingTouch = false;
private boolean mEatingHover = false;
+ private ProgressBar mProgressBar;
+ private MenuItem.Listener mOverflowItemListener = () -> {
+ createOverflowDialog();
+ setState(getState());
+ };
public Toolbar(Context context) {
this(context, null);
@@ -202,6 +208,7 @@
mTitleLogoContainer = requireViewById(R.id.car_ui_toolbar_title_logo_container);
mTitleLogo = requireViewById(R.id.car_ui_toolbar_title_logo);
mSearchView = requireViewById(R.id.car_ui_toolbar_search_view);
+ mProgressBar = requireViewById(R.id.car_ui_toolbar_progress_bar);
mTitle.setText(a.getString(R.styleable.CarUiToolbar_title));
setLogo(a.getResourceId(R.styleable.CarUiToolbar_logo, 0));
@@ -600,10 +607,7 @@
for (MenuItem item : mMenuItems) {
if (item.getDisplayBehavior() == MenuItem.DisplayBehavior.NEVER) {
mOverflowItems.add(item);
- item.setListener(() -> {
- createOverflowDialog();
- setState(getState());
- });
+ item.setListener(mOverflowItemListener);
} else {
MenuItemRenderer renderer = new MenuItemRenderer(item, mMenuItemsContainer);
mMenuItemRenderers.add(renderer);
@@ -717,8 +721,7 @@
/**
* Set whether or not to show the {@link MenuItem MenuItems} while searching. Default false.
* Even if this is set to true, the {@link MenuItem} created by
- * {@link MenuItem.Builder#createSearch(Context, MenuItem.OnClickListener)} will still be
- * hidden.
+ * {@link MenuItem.Builder#setToSearch()} will still be hidden.
*/
public void setShowMenuItemsWhileSearching(boolean showMenuItems) {
mShowMenuItemsWhileSearching = showMenuItems;
@@ -919,4 +922,19 @@
public boolean unregisterOnBackListener(OnBackListener listener) {
return mOnBackListeners.remove(listener);
}
+
+ /** Shows the progress bar */
+ public void showProgressBar() {
+ mProgressBar.setVisibility(View.VISIBLE);
+ }
+
+ /** Hides the progress bar */
+ public void hideProgressBar() {
+ mProgressBar.setVisibility(View.GONE);
+ }
+
+ /** Returns the progress bar */
+ public ProgressBar getProgressBar() {
+ return mProgressBar;
+ }
}
diff --git a/car-ui-lib/src/com/android/car/ui/utils/CarUxRestrictionsUtil.java b/car-ui-lib/src/com/android/car/ui/utils/CarUxRestrictionsUtil.java
index af65780..31ac24a 100644
--- a/car-ui-lib/src/com/android/car/ui/utils/CarUxRestrictionsUtil.java
+++ b/car-ui-lib/src/com/android/car/ui/utils/CarUxRestrictionsUtil.java
@@ -27,6 +27,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import com.android.car.ui.R;
@@ -66,7 +67,7 @@
}
};
- mCarApi = Car.createCar(context);
+ mCarApi = Car.createCar(context.getApplicationContext());
mObservers = Collections.newSetFromMap(new WeakHashMap<>());
try {
@@ -154,4 +155,10 @@
return str;
}
+
+ /** Sets car UX restrictions. Only used for testing. */
+ @VisibleForTesting
+ public void setUxRestrictions(CarUxRestrictions carUxRestrictions) {
+ mCarUxRestrictions = carUxRestrictions;
+ }
}
diff --git a/car-ui-lib/tests/apitest/current.xml b/car-ui-lib/tests/apitest/current.xml
index 9bd897a..4180cae 100644
--- a/car-ui-lib/tests/apitest/current.xml
+++ b/car-ui-lib/tests/apitest/current.xml
@@ -2,6 +2,7 @@
<!--This file is AUTO GENERATED, DO NOT EDIT MANUALLY.-->
<resources>
<public type="attr" name="CarUiToolbarStyle"/>
+ <public type="attr" name="carUiPreferenceStyle"/>
<public type="attr" name="carUiRecyclerViewStyle"/>
<public type="attr" name="state_ux_restricted"/>
<public type="bool" name="car_ui_list_item_single_line_title"/>
@@ -44,7 +45,13 @@
<public type="dimen" name="car_ui_dialog_edittext_margin_end"/>
<public type="dimen" name="car_ui_dialog_edittext_margin_start"/>
<public type="dimen" name="car_ui_dialog_edittext_margin_top"/>
+ <public type="dimen" name="car_ui_dialog_icon_size"/>
+ <public type="dimen" name="car_ui_dialog_title_margin"/>
<public type="dimen" name="car_ui_header_list_item_height"/>
+ <public type="dimen" name="car_ui_keyline_1"/>
+ <public type="dimen" name="car_ui_keyline_2"/>
+ <public type="dimen" name="car_ui_keyline_3"/>
+ <public type="dimen" name="car_ui_keyline_4"/>
<public type="dimen" name="car_ui_letter_spacing_body1"/>
<public type="dimen" name="car_ui_letter_spacing_body3"/>
<public type="dimen" name="car_ui_list_item_action_divider_height"/>
@@ -54,6 +61,10 @@
<public type="dimen" name="car_ui_list_item_height"/>
<public type="dimen" name="car_ui_list_item_icon_container_width"/>
<public type="dimen" name="car_ui_list_item_icon_size"/>
+ <public type="dimen" name="car_ui_list_item_radio_button_end_inset"/>
+ <public type="dimen" name="car_ui_list_item_radio_button_height"/>
+ <public type="dimen" name="car_ui_list_item_radio_button_icon_container_width"/>
+ <public type="dimen" name="car_ui_list_item_radio_button_start_inset"/>
<public type="dimen" name="car_ui_list_item_start_inset"/>
<public type="dimen" name="car_ui_list_item_supplemental_icon_size"/>
<public type="dimen" name="car_ui_list_item_text_no_icon_start_margin"/>
@@ -183,8 +194,10 @@
<public type="style" name="Preference.CarUi.DialogPreference"/>
<public type="style" name="Preference.CarUi.DialogPreference.EditTextPreference"/>
<public type="style" name="Preference.CarUi.DropDown"/>
+ <public type="style" name="Preference.CarUi.Icon"/>
<public type="style" name="Preference.CarUi.Information"/>
<public type="style" name="Preference.CarUi.ListPreference"/>
+ <public type="style" name="Preference.CarUi.Preference"/>
<public type="style" name="Preference.CarUi.PreferenceScreen"/>
<public type="style" name="Preference.CarUi.SeekBarPreference"/>
<public type="style" name="Preference.CarUi.SwitchPreference"/>
@@ -192,6 +205,7 @@
<public type="style" name="PreferenceFragmentList.CarUi"/>
<public type="style" name="RadioButton.CarUi"/>
<public type="style" name="TextAppearance.CarUi"/>
+ <public type="style" name="TextAppearance.CarUi.AlertDialog.Subtitle"/>
<public type="style" name="TextAppearance.CarUi.ListItem"/>
<public type="style" name="TextAppearance.CarUi.ListItem.Body"/>
<public type="style" name="TextAppearance.CarUi.PreferenceCategoryTitle"/>
@@ -205,6 +219,10 @@
<public type="style" name="TextAppearance.CarUi.Widget.Toolbar.Title"/>
<public type="style" name="Theme.CarUi"/>
<public type="style" name="Widget.CarUi"/>
+ <public type="style" name="Widget.CarUi.AlertDialog"/>
+ <public type="style" name="Widget.CarUi.AlertDialog.HeaderContainer"/>
+ <public type="style" name="Widget.CarUi.AlertDialog.Icon"/>
+ <public type="style" name="Widget.CarUi.AlertDialog.TitleContainer"/>
<public type="style" name="Widget.CarUi.Button.Borderless.Colored"/>
<public type="style" name="Widget.CarUi.CarUiRecyclerView"/>
<public type="style" name="Widget.CarUi.CarUiRecyclerView.NestedRecyclerView"/>
@@ -216,6 +234,7 @@
<public type="style" name="Widget.CarUi.Toolbar.MenuItem.Container"/>
<public type="style" name="Widget.CarUi.Toolbar.NavIcon"/>
<public type="style" name="Widget.CarUi.Toolbar.NavIconContainer"/>
+ <public type="style" name="Widget.CarUi.Toolbar.ProgressBar"/>
<public type="style" name="Widget.CarUi.Toolbar.Search.CloseIcon"/>
<public type="style" name="Widget.CarUi.Toolbar.Search.EditText"/>
<public type="style" name="Widget.CarUi.Toolbar.Search.SearchIcon"/>
diff --git a/car-ui-lib/tests/paintbooth/build.gradle b/car-ui-lib/tests/paintbooth/build.gradle
index d8f89f6..0ef3514 100644
--- a/car-ui-lib/tests/paintbooth/build.gradle
+++ b/car-ui-lib/tests/paintbooth/build.gradle
@@ -45,6 +45,7 @@
dependencies {
implementation project(':')
+ debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0'
api 'androidx.annotation:annotation:1.1.0'
api 'androidx.constraintlayout:constraintlayout:1.1.3'
api 'androidx.recyclerview:recyclerview:1.0.0'
diff --git a/car-ui-lib/tests/paintbooth/res/layout/dialogs_activity.xml b/car-ui-lib/tests/paintbooth/res/layout/dialogs_activity.xml
deleted file mode 100644
index d94a16a..0000000
--- a/car-ui-lib/tests/paintbooth/res/layout/dialogs_activity.xml
+++ /dev/null
@@ -1,102 +0,0 @@
-<?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:id="@+id/dialog_layout"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="@drawable/car_ui_activity_background">
-
- <com.android.car.ui.toolbar.Toolbar
- android:id="@+id/toolbar"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- app:layout_constraintTop_toTopOf="parent"
- app:state="subpage"
- app:title="@string/app_name" />
-
- <Button
- android:id="@+id/show_dialog_bt"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/dialog_show_dialog"
- app:layout_constraintTop_toBottomOf="@+id/toolbar"
- app:layout_constraintBottom_toTopOf="@+id/show_dialog_with_textbox"
- app:layout_constraintLeft_toLeftOf="parent"
- app:layout_constraintRight_toRightOf="parent"/>
-
- <Button
- android:id="@+id/show_dialog_with_textbox"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/dialog_show_dialog_edit"
- app:layout_constraintTop_toBottomOf="@+id/show_dialog_bt"
- app:layout_constraintBottom_toTopOf="@+id/show_dialog_only_positive_bt"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent" />
-
- <Button
- android:id="@+id/show_dialog_only_positive_bt"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/dialog_show_dialog_only_positive"
- app:layout_constraintTop_toBottomOf="@+id/show_dialog_with_textbox"
- app:layout_constraintBottom_toTopOf="@+id/show_dialog_with_no_button_set"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent" />
-
- <Button
- android:id="@+id/show_dialog_with_no_button_set"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/dialog_show_dialog_no_button"
- app:layout_constraintBottom_toTopOf="@+id/show_dialog_with_checkbox_bt"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/show_dialog_only_positive_bt" />
-
- <Button
- android:id="@+id/show_dialog_with_checkbox_bt"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/dialog_show_dialog_checkbox"
- app:layout_constraintBottom_toTopOf="@+id/show_dialog_without_title"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/show_dialog_with_no_button_set" />
-
- <Button
- android:id="@+id/show_dialog_without_title"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/dialog_show_dialog_no_title"
- app:layout_constraintBottom_toTopOf="@+id/show_toast"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/show_dialog_with_checkbox_bt" />
-
- <Button
- android:id="@+id/show_toast"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/dialog_show_toast"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/show_dialog_without_title" />
-
-</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/car-ui-lib/tests/paintbooth/res/values/strings.xml b/car-ui-lib/tests/paintbooth/res/values/strings.xml
index 926e814..e2159c1 100644
--- a/car-ui-lib/tests/paintbooth/res/values/strings.xml
+++ b/car-ui-lib/tests/paintbooth/res/values/strings.xml
@@ -218,6 +218,9 @@
<!-- Text for show dialog button [CHAR_LIMIT=18]-->
<string name="dialog_show_dialog">Show Dialog</string>
+ <!-- Text for show dialog button [CHAR_LIMIT=30]-->
+ <string name="dialog_show_dialog_icon">Show Dialog with icon</string>
+
<!-- Text for Dialog with edit text box button [CHAR_LIMIT=50]-->
<string name="dialog_show_dialog_edit">Show Dialog with edit text box</string>
@@ -236,6 +239,12 @@
<!-- Text for show Toast button [CHAR_LIMIT=16]-->
<string name="dialog_show_toast">Show Toast</string>
+ <!-- Button that shows a dialog with a subtitle [CHAR_LIMIT=50]-->
+ <string name="dialog_show_subtitle">Show Dialog with title and subtitle</string>
+
+ <!-- Button that shows a dialog with a subtitle and icon [CHAR_LIMIT=50]-->
+ <string name="dialog_show_subtitle_and_icon">Show Dialog with title, subtitle, and icon</string>
+
<!--This section is for widget attributes -->
<eat-comment/>
<!-- Text for checkbox [CHAR_LIMIT=16]-->
diff --git a/car-ui-lib/tests/paintbooth/res/xml/menuitems.xml b/car-ui-lib/tests/paintbooth/res/xml/menuitems.xml
index 7899572..4f22379 100644
--- a/car-ui-lib/tests/paintbooth/res/xml/menuitems.xml
+++ b/car-ui-lib/tests/paintbooth/res/xml/menuitems.xml
@@ -15,6 +15,8 @@
~ limitations under the License.
-->
<MenuItems xmlns:app="http://schemas.android.com/apk/res-auto">
+ <MenuItem app:search="true"/>
+ <MenuItem app:settings="true"/>
<MenuItem
app:title="@string/preferences_screen_title"/>
<MenuItem
diff --git a/car-ui-lib/tests/paintbooth/res/xml/preference_samples.xml b/car-ui-lib/tests/paintbooth/res/xml/preference_samples.xml
index 87e491a..b49dd0d 100644
--- a/car-ui-lib/tests/paintbooth/res/xml/preference_samples.xml
+++ b/car-ui-lib/tests/paintbooth/res/xml/preference_samples.xml
@@ -63,7 +63,7 @@
android:title="@string/title_switch_preference"
android:summary="@string/summary_switch_preference"/>
- <com.android.car.ui.preference.CarUiDropDownPreference
+ <DropDownPreference
android:key="dropdown"
android:title="@string/title_dropdown_preference"
android:entries="@array/entries"
@@ -80,13 +80,13 @@
<PreferenceCategory
android:title="@string/dialogs">
- <com.android.car.ui.preference.CarUiEditTextPreference
+ <EditTextPreference
android:key="edittext"
android:title="@string/title_edittext_preference"
app:useSimpleSummaryProvider="true"
android:dialogTitle="@string/dialog_title_edittext_preference"/>
- <com.android.car.ui.preference.CarUiListPreference
+ <ListPreference
android:key="list"
android:title="@string/title_list_preference"
app:useSimpleSummaryProvider="true"
@@ -94,7 +94,7 @@
android:entryValues="@array/entry_values"
android:dialogTitle="@string/dialog_title_list_preference"/>
- <com.android.car.ui.preference.CarUiMultiSelectListPreference
+ <MultiSelectListPreference
android:key="multi_select_list"
android:title="@string/title_multi_list_preference"
android:summary="@string/summary_multi_list_preference"
@@ -108,19 +108,19 @@
android:title="@string/advanced_attributes"
app:initialExpandedChildrenCount="1">
- <com.android.car.ui.preference.CarUiPreference
+ <Preference
android:key="expandable"
android:title="@string/title_expandable_preference"
android:summary="@string/summary_expandable_preference"/>
- <com.android.car.ui.preference.CarUiPreference
+ <Preference
android:title="@string/title_intent_preference"
android:summary="@string/summary_intent_preference">
<intent android:action="android.intent.action.VIEW"
android:data="http://www.android.com"/>
- </com.android.car.ui.preference.CarUiPreference>
+ </Preference>
<SwitchPreference
android:key="parent"
diff --git a/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/MainActivity.java b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/MainActivity.java
index 2ae5f2b..f05a827 100644
--- a/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/MainActivity.java
+++ b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/MainActivity.java
@@ -19,11 +19,13 @@
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
+import android.util.Log;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
+import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
@@ -38,6 +40,7 @@
import com.android.car.ui.paintbooth.widgets.WidgetActivity;
import com.android.car.ui.recyclerview.CarUiRecyclerView;
+import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
@@ -106,5 +109,84 @@
CarUiRecyclerView prv = findViewById(R.id.activities);
prv.setAdapter(mAdapter);
+
+ initLeakCanary();
+ }
+
+ private void initLeakCanary() {
+ // This sets LeakCanary to report errors after a single leak instead of 5, and to ask for
+ // permission to use storage, which it needs to work.
+ //
+ // Equivalent to this non-reflection code:
+ //
+ // Config config = LeakCanary.INSTANCE.getConfig();
+ // LeakCanary.INSTANCE.setConfig(config.copy(config.getDumpHeap(),
+ // config.getDumpHeapWhenDebugging(),
+ // 1,
+ // config.getReferenceMatchers(),
+ // config.getObjectInspectors(),
+ // config.getOnHeapAnalyzedListener(),
+ // config.getMetatadaExtractor(),
+ // config.getComputeRetainedHeapSize(),
+ // config.getMaxStoredHeapDumps(),
+ // true,
+ // config.getUseExperimentalLeakFinders()));
+ try {
+ Class<?> canaryClass = Class.forName("leakcanary.LeakCanary");
+ try {
+ Class<?> onHeapAnalyzedListenerClass =
+ Class.forName("leakcanary.OnHeapAnalyzedListener");
+ Class<?> metadataExtractorClass = Class.forName("shark.MetadataExtractor");
+ Method getConfig = canaryClass.getMethod("getConfig");
+ Class<?> configClass = getConfig.getReturnType();
+ Method setConfig = canaryClass.getMethod("setConfig", configClass);
+ Method copy = configClass.getMethod("copy", boolean.class, boolean.class,
+ int.class, List.class, List.class, onHeapAnalyzedListenerClass,
+ metadataExtractorClass, boolean.class, int.class, boolean.class,
+ boolean.class);
+
+ Object canary = canaryClass.getField("INSTANCE").get(null);
+ Object currentConfig = getConfig.invoke(canary);
+
+ Boolean dumpHeap = (Boolean) configClass
+ .getMethod("getDumpHeap").invoke(currentConfig);
+ Boolean dumpHeapWhenDebugging = (Boolean) configClass
+ .getMethod("getDumpHeapWhenDebugging").invoke(currentConfig);
+ List<?> referenceMatchers = (List<?>) configClass
+ .getMethod("getReferenceMatchers").invoke(currentConfig);
+ List<?> objectInspectors = (List<?>) configClass
+ .getMethod("getObjectInspectors").invoke(currentConfig);
+ Object onHeapAnalyzedListener = configClass
+ .getMethod("getOnHeapAnalyzedListener").invoke(currentConfig);
+ // Yes, LeakCanary misspelled metadata
+ Object metadataExtractor = configClass
+ .getMethod("getMetatadaExtractor").invoke(currentConfig);
+ Boolean computeRetainedHeapSize = (Boolean) configClass
+ .getMethod("getComputeRetainedHeapSize").invoke(currentConfig);
+ Integer maxStoredHeapDumps = (Integer) configClass
+ .getMethod("getMaxStoredHeapDumps").invoke(currentConfig);
+ Boolean useExperimentalLeakFinders = (Boolean) configClass
+ .getMethod("getUseExperimentalLeakFinders").invoke(currentConfig);
+
+ setConfig.invoke(canary, copy.invoke(currentConfig,
+ dumpHeap,
+ dumpHeapWhenDebugging,
+ 1,
+ referenceMatchers,
+ objectInspectors,
+ onHeapAnalyzedListener,
+ metadataExtractor,
+ computeRetainedHeapSize,
+ maxStoredHeapDumps,
+ true,
+ useExperimentalLeakFinders));
+
+ } catch (ReflectiveOperationException e) {
+ Log.e("paintbooth", "Error initializing LeakCanary", e);
+ Toast.makeText(this, "Error initializing LeakCanary", Toast.LENGTH_LONG).show();
+ }
+ } catch (ClassNotFoundException e) {
+ // LeakCanary is not used in this build, do nothing.
+ }
}
}
diff --git a/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/dialogs/DialogsActivity.java b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/dialogs/DialogsActivity.java
index 5b5c330..505a026 100644
--- a/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/dialogs/DialogsActivity.java
+++ b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/dialogs/DialogsActivity.java
@@ -18,100 +18,181 @@
import android.app.Activity;
import android.os.Bundle;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
import android.widget.Button;
import android.widget.Toast;
+import androidx.annotation.NonNull;
+
import com.android.car.ui.AlertDialogBuilder;
import com.android.car.ui.paintbooth.R;
+import com.android.car.ui.recyclerview.CarUiRecyclerView;
+
+import java.util.ArrayList;
+import java.util.List;
/**
* Activity that shows different dialogs from the device default theme.
*/
public class DialogsActivity extends Activity {
+ private final List<Pair<Integer, View.OnClickListener>> mButtons = new ArrayList<>();
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- setContentView(R.layout.dialogs_activity);
+ setContentView(R.layout.car_ui_recycler_view_activity);
- Button showDialogButton = findViewById(R.id.show_dialog_bt);
- showDialogButton.setOnClickListener(v -> openDialog(false));
+ mButtons.add(Pair.create(R.string.dialog_show_dialog,
+ v -> showDialog()));
+ mButtons.add(Pair.create(R.string.dialog_show_dialog_icon,
+ v -> showDialogWithIcon()));
+ mButtons.add(Pair.create(R.string.dialog_show_dialog_edit,
+ v -> showDialogWithTextBox()));
+ mButtons.add(Pair.create(R.string.dialog_show_dialog_only_positive,
+ v -> showDialogWithOnlyPositiveButton()));
+ mButtons.add(Pair.create(R.string.dialog_show_dialog_no_button,
+ v -> showDialogWithNoButtonProvided()));
+ mButtons.add(Pair.create(R.string.dialog_show_dialog_checkbox,
+ v -> showDialogWithCheckbox()));
+ mButtons.add(Pair.create(R.string.dialog_show_dialog_no_title,
+ v -> showDialogWithoutTitle()));
+ mButtons.add(Pair.create(R.string.dialog_show_toast,
+ v -> showToast()));
+ mButtons.add(Pair.create(R.string.dialog_show_subtitle,
+ v -> showDialogWithSubtitle()));
+ mButtons.add(Pair.create(R.string.dialog_show_subtitle_and_icon,
+ v -> showDialogWithSubtitleAndIcon()));
- Button showDialogOnlyPositiveButton = findViewById(R.id.show_dialog_only_positive_bt);
- showDialogOnlyPositiveButton.setOnClickListener(v -> openDialogWithOnlyPositiveButton());
-
- Button showDialogWithoutTitleButton = findViewById(R.id.show_dialog_without_title);
- showDialogWithoutTitleButton.setOnClickListener(v -> openDialogWithoutTitle());
-
- Button showDialogWithNoButtonProvided = findViewById(R.id.show_dialog_with_no_button_set);
- showDialogWithNoButtonProvided.setOnClickListener(v -> openDialogWithNoButtonProvided());
-
- Button showDialogWithCheckboxButton = findViewById(R.id.show_dialog_with_checkbox_bt);
- showDialogWithCheckboxButton.setOnClickListener(v -> openDialog(true));
-
- Button showDialogWithTextbox = findViewById(R.id.show_dialog_with_textbox);
- showDialogWithTextbox.setOnClickListener(v -> openDialogWithTextbox());
-
- Button showToast = findViewById(R.id.show_toast);
- showToast.setOnClickListener(v -> showToast());
+ CarUiRecyclerView recyclerView = requireViewById(R.id.list);
+ recyclerView.setAdapter(mAdapter);
}
- private void openDialog(boolean showCheckbox) {
- AlertDialogBuilder builder = new AlertDialogBuilder(this);
-
- if (showCheckbox) {
- // Set Custom Title
- builder.setTitle("Custom Dialog Box");
- builder.setMultiChoiceItems(
- new CharSequence[]{"I am a checkbox"},
- new boolean[]{false},
- (dialog, which, isChecked) -> {
- });
- } else {
- builder.setTitle("Standard Alert Dialog").setMessage("With a message to show.");
- }
-
- builder
- .setPositiveButton("OK", (dialoginterface, i) -> {
+ private void showDialog() {
+ new AlertDialogBuilder(this)
+ .setTitle("Standard Alert Dialog")
+ .setMessage("With a message to show.")
+ .setPositiveButton("OK", (dialogInterface, which) -> {
})
- .setNegativeButton("CANCEL", (dialog, which) -> {
- });
- builder.show();
- }
-
- private void openDialogWithNoButtonProvided() {
- AlertDialogBuilder builder = new AlertDialogBuilder(this);
- builder.setTitle("Standard Alert Dialog").show();
- }
-
- private void openDialogWithTextbox() {
- AlertDialogBuilder builder = new AlertDialogBuilder(this);
- builder.setTitle("Standard Alert Dialog").setEditBox("Edit me please", null, null);
- builder.setPositiveButton("OK", (dialoginterface, i) -> {
- });
- builder.show();
- }
-
- private void openDialogWithOnlyPositiveButton() {
- AlertDialogBuilder builder = new AlertDialogBuilder(this);
- builder.setTitle("Standard Alert Dialog").setMessage("With a message to show.");
- builder.setPositiveButton("OK", (dialoginterface, i) -> {
- });
- builder.show();
- }
-
- private void openDialogWithoutTitle() {
- AlertDialogBuilder builder = new AlertDialogBuilder(this);
- builder.setMessage("I dont have a title.");
- builder
- .setPositiveButton("OK", (dialoginterface, i) -> {
+ .setNegativeButton("CANCEL", (dialogInterface, which) -> {
})
- .setNegativeButton("CANCEL", (dialog, which) -> {
- });
- builder.show();
+ .show();
+ }
+
+ private void showDialogWithIcon() {
+ new AlertDialogBuilder(this)
+ .setTitle("Alert dialog with icon")
+ .setMessage("The message body of the alert")
+ .setIcon(R.drawable.ic_tracklist)
+ .show();
+ }
+
+ private void showDialogWithNoButtonProvided() {
+ new AlertDialogBuilder(this)
+ .setTitle("Standard Alert Dialog")
+ .show();
+ }
+
+ private void showDialogWithCheckbox() {
+ new AlertDialogBuilder(this)
+ .setTitle("Custom Dialog Box")
+ .setMultiChoiceItems(
+ new CharSequence[]{"I am a checkbox"},
+ new boolean[]{false},
+ (dialog, which, isChecked) -> {
+ })
+ .setPositiveButton("OK", (dialogInterface, which) -> {
+ })
+ .setNegativeButton("CANCEL", (dialogInterface, which) -> {
+ })
+ .show();
+ }
+
+ private void showDialogWithTextBox() {
+ new AlertDialogBuilder(this)
+ .setTitle("Standard Alert Dialog")
+ .setEditBox("Edit me please", null, null)
+ .setPositiveButton("OK", (dialogInterface, i) -> {
+ })
+ .show();
+ }
+
+ private void showDialogWithOnlyPositiveButton() {
+ new AlertDialogBuilder(this)
+ .setTitle("Standard Alert Dialog").setMessage("With a message to show.")
+ .setPositiveButton("OK", (dialogInterface, i) -> {
+ })
+ .show();
+ }
+
+ private void showDialogWithoutTitle() {
+ new AlertDialogBuilder(this)
+ .setMessage("I dont have a title.")
+ .setPositiveButton("OK", (dialogInterface, i) -> {
+ })
+ .setNegativeButton("CANCEL", (dialogInterface, which) -> {
+ })
+ .show();
}
private void showToast() {
Toast.makeText(this, "Toast message looks like this", Toast.LENGTH_LONG).show();
}
+
+ private void showDialogWithSubtitle() {
+ new AlertDialogBuilder(this)
+ .setTitle("My Title!")
+ .setSubtitle("My Subtitle!")
+ .setMessage("My Message!")
+ .show();
+ }
+
+ private void showDialogWithSubtitleAndIcon() {
+ new AlertDialogBuilder(this)
+ .setTitle("My Title!")
+ .setSubtitle("My Subtitle!")
+ .setMessage("My Message!")
+ .setIcon(R.drawable.ic_tracklist)
+ .show();
+ }
+
+ private static class ViewHolder extends CarUiRecyclerView.ViewHolder {
+
+ private final Button mButton;
+
+ ViewHolder(View itemView) {
+ super(itemView);
+ mButton = itemView.requireViewById(R.id.button);
+ }
+
+ public void bind(Integer title, View.OnClickListener listener) {
+ mButton.setText(title);
+ mButton.setOnClickListener(listener);
+ }
+ }
+
+ private final CarUiRecyclerView.Adapter<ViewHolder> mAdapter =
+ new CarUiRecyclerView.Adapter<ViewHolder>() {
+ @Override
+ public int getItemCount() {
+ return mButtons.size();
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int position) {
+ View item =
+ LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item,
+ parent, false);
+ return new ViewHolder(item);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+ Pair<Integer, View.OnClickListener> pair = mButtons.get(position);
+ holder.bind(pair.first, pair.second);
+ }
+ };
}
diff --git a/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/toolbar/ToolbarActivity.java b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/toolbar/ToolbarActivity.java
index b42a2ec..a5e7fa3 100644
--- a/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/toolbar/ToolbarActivity.java
+++ b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/toolbar/ToolbarActivity.java
@@ -62,13 +62,25 @@
return false;
});
- mMenuItems.add(
- MenuItem.Builder.createSearch(this, i -> toolbar.setState(Toolbar.State.SEARCH)));
+ mMenuItems.add(MenuItem.builder(this)
+ .setToSearch()
+ .setOnClickListener(i -> toolbar.setState(Toolbar.State.SEARCH))
+ .build());
toolbar.setMenuItems(mMenuItems);
- mButtons.add(Pair.create(getString(R.string.toolbar_change_title),
- v -> toolbar.setTitle(toolbar.getTitle() + " X")));
+ mButtons.add(Pair.create("Toggle progress bar", v -> {
+ if (toolbar.getProgressBar().getVisibility() == View.GONE) {
+ toolbar.showProgressBar();
+ Toast.makeText(this, "showing progress bar", Toast.LENGTH_SHORT).show();
+ } else {
+ toolbar.hideProgressBar();
+ Toast.makeText(this, "hiding progress bar", Toast.LENGTH_SHORT).show();
+ }
+ }));
+
+ mButtons.add(Pair.create("Change title", v ->
+ toolbar.setTitle(toolbar.getTitle() + " X")));
mButtons.add(Pair.create(getString(R.string.toolbar_set_xml_resource), v -> {
mMenuItems.clear();
@@ -76,9 +88,11 @@
}));
mButtons.add(Pair.create(getString(R.string.toolbar_add_icon), v -> {
- mMenuItems.add(MenuItem.Builder.createSettings(
- this, i -> Toast.makeText(this, "Clicked",
- Toast.LENGTH_SHORT).show()));
+ mMenuItems.add(MenuItem.builder(this)
+ .setToSettings()
+ .setOnClickListener(i -> Toast.makeText(this, "Clicked",
+ Toast.LENGTH_SHORT).show())
+ .build());
toolbar.setMenuItems(mMenuItems);
}));
diff --git a/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/CarUiRecyclerViewTest.java b/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/CarUiRecyclerViewTest.java
index 5b2a1e3..5cf9e85 100644
--- a/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/CarUiRecyclerViewTest.java
+++ b/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/CarUiRecyclerViewTest.java
@@ -18,7 +18,11 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
import android.content.Context;
+import android.content.res.Resources;
import android.view.LayoutInflater;
import android.view.View;
@@ -34,6 +38,7 @@
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
import org.robolectric.RuntimeEnvironment;
@RunWith(CarUiRobolectricTestRunner.class)
@@ -104,4 +109,24 @@
assertThat(mCarUiRecyclerView.mNestedRecyclerView).isNotNull();
}
+
+ @Test
+ public void init_shouldNotContainNestedRecyclerView() {
+ Context context = spy(mContext);
+ Resources resources = spy(mContext.getResources());
+ when(resources.getBoolean(R.bool.car_ui_scrollbar_enable)).thenReturn(false);
+ when(context.getResources()).thenReturn(resources);
+
+ mCarUiRecyclerView = new CarUiRecyclerView(context);
+
+ assertThat(mCarUiRecyclerView.mNestedRecyclerView).isNull();
+ }
+
+ @Test
+ public void init_shouldHaveGridLayout() {
+ mCarUiRecyclerView = new CarUiRecyclerView(mContext,
+ Robolectric.buildAttributeSet().addAttribute(R.attr.layoutStyle, "grid").build());
+ assertThat(mCarUiRecyclerView.getEffectiveLayoutManager()).isInstanceOf(
+ GridLayoutManager.class);
+ }
}
diff --git a/car-ui-lib/tests/robotests/src/com/android/car/ui/toolbar/ToolbarTest.java b/car-ui-lib/tests/robotests/src/com/android/car/ui/toolbar/ToolbarTest.java
index 239caee..54eaee7 100644
--- a/car-ui-lib/tests/robotests/src/com/android/car/ui/toolbar/ToolbarTest.java
+++ b/car-ui-lib/tests/robotests/src/com/android/car/ui/toolbar/ToolbarTest.java
@@ -323,7 +323,7 @@
public void menuItems_setId_shouldWork() {
MenuItem item = MenuItem.builder(mContext).build();
- assertThat(item.getId()).isEqualTo(0);
+ assertThat(item.getId()).isEqualTo(View.NO_ID);
item.setId(7);
@@ -417,8 +417,7 @@
@Test
public void menuItems_searchScreen_shouldHideMenuItems() {
mToolbar.setMenuItems(Arrays.asList(
- MenuItem.Builder.createSearch(mContext, i -> {
- }),
+ MenuItem.builder(mContext).setToSearch().build(),
createMenuItem(i -> {
})));
@@ -432,8 +431,7 @@
@Test
public void menuItems_showMenuItemsWhileSearching() {
mToolbar.setMenuItems(Arrays.asList(
- MenuItem.Builder.createSearch(mContext, i -> {
- }),
+ MenuItem.builder(mContext).setToSearch().build(),
createMenuItem(i -> {
})));
@@ -445,7 +443,7 @@
}
private MenuItem createMenuItem(MenuItem.OnClickListener listener) {
- return new MenuItem.Builder(mContext)
+ return MenuItem.builder(mContext)
.setTitle("Button!")
.setOnClickListener(listener)
.build();