fix: 引入Settings的Module
This commit is contained in:
266
car-qc-lib/src/com/android/car/qc/QCActionItem.java
Normal file
266
car-qc-lib/src/com/android/car/qc/QCActionItem.java
Normal file
@@ -0,0 +1,266 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.car.qc;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Parcel;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
/**
|
||||
* Quick Control Action that are includes as either start or end actions in {@link QCRow}
|
||||
*/
|
||||
public class QCActionItem extends QCItem {
|
||||
private final boolean mIsChecked;
|
||||
private final boolean mIsAvailable;
|
||||
private final boolean mIsClickable;
|
||||
private Icon mIcon;
|
||||
private PendingIntent mAction;
|
||||
private PendingIntent mDisabledClickAction;
|
||||
private String mContentDescription;
|
||||
|
||||
public QCActionItem(@NonNull @QCItemType String type, boolean isChecked, boolean isEnabled,
|
||||
boolean isAvailable, boolean isClickable, boolean isClickableWhileDisabled,
|
||||
@Nullable Icon icon, @Nullable String contentDescription,
|
||||
@Nullable PendingIntent action, @Nullable PendingIntent disabledClickAction) {
|
||||
super(type, isEnabled, isClickableWhileDisabled);
|
||||
mIsChecked = isChecked;
|
||||
mIsAvailable = isAvailable;
|
||||
mIsClickable = isClickable;
|
||||
mIcon = icon;
|
||||
mContentDescription = contentDescription;
|
||||
mAction = action;
|
||||
mDisabledClickAction = disabledClickAction;
|
||||
}
|
||||
|
||||
public QCActionItem(@NonNull Parcel in) {
|
||||
super(in);
|
||||
mIsChecked = in.readBoolean();
|
||||
mIsAvailable = in.readBoolean();
|
||||
mIsClickable = in.readBoolean();
|
||||
boolean hasIcon = in.readBoolean();
|
||||
if (hasIcon) {
|
||||
mIcon = Icon.CREATOR.createFromParcel(in);
|
||||
}
|
||||
boolean hasContentDescription = in.readBoolean();
|
||||
if (hasContentDescription) {
|
||||
mContentDescription = in.readString();
|
||||
}
|
||||
boolean hasAction = in.readBoolean();
|
||||
if (hasAction) {
|
||||
mAction = PendingIntent.CREATOR.createFromParcel(in);
|
||||
}
|
||||
boolean hasDisabledClickAction = in.readBoolean();
|
||||
if (hasDisabledClickAction) {
|
||||
mDisabledClickAction = PendingIntent.CREATOR.createFromParcel(in);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
super.writeToParcel(dest, flags);
|
||||
dest.writeBoolean(mIsChecked);
|
||||
dest.writeBoolean(mIsAvailable);
|
||||
dest.writeBoolean(mIsClickable);
|
||||
boolean includeIcon = getType().equals(QC_TYPE_ACTION_TOGGLE) && mIcon != null;
|
||||
dest.writeBoolean(includeIcon);
|
||||
if (includeIcon) {
|
||||
mIcon.writeToParcel(dest, flags);
|
||||
}
|
||||
boolean hasContentDescription = mContentDescription != null;
|
||||
dest.writeBoolean(hasContentDescription);
|
||||
if (hasContentDescription) {
|
||||
dest.writeString(mContentDescription);
|
||||
}
|
||||
boolean hasAction = mAction != null;
|
||||
dest.writeBoolean(hasAction);
|
||||
if (hasAction) {
|
||||
mAction.writeToParcel(dest, flags);
|
||||
}
|
||||
boolean hasDisabledClickAction = mDisabledClickAction != null;
|
||||
dest.writeBoolean(hasDisabledClickAction);
|
||||
if (hasDisabledClickAction) {
|
||||
mDisabledClickAction.writeToParcel(dest, flags);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent getPrimaryAction() {
|
||||
return mAction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent getDisabledClickAction() {
|
||||
return mDisabledClickAction;
|
||||
}
|
||||
|
||||
public boolean isChecked() {
|
||||
return mIsChecked;
|
||||
}
|
||||
|
||||
public boolean isAvailable() {
|
||||
return mIsAvailable;
|
||||
}
|
||||
|
||||
public boolean isClickable() {
|
||||
return mIsClickable;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Icon getIcon() {
|
||||
return mIcon;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getContentDescription() {
|
||||
return mContentDescription;
|
||||
}
|
||||
|
||||
public static Creator<QCActionItem> CREATOR = new Creator<QCActionItem>() {
|
||||
@Override
|
||||
public QCActionItem createFromParcel(Parcel source) {
|
||||
return new QCActionItem(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public QCActionItem[] newArray(int size) {
|
||||
return new QCActionItem[size];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Builder for {@link QCActionItem}.
|
||||
*/
|
||||
public static class Builder {
|
||||
private final String mType;
|
||||
private boolean mIsChecked;
|
||||
private boolean mIsEnabled = true;
|
||||
private boolean mIsAvailable = true;
|
||||
private boolean mIsClickable = true;
|
||||
private boolean mIsClickableWhileDisabled = false;
|
||||
private Icon mIcon;
|
||||
private PendingIntent mAction;
|
||||
private PendingIntent mDisabledClickAction;
|
||||
private String mContentDescription;
|
||||
|
||||
public Builder(@NonNull @QCItemType String type) {
|
||||
if (!isValidType(type)) {
|
||||
throw new IllegalArgumentException("Invalid QCActionItem type provided" + type);
|
||||
}
|
||||
mType = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not the action item should be checked.
|
||||
*/
|
||||
public Builder setChecked(boolean checked) {
|
||||
mIsChecked = checked;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not the action item should be enabled.
|
||||
*/
|
||||
public Builder setEnabled(boolean enabled) {
|
||||
mIsEnabled = enabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not the action item is available.
|
||||
*/
|
||||
public Builder setAvailable(boolean available) {
|
||||
mIsAvailable = available;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the action is clickable. This differs from available in that the style will
|
||||
* remain as if it's enabled/available but click actions will not be processed.
|
||||
*/
|
||||
public Builder setClickable(boolean clickable) {
|
||||
mIsClickable = clickable;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not an action item should be clickable while disabled.
|
||||
*/
|
||||
public Builder setClickableWhileDisabled(boolean clickable) {
|
||||
mIsClickableWhileDisabled = clickable;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the icon for {@link QC_TYPE_ACTION_TOGGLE} actions
|
||||
*/
|
||||
public Builder setIcon(@Nullable Icon icon) {
|
||||
mIcon = icon;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the content description
|
||||
*/
|
||||
public Builder setContentDescription(@Nullable String contentDescription) {
|
||||
mContentDescription = contentDescription;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the string resource to use for content description
|
||||
*/
|
||||
public Builder setContentDescription(@NonNull Context context,
|
||||
@StringRes int contentDescriptionResId) {
|
||||
mContentDescription = context.getString(contentDescriptionResId);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the PendingIntent to be sent when the action item is clicked.
|
||||
*/
|
||||
public Builder setAction(@Nullable PendingIntent action) {
|
||||
mAction = action;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the PendingIntent to be sent when the action item is clicked while disabled.
|
||||
*/
|
||||
public Builder setDisabledClickAction(@Nullable PendingIntent action) {
|
||||
mDisabledClickAction = action;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the final {@link QCActionItem}.
|
||||
*/
|
||||
public QCActionItem build() {
|
||||
return new QCActionItem(mType, mIsChecked, mIsEnabled, mIsAvailable, mIsClickable,
|
||||
mIsClickableWhileDisabled, mIcon, mContentDescription, mAction,
|
||||
mDisabledClickAction);
|
||||
}
|
||||
|
||||
private boolean isValidType(String type) {
|
||||
return type.equals(QC_TYPE_ACTION_SWITCH) || type.equals(QC_TYPE_ACTION_TOGGLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
154
car-qc-lib/src/com/android/car/qc/QCItem.java
Normal file
154
car-qc-lib/src/com/android/car/qc/QCItem.java
Normal file
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.car.qc;
|
||||
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringDef;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
/**
|
||||
* Base class for all quick controls elements.
|
||||
*/
|
||||
public abstract class QCItem implements Parcelable {
|
||||
public static final String QC_TYPE_LIST = "QC_TYPE_LIST";
|
||||
public static final String QC_TYPE_ROW = "QC_TYPE_ROW";
|
||||
public static final String QC_TYPE_TILE = "QC_TYPE_TILE";
|
||||
public static final String QC_TYPE_SLIDER = "QC_TYPE_SLIDER";
|
||||
public static final String QC_TYPE_ACTION_SWITCH = "QC_TYPE_ACTION_SWITCH";
|
||||
public static final String QC_TYPE_ACTION_TOGGLE = "QC_TYPE_ACTION_TOGGLE";
|
||||
|
||||
public static final String QC_ACTION_TOGGLE_STATE = "QC_ACTION_TOGGLE_STATE";
|
||||
public static final String QC_ACTION_SLIDER_VALUE = "QC_ACTION_SLIDER_VALUE";
|
||||
|
||||
@StringDef(value = {
|
||||
QC_TYPE_LIST,
|
||||
QC_TYPE_ROW,
|
||||
QC_TYPE_TILE,
|
||||
QC_TYPE_SLIDER,
|
||||
QC_TYPE_ACTION_SWITCH,
|
||||
QC_TYPE_ACTION_TOGGLE,
|
||||
})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface QCItemType {
|
||||
}
|
||||
|
||||
private final String mType;
|
||||
private final boolean mIsEnabled;
|
||||
private final boolean mIsClickableWhileDisabled;
|
||||
private ActionHandler mActionHandler;
|
||||
private ActionHandler mDisabledClickActionHandler;
|
||||
|
||||
public QCItem(@NonNull @QCItemType String type) {
|
||||
this(type, /* isEnabled= */true, /* isClickableWhileDisabled= */ false);
|
||||
}
|
||||
|
||||
public QCItem(@NonNull @QCItemType String type, boolean isEnabled,
|
||||
boolean isClickableWhileDisabled) {
|
||||
mType = type;
|
||||
mIsEnabled = isEnabled;
|
||||
mIsClickableWhileDisabled = isClickableWhileDisabled;
|
||||
}
|
||||
|
||||
public QCItem(@NonNull Parcel in) {
|
||||
mType = in.readString();
|
||||
mIsEnabled = in.readBoolean();
|
||||
mIsClickableWhileDisabled = in.readBoolean();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@QCItemType
|
||||
public String getType() {
|
||||
return mType;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return mIsEnabled;
|
||||
}
|
||||
|
||||
public boolean isClickableWhileDisabled() {
|
||||
return mIsClickableWhileDisabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(mType);
|
||||
dest.writeBoolean(mIsEnabled);
|
||||
dest.writeBoolean(mIsClickableWhileDisabled);
|
||||
}
|
||||
|
||||
public void setActionHandler(@Nullable ActionHandler handler) {
|
||||
mActionHandler = handler;
|
||||
}
|
||||
|
||||
public void setDisabledClickActionHandler(@Nullable ActionHandler handler) {
|
||||
mDisabledClickActionHandler = handler;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ActionHandler getActionHandler() {
|
||||
return mActionHandler;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ActionHandler getDisabledClickActionHandler() {
|
||||
return mDisabledClickActionHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the PendingIntent that is sent when the item is clicked.
|
||||
*/
|
||||
@Nullable
|
||||
public abstract PendingIntent getPrimaryAction();
|
||||
|
||||
/**
|
||||
* Returns the PendingIntent that is sent when the item is clicked while disabled.
|
||||
*/
|
||||
@Nullable
|
||||
public abstract PendingIntent getDisabledClickAction();
|
||||
|
||||
/**
|
||||
* Action handler that can listen for an action to occur and notify listeners.
|
||||
*/
|
||||
public interface ActionHandler {
|
||||
/**
|
||||
* Callback when an action occurs.
|
||||
* @param item the QCItem that sent the action
|
||||
* @param context the context for the action
|
||||
* @param intent the intent that was sent with the action
|
||||
*/
|
||||
void onAction(@NonNull QCItem item, @NonNull Context context, @NonNull Intent intent);
|
||||
|
||||
default boolean isActivity() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
106
car-qc-lib/src/com/android/car/qc/QCList.java
Normal file
106
car-qc-lib/src/com/android/car/qc/QCList.java
Normal file
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.car.qc;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.os.Parcel;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Wrapping quick controls element that contains QCRow elements.
|
||||
*/
|
||||
public class QCList extends QCItem {
|
||||
private final List<QCRow> mRows;
|
||||
|
||||
public QCList(@NonNull List<QCRow> rows) {
|
||||
super(QC_TYPE_LIST);
|
||||
mRows = Collections.unmodifiableList(rows);
|
||||
}
|
||||
|
||||
public QCList(@NonNull Parcel in) {
|
||||
super(in);
|
||||
int rowCount = in.readInt();
|
||||
List<QCRow> rows = new ArrayList<>();
|
||||
for (int i = 0; i < rowCount; i++) {
|
||||
rows.add(QCRow.CREATOR.createFromParcel(in));
|
||||
}
|
||||
mRows = Collections.unmodifiableList(rows);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
super.writeToParcel(dest, flags);
|
||||
dest.writeInt(mRows.size());
|
||||
for (QCRow row : mRows) {
|
||||
row.writeToParcel(dest, flags);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent getPrimaryAction() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent getDisabledClickAction() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public List<QCRow> getRows() {
|
||||
return mRows;
|
||||
}
|
||||
|
||||
public static Creator<QCList> CREATOR = new Creator<QCList>() {
|
||||
@Override
|
||||
public QCList createFromParcel(Parcel source) {
|
||||
return new QCList(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public QCList[] newArray(int size) {
|
||||
return new QCList[size];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Builder for {@link QCList}.
|
||||
*/
|
||||
public static class Builder {
|
||||
private final List<QCRow> mRows = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Adds a {@link QCRow} to the list.
|
||||
*/
|
||||
public Builder addRow(@NonNull QCRow row) {
|
||||
mRows.add(row);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the final {@link QCList}.
|
||||
*/
|
||||
public QCList build() {
|
||||
return new QCList(mRows);
|
||||
}
|
||||
}
|
||||
}
|
||||
315
car-qc-lib/src/com/android/car/qc/QCRow.java
Normal file
315
car-qc-lib/src/com/android/car/qc/QCRow.java
Normal file
@@ -0,0 +1,315 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.car.qc;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Parcel;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Quick Control Row Element
|
||||
* ------------------------------------
|
||||
* | | Title | |
|
||||
* | StartItems | Subtitle | EndItems |
|
||||
* | | Sliders | |
|
||||
* ------------------------------------
|
||||
*/
|
||||
public class QCRow extends QCItem {
|
||||
private final String mTitle;
|
||||
private final String mSubtitle;
|
||||
private final Icon mStartIcon;
|
||||
private final boolean mIsStartIconTintable;
|
||||
private final QCSlider mSlider;
|
||||
private final List<QCActionItem> mStartItems;
|
||||
private final List<QCActionItem> mEndItems;
|
||||
private final PendingIntent mPrimaryAction;
|
||||
private PendingIntent mDisabledClickAction;
|
||||
|
||||
public QCRow(@Nullable String title, @Nullable String subtitle, boolean isEnabled,
|
||||
boolean isClickableWhileDisabled, @Nullable PendingIntent primaryAction,
|
||||
@Nullable PendingIntent disabledClickAction, @Nullable Icon startIcon,
|
||||
boolean isIconTintable, @Nullable QCSlider slider,
|
||||
@NonNull List<QCActionItem> startItems, @NonNull List<QCActionItem> endItems) {
|
||||
super(QC_TYPE_ROW, isEnabled, isClickableWhileDisabled);
|
||||
mTitle = title;
|
||||
mSubtitle = subtitle;
|
||||
mPrimaryAction = primaryAction;
|
||||
mDisabledClickAction = disabledClickAction;
|
||||
mStartIcon = startIcon;
|
||||
mIsStartIconTintable = isIconTintable;
|
||||
mSlider = slider;
|
||||
mStartItems = Collections.unmodifiableList(startItems);
|
||||
mEndItems = Collections.unmodifiableList(endItems);
|
||||
}
|
||||
|
||||
public QCRow(@NonNull Parcel in) {
|
||||
super(in);
|
||||
mTitle = in.readString();
|
||||
mSubtitle = in.readString();
|
||||
boolean hasIcon = in.readBoolean();
|
||||
if (hasIcon) {
|
||||
mStartIcon = Icon.CREATOR.createFromParcel(in);
|
||||
} else {
|
||||
mStartIcon = null;
|
||||
}
|
||||
mIsStartIconTintable = in.readBoolean();
|
||||
boolean hasSlider = in.readBoolean();
|
||||
if (hasSlider) {
|
||||
mSlider = QCSlider.CREATOR.createFromParcel(in);
|
||||
} else {
|
||||
mSlider = null;
|
||||
}
|
||||
List<QCActionItem> startItems = new ArrayList<>();
|
||||
int startItemCount = in.readInt();
|
||||
for (int i = 0; i < startItemCount; i++) {
|
||||
startItems.add(QCActionItem.CREATOR.createFromParcel(in));
|
||||
}
|
||||
mStartItems = Collections.unmodifiableList(startItems);
|
||||
List<QCActionItem> endItems = new ArrayList<>();
|
||||
int endItemCount = in.readInt();
|
||||
for (int i = 0; i < endItemCount; i++) {
|
||||
endItems.add(QCActionItem.CREATOR.createFromParcel(in));
|
||||
}
|
||||
mEndItems = Collections.unmodifiableList(endItems);
|
||||
boolean hasPrimaryAction = in.readBoolean();
|
||||
if (hasPrimaryAction) {
|
||||
mPrimaryAction = PendingIntent.CREATOR.createFromParcel(in);
|
||||
} else {
|
||||
mPrimaryAction = null;
|
||||
}
|
||||
boolean hasDisabledClickAction = in.readBoolean();
|
||||
if (hasDisabledClickAction) {
|
||||
mDisabledClickAction = PendingIntent.CREATOR.createFromParcel(in);
|
||||
} else {
|
||||
mDisabledClickAction = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
super.writeToParcel(dest, flags);
|
||||
dest.writeString(mTitle);
|
||||
dest.writeString(mSubtitle);
|
||||
boolean hasStartIcon = mStartIcon != null;
|
||||
dest.writeBoolean(hasStartIcon);
|
||||
if (hasStartIcon) {
|
||||
mStartIcon.writeToParcel(dest, flags);
|
||||
}
|
||||
dest.writeBoolean(mIsStartIconTintable);
|
||||
boolean hasSlider = mSlider != null;
|
||||
dest.writeBoolean(hasSlider);
|
||||
if (hasSlider) {
|
||||
mSlider.writeToParcel(dest, flags);
|
||||
}
|
||||
dest.writeInt(mStartItems.size());
|
||||
for (QCActionItem startItem : mStartItems) {
|
||||
startItem.writeToParcel(dest, flags);
|
||||
}
|
||||
dest.writeInt(mEndItems.size());
|
||||
for (QCActionItem endItem : mEndItems) {
|
||||
endItem.writeToParcel(dest, flags);
|
||||
}
|
||||
boolean hasPrimaryAction = mPrimaryAction != null;
|
||||
dest.writeBoolean(hasPrimaryAction);
|
||||
if (hasPrimaryAction) {
|
||||
mPrimaryAction.writeToParcel(dest, flags);
|
||||
}
|
||||
boolean hasDisabledClickAction = mDisabledClickAction != null;
|
||||
dest.writeBoolean(hasDisabledClickAction);
|
||||
if (hasDisabledClickAction) {
|
||||
mDisabledClickAction.writeToParcel(dest, flags);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent getPrimaryAction() {
|
||||
return mPrimaryAction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent getDisabledClickAction() {
|
||||
return mDisabledClickAction;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getTitle() {
|
||||
return mTitle;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getSubtitle() {
|
||||
return mSubtitle;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Icon getStartIcon() {
|
||||
return mStartIcon;
|
||||
}
|
||||
|
||||
public boolean isStartIconTintable() {
|
||||
return mIsStartIconTintable;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public QCSlider getSlider() {
|
||||
return mSlider;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public List<QCActionItem> getStartItems() {
|
||||
return mStartItems;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public List<QCActionItem> getEndItems() {
|
||||
return mEndItems;
|
||||
}
|
||||
|
||||
public static Creator<QCRow> CREATOR = new Creator<QCRow>() {
|
||||
@Override
|
||||
public QCRow createFromParcel(Parcel source) {
|
||||
return new QCRow(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public QCRow[] newArray(int size) {
|
||||
return new QCRow[size];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Builder for {@link QCRow}.
|
||||
*/
|
||||
public static class Builder {
|
||||
private final List<QCActionItem> mStartItems = new ArrayList<>();
|
||||
private final List<QCActionItem> mEndItems = new ArrayList<>();
|
||||
private Icon mStartIcon;
|
||||
private boolean mIsStartIconTintable = true;
|
||||
private String mTitle;
|
||||
private String mSubtitle;
|
||||
private boolean mIsEnabled = true;
|
||||
private boolean mIsClickableWhileDisabled = false;
|
||||
private QCSlider mSlider;
|
||||
private PendingIntent mPrimaryAction;
|
||||
private PendingIntent mDisabledClickAction;
|
||||
|
||||
/**
|
||||
* Sets the row title.
|
||||
*/
|
||||
public Builder setTitle(@Nullable String title) {
|
||||
mTitle = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the row subtitle.
|
||||
*/
|
||||
public Builder setSubtitle(@Nullable String subtitle) {
|
||||
mSubtitle = subtitle;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not the row is enabled. Note that this only affects the main row area,
|
||||
* not the action items contained within the row.
|
||||
*/
|
||||
public Builder setEnabled(boolean enabled) {
|
||||
mIsEnabled = enabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not the row should be clickable while disabled.
|
||||
*/
|
||||
public Builder setClickableWhileDisabled(boolean clickable) {
|
||||
mIsClickableWhileDisabled = clickable;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the row icon.
|
||||
*/
|
||||
public Builder setIcon(@Nullable Icon icon) {
|
||||
mStartIcon = icon;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not the row icon is tintable.
|
||||
*/
|
||||
public Builder setIconTintable(boolean tintable) {
|
||||
mIsStartIconTintable = tintable;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a {@link QCSlider} to the slider area.
|
||||
*/
|
||||
public Builder addSlider(@Nullable QCSlider slider) {
|
||||
mSlider = slider;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the PendingIntent to be sent when the row is clicked.
|
||||
*/
|
||||
public Builder setPrimaryAction(@Nullable PendingIntent action) {
|
||||
mPrimaryAction = action;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the PendingIntent to be sent when the action item is clicked while disabled.
|
||||
*/
|
||||
public Builder setDisabledClickAction(@Nullable PendingIntent action) {
|
||||
mDisabledClickAction = action;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a {@link QCActionItem} to the start items area.
|
||||
*/
|
||||
public Builder addStartItem(@NonNull QCActionItem item) {
|
||||
mStartItems.add(item);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a {@link QCActionItem} to the end items area.
|
||||
*/
|
||||
public Builder addEndItem(@NonNull QCActionItem item) {
|
||||
mEndItems.add(item);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the final {@link QCRow}.
|
||||
*/
|
||||
public QCRow build() {
|
||||
return new QCRow(mTitle, mSubtitle, mIsEnabled, mIsClickableWhileDisabled,
|
||||
mPrimaryAction, mDisabledClickAction, mStartIcon, mIsStartIconTintable,
|
||||
mSlider, mStartItems, mEndItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
189
car-qc-lib/src/com/android/car/qc/QCSlider.java
Normal file
189
car-qc-lib/src/com/android/car/qc/QCSlider.java
Normal file
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.car.qc;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.os.Parcel;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Quick Control Slider included in {@link QCRow}
|
||||
*/
|
||||
public class QCSlider extends QCItem {
|
||||
private int mMin = 0;
|
||||
private int mMax = 100;
|
||||
private int mValue = 0;
|
||||
private PendingIntent mInputAction;
|
||||
private PendingIntent mDisabledClickAction;
|
||||
|
||||
public QCSlider(int min, int max, int value, boolean enabled, boolean clickableWhileDisabled,
|
||||
@Nullable PendingIntent inputAction, @Nullable PendingIntent disabledClickAction) {
|
||||
super(QC_TYPE_SLIDER, enabled, clickableWhileDisabled);
|
||||
mMin = min;
|
||||
mMax = max;
|
||||
mValue = value;
|
||||
mInputAction = inputAction;
|
||||
mDisabledClickAction = disabledClickAction;
|
||||
}
|
||||
|
||||
public QCSlider(@NonNull Parcel in) {
|
||||
super(in);
|
||||
mMin = in.readInt();
|
||||
mMax = in.readInt();
|
||||
mValue = in.readInt();
|
||||
boolean hasAction = in.readBoolean();
|
||||
if (hasAction) {
|
||||
mInputAction = PendingIntent.CREATOR.createFromParcel(in);
|
||||
}
|
||||
boolean hasDisabledClickAction = in.readBoolean();
|
||||
if (hasDisabledClickAction) {
|
||||
mDisabledClickAction = PendingIntent.CREATOR.createFromParcel(in);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
super.writeToParcel(dest, flags);
|
||||
dest.writeInt(mMin);
|
||||
dest.writeInt(mMax);
|
||||
dest.writeInt(mValue);
|
||||
boolean hasAction = mInputAction != null;
|
||||
dest.writeBoolean(hasAction);
|
||||
if (hasAction) {
|
||||
mInputAction.writeToParcel(dest, flags);
|
||||
}
|
||||
boolean hasDisabledClickAction = mDisabledClickAction != null;
|
||||
dest.writeBoolean(hasDisabledClickAction);
|
||||
if (hasDisabledClickAction) {
|
||||
mDisabledClickAction.writeToParcel(dest, flags);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent getPrimaryAction() {
|
||||
return mInputAction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent getDisabledClickAction() {
|
||||
return mDisabledClickAction;
|
||||
}
|
||||
|
||||
public int getMin() {
|
||||
return mMin;
|
||||
}
|
||||
|
||||
public int getMax() {
|
||||
return mMax;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return mValue;
|
||||
}
|
||||
|
||||
public static Creator<QCSlider> CREATOR = new Creator<QCSlider>() {
|
||||
@Override
|
||||
public QCSlider createFromParcel(Parcel source) {
|
||||
return new QCSlider(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public QCSlider[] newArray(int size) {
|
||||
return new QCSlider[size];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Builder for {@link QCSlider}.
|
||||
*/
|
||||
public static class Builder {
|
||||
private int mMin = 0;
|
||||
private int mMax = 100;
|
||||
private int mValue = 0;
|
||||
private boolean mIsEnabled = true;
|
||||
private boolean mIsClickableWhileDisabled = false;
|
||||
private PendingIntent mInputAction;
|
||||
private PendingIntent mDisabledClickAction;
|
||||
|
||||
/**
|
||||
* Set the minimum allowed value for the slider input.
|
||||
*/
|
||||
public Builder setMin(int min) {
|
||||
mMin = min;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the maximum allowed value for the slider input.
|
||||
*/
|
||||
public Builder setMax(int max) {
|
||||
mMax = max;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current value for the slider input.
|
||||
*/
|
||||
public Builder setValue(int value) {
|
||||
mValue = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not the slider is enabled.
|
||||
*/
|
||||
public Builder setEnabled(boolean enabled) {
|
||||
mIsEnabled = enabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not a slider should be clickable while disabled.
|
||||
*/
|
||||
public Builder setClickableWhileDisabled(boolean clickable) {
|
||||
mIsClickableWhileDisabled = clickable;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the PendingIntent to be sent when the slider value is changed.
|
||||
*/
|
||||
public Builder setInputAction(@Nullable PendingIntent inputAction) {
|
||||
mInputAction = inputAction;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the PendingIntent to be sent when the action item is clicked while disabled.
|
||||
*/
|
||||
public Builder setDisabledClickAction(@Nullable PendingIntent action) {
|
||||
mDisabledClickAction = action;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the final {@link QCSlider}.
|
||||
*/
|
||||
public QCSlider build() {
|
||||
return new QCSlider(mMin, mMax, mValue, mIsEnabled, mIsClickableWhileDisabled,
|
||||
mInputAction, mDisabledClickAction);
|
||||
}
|
||||
}
|
||||
}
|
||||
222
car-qc-lib/src/com/android/car/qc/QCTile.java
Normal file
222
car-qc-lib/src/com/android/car/qc/QCTile.java
Normal file
@@ -0,0 +1,222 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.car.qc;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Parcel;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Quick Control Tile Element
|
||||
* ------------
|
||||
* | -------- |
|
||||
* | | Icon | |
|
||||
* | -------- |
|
||||
* | Subtitle |
|
||||
* ------------
|
||||
*/
|
||||
public class QCTile extends QCItem {
|
||||
private final boolean mIsChecked;
|
||||
private final boolean mIsAvailable;
|
||||
private final String mSubtitle;
|
||||
private Icon mIcon;
|
||||
private PendingIntent mAction;
|
||||
private PendingIntent mDisabledClickAction;
|
||||
|
||||
public QCTile(boolean isChecked, boolean isEnabled, boolean isAvailable,
|
||||
boolean isClickableWhileDisabled, @Nullable String subtitle, @Nullable Icon icon,
|
||||
@Nullable PendingIntent action, @Nullable PendingIntent disabledClickAction) {
|
||||
super(QC_TYPE_TILE, isEnabled, isClickableWhileDisabled);
|
||||
mIsChecked = isChecked;
|
||||
mIsAvailable = isAvailable;
|
||||
mSubtitle = subtitle;
|
||||
mIcon = icon;
|
||||
mAction = action;
|
||||
mDisabledClickAction = disabledClickAction;
|
||||
}
|
||||
|
||||
public QCTile(@NonNull Parcel in) {
|
||||
super(in);
|
||||
mIsChecked = in.readBoolean();
|
||||
mIsAvailable = in.readBoolean();
|
||||
mSubtitle = in.readString();
|
||||
boolean hasIcon = in.readBoolean();
|
||||
if (hasIcon) {
|
||||
mIcon = Icon.CREATOR.createFromParcel(in);
|
||||
}
|
||||
boolean hasAction = in.readBoolean();
|
||||
if (hasAction) {
|
||||
mAction = PendingIntent.CREATOR.createFromParcel(in);
|
||||
}
|
||||
boolean hasDisabledClickAction = in.readBoolean();
|
||||
if (hasDisabledClickAction) {
|
||||
mDisabledClickAction = PendingIntent.CREATOR.createFromParcel(in);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
super.writeToParcel(dest, flags);
|
||||
dest.writeBoolean(mIsChecked);
|
||||
dest.writeBoolean(mIsAvailable);
|
||||
dest.writeString(mSubtitle);
|
||||
boolean hasIcon = mIcon != null;
|
||||
dest.writeBoolean(hasIcon);
|
||||
if (hasIcon) {
|
||||
mIcon.writeToParcel(dest, flags);
|
||||
}
|
||||
boolean hasAction = mAction != null;
|
||||
dest.writeBoolean(hasAction);
|
||||
if (hasAction) {
|
||||
mAction.writeToParcel(dest, flags);
|
||||
}
|
||||
boolean hasDisabledClickAction = mDisabledClickAction != null;
|
||||
dest.writeBoolean(hasDisabledClickAction);
|
||||
if (hasDisabledClickAction) {
|
||||
mDisabledClickAction.writeToParcel(dest, flags);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent getPrimaryAction() {
|
||||
return mAction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent getDisabledClickAction() {
|
||||
return mDisabledClickAction;
|
||||
}
|
||||
|
||||
public boolean isChecked() {
|
||||
return mIsChecked;
|
||||
}
|
||||
|
||||
public boolean isAvailable() {
|
||||
return mIsAvailable;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getSubtitle() {
|
||||
return mSubtitle;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Icon getIcon() {
|
||||
return mIcon;
|
||||
}
|
||||
|
||||
public static Creator<QCTile> CREATOR = new Creator<QCTile>() {
|
||||
@Override
|
||||
public QCTile createFromParcel(Parcel source) {
|
||||
return new QCTile(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public QCTile[] newArray(int size) {
|
||||
return new QCTile[size];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Builder for {@link QCTile}.
|
||||
*/
|
||||
public static class Builder {
|
||||
private boolean mIsChecked;
|
||||
private boolean mIsEnabled = true;
|
||||
private boolean mIsAvailable = true;
|
||||
private boolean mIsClickableWhileDisabled = false;
|
||||
private String mSubtitle;
|
||||
private Icon mIcon;
|
||||
private PendingIntent mAction;
|
||||
private PendingIntent mDisabledClickAction;
|
||||
|
||||
/**
|
||||
* Sets whether or not the tile should be checked.
|
||||
*/
|
||||
public Builder setChecked(boolean checked) {
|
||||
mIsChecked = checked;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not the tile should be enabled.
|
||||
*/
|
||||
public Builder setEnabled(boolean enabled) {
|
||||
mIsEnabled = enabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not the action item is available.
|
||||
*/
|
||||
public Builder setAvailable(boolean available) {
|
||||
mIsAvailable = available;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not a tile should be clickable while disabled.
|
||||
*/
|
||||
public Builder setClickableWhileDisabled(boolean clickable) {
|
||||
mIsClickableWhileDisabled = clickable;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the tile's subtitle.
|
||||
*/
|
||||
public Builder setSubtitle(@Nullable String subtitle) {
|
||||
mSubtitle = subtitle;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the tile's icon.
|
||||
*/
|
||||
public Builder setIcon(@Nullable Icon icon) {
|
||||
mIcon = icon;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the PendingIntent to be sent when the tile is clicked.
|
||||
*/
|
||||
public Builder setAction(@Nullable PendingIntent action) {
|
||||
mAction = action;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the PendingIntent to be sent when the action item is clicked while disabled.
|
||||
*/
|
||||
public Builder setDisabledClickAction(@Nullable PendingIntent action) {
|
||||
mDisabledClickAction = action;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the final {@link QCTile}.
|
||||
*/
|
||||
public QCTile build() {
|
||||
return new QCTile(mIsChecked, mIsEnabled, mIsAvailable, mIsClickableWhileDisabled,
|
||||
mSubtitle, mIcon, mAction, mDisabledClickAction);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.car.qc.controller;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.lifecycle.Observer;
|
||||
|
||||
import com.android.car.qc.QCItem;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Base controller class for Quick Controls.
|
||||
*/
|
||||
public abstract class BaseQCController implements QCItemCallback {
|
||||
protected final Context mContext;
|
||||
protected final List<Observer<QCItem>> mObservers = new ArrayList<>();
|
||||
protected boolean mShouldListen = false;
|
||||
protected boolean mWasListening = false;
|
||||
protected QCItem mQCItem;
|
||||
|
||||
public BaseQCController(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update whether or not the controller should be listening to updates from the provider.
|
||||
*/
|
||||
public void listen(boolean shouldListen) {
|
||||
mShouldListen = shouldListen;
|
||||
updateListening();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a QCItem observer to the controller.
|
||||
*/
|
||||
@UiThread
|
||||
public void addObserver(Observer<QCItem> observer) {
|
||||
mObservers.add(observer);
|
||||
updateListening();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a QCItem observer from the controller.
|
||||
*/
|
||||
@UiThread
|
||||
public void removeObserver(Observer<QCItem> observer) {
|
||||
mObservers.remove(observer);
|
||||
updateListening();
|
||||
}
|
||||
|
||||
@UiThread
|
||||
@Override
|
||||
public void onQCItemUpdated(@Nullable QCItem item) {
|
||||
mQCItem = item;
|
||||
mObservers.forEach(o -> o.onChanged(mQCItem));
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the controller. This should be called when the controller is no longer needed so
|
||||
* the listeners can be cleaned up.
|
||||
*/
|
||||
public void destroy() {
|
||||
mShouldListen = false;
|
||||
mObservers.clear();
|
||||
updateListening();
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a single retrieval from the provider (without subscribing to live updates).
|
||||
*/
|
||||
public abstract void bind();
|
||||
|
||||
/**
|
||||
* Subclasses must override this method to handle a listening update.
|
||||
*/
|
||||
protected abstract void updateListening();
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.car.qc.controller;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.android.car.qc.provider.BaseLocalQCProvider;
|
||||
|
||||
/**
|
||||
* Controller for binding to local quick control providers.
|
||||
*/
|
||||
public class LocalQCController extends BaseQCController {
|
||||
|
||||
private final BaseLocalQCProvider mProvider;
|
||||
|
||||
private final BaseLocalQCProvider.Notifier mProviderNotifier =
|
||||
new BaseLocalQCProvider.Notifier() {
|
||||
@Override
|
||||
public void notifyUpdate() {
|
||||
if (mShouldListen && !mObservers.isEmpty()) {
|
||||
onQCItemUpdated(mProvider.getQCItem());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public LocalQCController(Context context, BaseLocalQCProvider provider) {
|
||||
super(context);
|
||||
mProvider = provider;
|
||||
mProvider.setNotifier(mProviderNotifier);
|
||||
mQCItem = mProvider.getQCItem();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind() {
|
||||
onQCItemUpdated(mProvider.getQCItem());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateListening() {
|
||||
boolean listen = mShouldListen && !mObservers.isEmpty();
|
||||
if (mWasListening != listen) {
|
||||
mWasListening = listen;
|
||||
mProvider.shouldListen(listen);
|
||||
if (listen) {
|
||||
mQCItem = mProvider.getQCItem();
|
||||
onQCItemUpdated(mQCItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
super.destroy();
|
||||
mProvider.onDestroy();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.car.qc.controller;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.car.qc.QCItem;
|
||||
|
||||
/**
|
||||
* Callback to be executed when a QCItem changes.
|
||||
*/
|
||||
public interface QCItemCallback {
|
||||
/**
|
||||
* Called when QCItem is updated.
|
||||
*
|
||||
* @param item The updated QCItem.
|
||||
*/
|
||||
void onQCItemUpdated(@Nullable QCItem item);
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.car.qc.controller;
|
||||
|
||||
import static com.android.car.qc.provider.BaseQCProvider.EXTRA_ITEM;
|
||||
import static com.android.car.qc.provider.BaseQCProvider.EXTRA_URI;
|
||||
import static com.android.car.qc.provider.BaseQCProvider.METHOD_BIND;
|
||||
import static com.android.car.qc.provider.BaseQCProvider.METHOD_DESTROY;
|
||||
import static com.android.car.qc.provider.BaseQCProvider.METHOD_SUBSCRIBE;
|
||||
import static com.android.car.qc.provider.BaseQCProvider.METHOD_UNSUBSCRIBE;
|
||||
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.Context;
|
||||
import android.database.ContentObserver;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerExecutor;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.Looper;
|
||||
import android.os.Parcelable;
|
||||
import android.os.RemoteException;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.android.car.qc.QCItem;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* Controller for binding to remote quick control providers.
|
||||
*/
|
||||
public class RemoteQCController extends BaseQCController {
|
||||
private static final String TAG = "RemoteQCController";
|
||||
private static final long PROVIDER_ANR_TIMEOUT = 3000L;
|
||||
|
||||
private final Uri mUri;
|
||||
private final Executor mBackgroundExecutor;
|
||||
private final HandlerThread mBackgroundHandlerThread;
|
||||
private final ArrayMap<Pair<Uri, QCItemCallback>, QCObserver> mObserverLookup =
|
||||
new ArrayMap<>();
|
||||
|
||||
public RemoteQCController(Context context, Uri uri) {
|
||||
super(context);
|
||||
mUri = uri;
|
||||
mBackgroundHandlerThread = new HandlerThread(/* name= */ TAG + "HandlerThread");
|
||||
mBackgroundHandlerThread.start();
|
||||
mBackgroundExecutor = new HandlerExecutor(
|
||||
new Handler(mBackgroundHandlerThread.getLooper()));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
RemoteQCController(Context context, Uri uri, Executor backgroundExecutor) {
|
||||
super(context);
|
||||
mUri = uri;
|
||||
mBackgroundHandlerThread = null;
|
||||
mBackgroundExecutor = backgroundExecutor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind() {
|
||||
mBackgroundExecutor.execute(this::updateQCItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateListening() {
|
||||
boolean listen = mShouldListen && !mObservers.isEmpty();
|
||||
mBackgroundExecutor.execute(() -> updateListeningBg(listen));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
super.destroy();
|
||||
if (mBackgroundHandlerThread != null) {
|
||||
mBackgroundHandlerThread.quit();
|
||||
}
|
||||
try (ContentProviderClient client = getClient()) {
|
||||
if (client == null) {
|
||||
return;
|
||||
}
|
||||
Bundle b = new Bundle();
|
||||
b.putParcelable(EXTRA_URI, mUri);
|
||||
try {
|
||||
client.call(METHOD_DESTROY, /* arg= */ null, b);
|
||||
} catch (Exception e) {
|
||||
Log.d(TAG, "Error destroying QCItem", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void updateListeningBg(boolean isListening) {
|
||||
if (mWasListening != isListening) {
|
||||
mWasListening = isListening;
|
||||
if (isListening) {
|
||||
registerQCCallback(mContext.getMainExecutor(), /* callback= */ this);
|
||||
// Update one-time on a different thread so that it can display in parallel
|
||||
mBackgroundExecutor.execute(this::updateQCItem);
|
||||
} else {
|
||||
unregisterQCCallback(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void updateQCItem() {
|
||||
try {
|
||||
QCItem item = getQCItem();
|
||||
mContext.getMainExecutor().execute(() -> onQCItemUpdated(item));
|
||||
} catch (Exception e) {
|
||||
Log.d(TAG, "Error fetching QCItem", e);
|
||||
}
|
||||
}
|
||||
|
||||
private QCItem getQCItem() {
|
||||
try (ContentProviderClient provider = getClient()) {
|
||||
if (provider == null) {
|
||||
return null;
|
||||
}
|
||||
Bundle extras = new Bundle();
|
||||
extras.putParcelable(EXTRA_URI, mUri);
|
||||
Bundle res = provider.call(METHOD_BIND, /* arg= */ null, extras);
|
||||
if (res == null) {
|
||||
return null;
|
||||
}
|
||||
res.setDefusable(true);
|
||||
res.setClassLoader(QCItem.class.getClassLoader());
|
||||
Parcelable parcelable = res.getParcelable(EXTRA_ITEM);
|
||||
if (parcelable instanceof QCItem) {
|
||||
return (QCItem) parcelable;
|
||||
}
|
||||
return null;
|
||||
} catch (RemoteException e) {
|
||||
Log.d(TAG, "Error binding QCItem", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void subscribe() {
|
||||
try (ContentProviderClient client = getClient()) {
|
||||
if (client == null) {
|
||||
return;
|
||||
}
|
||||
Bundle b = new Bundle();
|
||||
b.putParcelable(EXTRA_URI, mUri);
|
||||
try {
|
||||
client.call(METHOD_SUBSCRIBE, /* arg= */ null, b);
|
||||
} catch (Exception e) {
|
||||
Log.d(TAG, "Error subscribing to QCItem", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void unsubscribe() {
|
||||
try (ContentProviderClient client = getClient()) {
|
||||
if (client == null) {
|
||||
return;
|
||||
}
|
||||
Bundle b = new Bundle();
|
||||
b.putParcelable(EXTRA_URI, mUri);
|
||||
try {
|
||||
client.call(METHOD_UNSUBSCRIBE, /* arg= */ null, b);
|
||||
} catch (Exception e) {
|
||||
Log.d(TAG, "Error unsubscribing from QCItem", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
ContentProviderClient getClient() {
|
||||
ContentProviderClient client = mContext.getContentResolver()
|
||||
.acquireContentProviderClient(mUri);
|
||||
if (client == null) {
|
||||
return null;
|
||||
}
|
||||
client.setDetectNotResponding(PROVIDER_ANR_TIMEOUT);
|
||||
return client;
|
||||
}
|
||||
|
||||
private void registerQCCallback(@NonNull Executor executor, @NonNull QCItemCallback callback) {
|
||||
getObserver(callback, new QCObserver(mUri, executor, callback)).startObserving();
|
||||
}
|
||||
|
||||
private void unregisterQCCallback(@NonNull QCItemCallback callback) {
|
||||
synchronized (mObserverLookup) {
|
||||
QCObserver observer = mObserverLookup.remove(new Pair<>(mUri, callback));
|
||||
if (observer != null) {
|
||||
observer.stopObserving();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private QCObserver getObserver(QCItemCallback callback, QCObserver observer) {
|
||||
Pair<Uri, QCItemCallback> key = new Pair<>(mUri, callback);
|
||||
synchronized (mObserverLookup) {
|
||||
QCObserver oldObserver = mObserverLookup.put(key, observer);
|
||||
if (oldObserver != null) {
|
||||
oldObserver.stopObserving();
|
||||
}
|
||||
}
|
||||
return observer;
|
||||
}
|
||||
|
||||
private class QCObserver {
|
||||
private final Uri mUri;
|
||||
private final Executor mExecutor;
|
||||
private final QCItemCallback mCallback;
|
||||
private boolean mIsSubscribed;
|
||||
|
||||
private final Runnable mUpdateItem = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
trySubscribe();
|
||||
QCItem item = getQCItem();
|
||||
mExecutor.execute(() -> mCallback.onQCItemUpdated(item));
|
||||
}
|
||||
};
|
||||
|
||||
private final ContentObserver mObserver = new ContentObserver(
|
||||
new Handler(Looper.getMainLooper())) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
android.os.AsyncTask.execute(mUpdateItem);
|
||||
}
|
||||
};
|
||||
|
||||
QCObserver(Uri uri, Executor executor, QCItemCallback callback) {
|
||||
mUri = uri;
|
||||
mExecutor = executor;
|
||||
mCallback = callback;
|
||||
}
|
||||
|
||||
void startObserving() {
|
||||
ContentProviderClient provider =
|
||||
mContext.getContentResolver().acquireContentProviderClient(mUri);
|
||||
if (provider != null) {
|
||||
provider.close();
|
||||
mContext.getContentResolver().registerContentObserver(
|
||||
mUri, /* notifyForDescendants= */ true, mObserver);
|
||||
trySubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
void trySubscribe() {
|
||||
if (!mIsSubscribed) {
|
||||
subscribe();
|
||||
mIsSubscribed = true;
|
||||
}
|
||||
}
|
||||
|
||||
void stopObserving() {
|
||||
mContext.getContentResolver().unregisterContentObserver(mObserver);
|
||||
if (mIsSubscribed) {
|
||||
unsubscribe();
|
||||
mIsSubscribed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.car.qc.provider;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.android.car.qc.QCItem;
|
||||
|
||||
/**
|
||||
* Base class for local Quick Control providers.
|
||||
*/
|
||||
public abstract class BaseLocalQCProvider {
|
||||
|
||||
/**
|
||||
* Callback to be executed when the QCItem updates.
|
||||
*/
|
||||
public interface Notifier {
|
||||
/**
|
||||
* Called when the QCItem has been updated.
|
||||
*/
|
||||
default void notifyUpdate() {
|
||||
}
|
||||
}
|
||||
|
||||
private Notifier mNotifier;
|
||||
private boolean mIsListening;
|
||||
protected final Context mContext;
|
||||
|
||||
public BaseLocalQCProvider(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the notifier that should be called when the QCItem updates.
|
||||
*/
|
||||
public void setNotifier(Notifier notifier) {
|
||||
mNotifier = notifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update whether or not the provider should be listening for live updates.
|
||||
*/
|
||||
public void shouldListen(boolean listen) {
|
||||
if (mIsListening == listen) {
|
||||
return;
|
||||
}
|
||||
mIsListening = listen;
|
||||
if (listen) {
|
||||
onSubscribed();
|
||||
} else {
|
||||
onUnsubscribed();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to create and return a {@link QCItem}.
|
||||
*/
|
||||
public abstract QCItem getQCItem();
|
||||
|
||||
/**
|
||||
* Called to inform the provider that it has been subscribed to.
|
||||
*/
|
||||
protected void onSubscribed() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to inform the provider that it has been unsubscribed from.
|
||||
*/
|
||||
protected void onUnsubscribed() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to inform the provider that it is being destroyed.
|
||||
*/
|
||||
public void onDestroy() {
|
||||
}
|
||||
|
||||
protected void notifyChange() {
|
||||
if (mNotifier != null) {
|
||||
mNotifier.notifyUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
235
car-qc-lib/src/com/android/car/qc/provider/BaseQCProvider.java
Normal file
235
car-qc-lib/src/com/android/car/qc/provider/BaseQCProvider.java
Normal file
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.car.qc.provider;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Process;
|
||||
import android.os.StrictMode;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.car.qc.QCItem;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Base Quick Controls provider implementation.
|
||||
*/
|
||||
public abstract class BaseQCProvider extends ContentProvider {
|
||||
public static final String METHOD_BIND = "QC_METHOD_BIND";
|
||||
public static final String METHOD_SUBSCRIBE = "QC_METHOD_SUBSCRIBE";
|
||||
public static final String METHOD_UNSUBSCRIBE = "QC_METHOD_UNSUBSCRIBE";
|
||||
public static final String METHOD_DESTROY = "QC_METHOD_DESTROY";
|
||||
public static final String EXTRA_URI = "QC_EXTRA_URI";
|
||||
public static final String EXTRA_ITEM = "QC_EXTRA_ITEM";
|
||||
|
||||
private static final String TAG = "BaseQCProvider";
|
||||
private static final long QC_ANR_TIMEOUT = 3000L;
|
||||
private static final Handler MAIN_THREAD_HANDLER = new Handler(Looper.getMainLooper());
|
||||
private String mCallbackMethod;
|
||||
private final Runnable mAnr = () -> {
|
||||
Process.sendSignal(Process.myPid(), Process.SIGNAL_QUIT);
|
||||
Log.e(TAG, "Timed out while handling QC method " + mCallbackMethod);
|
||||
};
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle call(String method, String arg, Bundle extras) {
|
||||
enforceCallingPermissions();
|
||||
|
||||
Uri uri = getUriWithoutUserId(validateIncomingUriOrNull(
|
||||
extras.getParcelable(EXTRA_URI)));
|
||||
switch(method) {
|
||||
case METHOD_BIND:
|
||||
QCItem item = handleBind(uri);
|
||||
Bundle b = new Bundle();
|
||||
b.putParcelable(EXTRA_ITEM, item);
|
||||
return b;
|
||||
case METHOD_SUBSCRIBE:
|
||||
handleSubscribe(uri);
|
||||
break;
|
||||
case METHOD_UNSUBSCRIBE:
|
||||
handleUnsubscribe(uri);
|
||||
break;
|
||||
case METHOD_DESTROY:
|
||||
handleDestroy(uri);
|
||||
break;
|
||||
}
|
||||
return super.call(method, arg, extras);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
|
||||
String sortOrder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType(Uri uri) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(Uri uri, ContentValues values) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(Uri uri, String selection, String[] selectionArgs) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to create and return a {@link QCItem}.
|
||||
*
|
||||
* onBind is expected to return as quickly as possible. Therefore, no network or other IO
|
||||
* will be allowed. Any loading that needs to be done should happen in the background and
|
||||
* should then notify the content resolver of the change when ready to provide the
|
||||
* complete data in onBind.
|
||||
*/
|
||||
@Nullable
|
||||
protected QCItem onBind(@NonNull Uri uri) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to inform an app that an item has been subscribed to.
|
||||
*
|
||||
* Subscribing is a way that a host can notify apps of which QCItems they would like to
|
||||
* receive updates for. The providing apps are expected to keep the content up to date
|
||||
* and notify of change via the content resolver.
|
||||
*/
|
||||
protected void onSubscribed(@NonNull Uri uri) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to inform an app that an item has been unsubscribed from.
|
||||
*
|
||||
* This is used to notify providing apps that a host is no longer listening
|
||||
* to updates, so any background processes and/or listeners should be removed.
|
||||
*/
|
||||
protected void onUnsubscribed(@NonNull Uri uri) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to inform an app that an item is being destroyed.
|
||||
*
|
||||
* This is used to notify providing apps that a host is no longer going to use this QCItem
|
||||
* instance, so the relevant elements should be cleaned up.
|
||||
*/
|
||||
protected void onDestroy(@NonNull Uri uri) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Set of packages that are allowed to call this provider.
|
||||
*/
|
||||
@NonNull
|
||||
protected abstract Set<String> getAllowlistedPackages();
|
||||
|
||||
private QCItem handleBind(Uri uri) {
|
||||
mCallbackMethod = "handleBind";
|
||||
MAIN_THREAD_HANDLER.postDelayed(mAnr, QC_ANR_TIMEOUT);
|
||||
try {
|
||||
return onBindStrict(uri);
|
||||
} finally {
|
||||
MAIN_THREAD_HANDLER.removeCallbacks(mAnr);
|
||||
}
|
||||
}
|
||||
|
||||
private QCItem onBindStrict(@NonNull Uri uri) {
|
||||
StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
|
||||
try {
|
||||
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
|
||||
.detectAll()
|
||||
|
||||
// TODO(268275789): Revert back to penaltyDeath and ensure it works in
|
||||
// presubmit
|
||||
.penaltyLog()
|
||||
|
||||
.build());
|
||||
return onBind(uri);
|
||||
} finally {
|
||||
StrictMode.setThreadPolicy(oldPolicy);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSubscribe(@NonNull Uri uri) {
|
||||
mCallbackMethod = "handleSubscribe";
|
||||
MAIN_THREAD_HANDLER.postDelayed(mAnr, QC_ANR_TIMEOUT);
|
||||
try {
|
||||
onSubscribed(uri);
|
||||
} finally {
|
||||
MAIN_THREAD_HANDLER.removeCallbacks(mAnr);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleUnsubscribe(@NonNull Uri uri) {
|
||||
mCallbackMethod = "handleUnsubscribe";
|
||||
MAIN_THREAD_HANDLER.postDelayed(mAnr, QC_ANR_TIMEOUT);
|
||||
try {
|
||||
onUnsubscribed(uri);
|
||||
} finally {
|
||||
MAIN_THREAD_HANDLER.removeCallbacks(mAnr);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDestroy(@NonNull Uri uri) {
|
||||
mCallbackMethod = "handleDestroy";
|
||||
MAIN_THREAD_HANDLER.postDelayed(mAnr, QC_ANR_TIMEOUT);
|
||||
try {
|
||||
onDestroy(uri);
|
||||
} finally {
|
||||
MAIN_THREAD_HANDLER.removeCallbacks(mAnr);
|
||||
}
|
||||
}
|
||||
|
||||
private Uri validateIncomingUriOrNull(Uri uri) {
|
||||
if (uri == null) {
|
||||
throw new IllegalArgumentException("Uri cannot be null");
|
||||
}
|
||||
return validateIncomingUri(uri);
|
||||
}
|
||||
|
||||
private void enforceCallingPermissions() {
|
||||
String callingPackage = getCallingPackage();
|
||||
if (callingPackage == null) {
|
||||
throw new IllegalArgumentException("Calling package cannot be null");
|
||||
}
|
||||
if (!getAllowlistedPackages().contains(callingPackage)) {
|
||||
throw new SecurityException(
|
||||
String.format("%s is not permitted to access provider: %s", callingPackage,
|
||||
getClass().getName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
101
car-qc-lib/src/com/android/car/qc/view/QCListView.java
Normal file
101
car-qc-lib/src/com/android/car/qc/view/QCListView.java
Normal file
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.car.qc.view;
|
||||
|
||||
import static com.android.car.qc.view.QCView.QCActionListener;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.lifecycle.Observer;
|
||||
|
||||
import com.android.car.qc.QCItem;
|
||||
import com.android.car.qc.QCList;
|
||||
|
||||
/**
|
||||
* Quick Controls view for {@link QCList} instances.
|
||||
*/
|
||||
public class QCListView extends LinearLayout implements Observer<QCItem> {
|
||||
|
||||
private QCActionListener mActionListener;
|
||||
|
||||
public QCListView(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public QCListView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public QCListView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init();
|
||||
}
|
||||
|
||||
public QCListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
setOrientation(VERTICAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the view's {@link QCActionListener}. This listener will propagate to all QCRows.
|
||||
*/
|
||||
public void setActionListener(QCActionListener listener) {
|
||||
mActionListener = listener;
|
||||
for (int i = 0; i < getChildCount(); i++) {
|
||||
QCRowView view = (QCRowView) getChildAt(i);
|
||||
view.setActionListener(mActionListener);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChanged(QCItem qcItem) {
|
||||
if (qcItem == null) {
|
||||
removeAllViews();
|
||||
return;
|
||||
}
|
||||
if (!qcItem.getType().equals(QCItem.QC_TYPE_LIST)) {
|
||||
throw new IllegalArgumentException("Expected QCList type for QCListView but got "
|
||||
+ qcItem.getType());
|
||||
}
|
||||
QCList qcList = (QCList) qcItem;
|
||||
int rowCount = qcList.getRows().size();
|
||||
for (int i = 0; i < rowCount; i++) {
|
||||
if (getChildAt(i) != null) {
|
||||
QCRowView view = (QCRowView) getChildAt(i);
|
||||
view.setRow(qcList.getRows().get(i));
|
||||
view.setActionListener(mActionListener);
|
||||
} else {
|
||||
QCRowView view = new QCRowView(getContext());
|
||||
view.setRow(qcList.getRows().get(i));
|
||||
view.setActionListener(mActionListener);
|
||||
addView(view);
|
||||
}
|
||||
}
|
||||
if (getChildCount() > rowCount) {
|
||||
// remove extra rows
|
||||
removeViews(rowCount, getChildCount() - rowCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
538
car-qc-lib/src/com/android/car/qc/view/QCRowView.java
Normal file
538
car-qc-lib/src/com/android/car/qc/view/QCRowView.java
Normal file
@@ -0,0 +1,538 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.car.qc.view;
|
||||
|
||||
import static com.android.car.qc.QCItem.QC_ACTION_SLIDER_VALUE;
|
||||
import static com.android.car.qc.QCItem.QC_ACTION_TOGGLE_STATE;
|
||||
import static com.android.car.qc.QCItem.QC_TYPE_ACTION_SWITCH;
|
||||
import static com.android.car.qc.view.QCView.QCActionListener;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Handler;
|
||||
import android.text.BidiFormatter;
|
||||
import android.text.TextDirectionHeuristics;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.LayoutRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.car.qc.QCActionItem;
|
||||
import com.android.car.qc.QCItem;
|
||||
import com.android.car.qc.QCRow;
|
||||
import com.android.car.qc.QCSlider;
|
||||
import com.android.car.qc.R;
|
||||
import com.android.car.ui.utils.CarUiUtils;
|
||||
import com.android.car.ui.utils.DirectManipulationHelper;
|
||||
import com.android.car.ui.uxr.DrawableStateToggleButton;
|
||||
|
||||
/**
|
||||
* Quick Controls view for {@link QCRow} instances.
|
||||
*/
|
||||
public class QCRowView extends FrameLayout {
|
||||
private static final String TAG = "QCRowView";
|
||||
|
||||
private LayoutInflater mLayoutInflater;
|
||||
private BidiFormatter mBidiFormatter;
|
||||
private View mContentView;
|
||||
private TextView mTitle;
|
||||
private TextView mSubtitle;
|
||||
private ImageView mStartIcon;
|
||||
@ColorInt
|
||||
private int mStartIconTint;
|
||||
private LinearLayout mStartItemsContainer;
|
||||
private LinearLayout mEndItemsContainer;
|
||||
private LinearLayout mSeekBarContainer;
|
||||
@Nullable
|
||||
private QCSlider mQCSlider;
|
||||
private QCSeekBarView mSeekBar;
|
||||
private QCActionListener mActionListener;
|
||||
private boolean mInDirectManipulationMode;
|
||||
|
||||
private QCSeekbarChangeListener mSeekbarChangeListener;
|
||||
private final View.OnKeyListener mSeekBarKeyListener = new View.OnKeyListener() {
|
||||
@Override
|
||||
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||||
if (mSeekBar == null || (!mSeekBar.isEnabled()
|
||||
&& !mSeekBar.isClickableWhileDisabled())) {
|
||||
return false;
|
||||
}
|
||||
// Consume nudge events in direct manipulation mode.
|
||||
if (mInDirectManipulationMode
|
||||
&& (keyCode == KeyEvent.KEYCODE_DPAD_LEFT
|
||||
|| keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
|
||||
|| keyCode == KeyEvent.KEYCODE_DPAD_UP
|
||||
|| keyCode == KeyEvent.KEYCODE_DPAD_DOWN)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle events to enter or exit direct manipulation mode.
|
||||
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
|
||||
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||
if (mQCSlider != null) {
|
||||
if (mQCSlider.isEnabled()) {
|
||||
setInDirectManipulationMode(v, mSeekBar, !mInDirectManipulationMode);
|
||||
} else {
|
||||
fireAction(mQCSlider, new Intent());
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
if (mInDirectManipulationMode) {
|
||||
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||
setInDirectManipulationMode(v, mSeekBar, false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't propagate confirm keys to the SeekBar to prevent a ripple effect on the thumb.
|
||||
if (KeyEvent.isConfirmKey(keyCode)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||
return mSeekBar.onKeyDown(keyCode, event);
|
||||
} else {
|
||||
return mSeekBar.onKeyUp(keyCode, event);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final View.OnFocusChangeListener mSeekBarFocusChangeListener =
|
||||
(v, hasFocus) -> {
|
||||
if (!hasFocus && mInDirectManipulationMode && mSeekBar != null) {
|
||||
setInDirectManipulationMode(v, mSeekBar, false);
|
||||
}
|
||||
};
|
||||
|
||||
private final View.OnGenericMotionListener mSeekBarScrollListener =
|
||||
(v, event) -> {
|
||||
if (!mInDirectManipulationMode || mSeekBar == null) {
|
||||
return false;
|
||||
}
|
||||
int adjustment = Math.round(event.getAxisValue(MotionEvent.AXIS_SCROLL));
|
||||
if (adjustment == 0) {
|
||||
return false;
|
||||
}
|
||||
int count = Math.abs(adjustment);
|
||||
int keyCode =
|
||||
adjustment < 0 ? KeyEvent.KEYCODE_DPAD_LEFT : KeyEvent.KEYCODE_DPAD_RIGHT;
|
||||
KeyEvent downEvent = new KeyEvent(event.getDownTime(), event.getEventTime(),
|
||||
KeyEvent.ACTION_DOWN, keyCode, /* repeat= */ 0);
|
||||
KeyEvent upEvent = new KeyEvent(event.getDownTime(), event.getEventTime(),
|
||||
KeyEvent.ACTION_UP, keyCode, /* repeat= */ 0);
|
||||
for (int i = 0; i < count; i++) {
|
||||
mSeekBar.onKeyDown(keyCode, downEvent);
|
||||
mSeekBar.onKeyUp(keyCode, upEvent);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
QCRowView(Context context) {
|
||||
super(context);
|
||||
init(context);
|
||||
}
|
||||
|
||||
QCRowView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context);
|
||||
}
|
||||
|
||||
QCRowView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init(context);
|
||||
}
|
||||
|
||||
QCRowView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
init(context);
|
||||
}
|
||||
|
||||
private void init(Context context) {
|
||||
mLayoutInflater = LayoutInflater.from(context);
|
||||
mBidiFormatter = BidiFormatter.getInstance();
|
||||
mLayoutInflater.inflate(R.layout.qc_row_view, /* root= */ this);
|
||||
mContentView = findViewById(R.id.qc_row_content);
|
||||
mTitle = findViewById(R.id.qc_title);
|
||||
mSubtitle = findViewById(R.id.qc_summary);
|
||||
mStartIcon = findViewById(R.id.qc_icon);
|
||||
mStartItemsContainer = findViewById(R.id.qc_row_start_items);
|
||||
mEndItemsContainer = findViewById(R.id.qc_row_end_items);
|
||||
mSeekBarContainer = findViewById(R.id.qc_seekbar_wrapper);
|
||||
mSeekBar = findViewById(R.id.qc_seekbar);
|
||||
}
|
||||
|
||||
void setActionListener(QCActionListener listener) {
|
||||
mActionListener = listener;
|
||||
}
|
||||
|
||||
void setRow(QCRow row) {
|
||||
if (row == null) {
|
||||
setVisibility(GONE);
|
||||
return;
|
||||
}
|
||||
setVisibility(VISIBLE);
|
||||
CarUiUtils.makeAllViewsEnabled(mContentView, row.isEnabled());
|
||||
if (!row.isEnabled()) {
|
||||
if (row.isClickableWhileDisabled() && (row.getDisabledClickAction() != null
|
||||
|| row.getDisabledClickActionHandler() != null)) {
|
||||
mContentView.setOnClickListener(v -> {
|
||||
fireAction(row, /* intent= */ null);
|
||||
});
|
||||
}
|
||||
} else if (row.getPrimaryAction() != null || row.getActionHandler() != null) {
|
||||
mContentView.setOnClickListener(v -> {
|
||||
fireAction(row, /* intent= */ null);
|
||||
});
|
||||
}
|
||||
if (!TextUtils.isEmpty(row.getTitle())) {
|
||||
mTitle.setVisibility(VISIBLE);
|
||||
mTitle.setText(
|
||||
mBidiFormatter.unicodeWrap(row.getTitle(), TextDirectionHeuristics.LOCALE));
|
||||
} else {
|
||||
mTitle.setVisibility(GONE);
|
||||
}
|
||||
if (!TextUtils.isEmpty(row.getSubtitle())) {
|
||||
mSubtitle.setVisibility(VISIBLE);
|
||||
mSubtitle.setText(
|
||||
mBidiFormatter.unicodeWrap(row.getSubtitle(), TextDirectionHeuristics.LOCALE));
|
||||
} else {
|
||||
mSubtitle.setVisibility(GONE);
|
||||
}
|
||||
if (row.getStartIcon() != null) {
|
||||
mStartIcon.setVisibility(VISIBLE);
|
||||
Drawable drawable = row.getStartIcon().loadDrawable(getContext());
|
||||
if (drawable != null && row.isStartIconTintable()) {
|
||||
if (mStartIconTint == 0) {
|
||||
mStartIconTint = getContext().getColor(R.color.qc_start_icon_color);
|
||||
}
|
||||
drawable.setTint(mStartIconTint);
|
||||
}
|
||||
mStartIcon.setImageDrawable(drawable);
|
||||
} else {
|
||||
mStartIcon.setImageDrawable(null);
|
||||
mStartIcon.setVisibility(GONE);
|
||||
}
|
||||
QCSlider slider = row.getSlider();
|
||||
if (slider != null) {
|
||||
mSeekBarContainer.setVisibility(View.VISIBLE);
|
||||
initSlider(slider);
|
||||
} else {
|
||||
mSeekBarContainer.setVisibility(View.GONE);
|
||||
mQCSlider = null;
|
||||
}
|
||||
|
||||
int startItemCount = row.getStartItems().size();
|
||||
for (int i = 0; i < startItemCount; i++) {
|
||||
QCActionItem action = row.getStartItems().get(i);
|
||||
initActionItem(mStartItemsContainer, mStartItemsContainer.getChildAt(i), action);
|
||||
}
|
||||
if (mStartItemsContainer.getChildCount() > startItemCount) {
|
||||
// remove extra items
|
||||
mStartItemsContainer.removeViews(startItemCount,
|
||||
mStartItemsContainer.getChildCount() - startItemCount);
|
||||
}
|
||||
if (startItemCount == 0) {
|
||||
mStartItemsContainer.setVisibility(View.GONE);
|
||||
} else {
|
||||
mStartItemsContainer.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
int endItemCount = row.getEndItems().size();
|
||||
for (int i = 0; i < endItemCount; i++) {
|
||||
QCActionItem action = row.getEndItems().get(i);
|
||||
initActionItem(mEndItemsContainer, mEndItemsContainer.getChildAt(i), action);
|
||||
}
|
||||
if (mEndItemsContainer.getChildCount() > endItemCount) {
|
||||
// remove extra items
|
||||
mEndItemsContainer.removeViews(endItemCount,
|
||||
mEndItemsContainer.getChildCount() - endItemCount);
|
||||
}
|
||||
if (endItemCount == 0) {
|
||||
mEndItemsContainer.setVisibility(View.GONE);
|
||||
} else {
|
||||
mEndItemsContainer.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void initActionItem(@NonNull ViewGroup root, @Nullable View actionView,
|
||||
@NonNull QCActionItem action) {
|
||||
if (action.getType().equals(QC_TYPE_ACTION_SWITCH)) {
|
||||
initSwitchView(action, root, actionView);
|
||||
} else {
|
||||
initToggleView(action, root, actionView);
|
||||
}
|
||||
}
|
||||
|
||||
private void initSwitchView(QCActionItem action, ViewGroup root, View actionView) {
|
||||
Switch switchView = actionView == null ? null : actionView.findViewById(
|
||||
android.R.id.switch_widget);
|
||||
if (switchView == null) {
|
||||
actionView = createActionView(root, actionView, R.layout.qc_action_switch);
|
||||
switchView = actionView.requireViewById(android.R.id.switch_widget);
|
||||
}
|
||||
CarUiUtils.makeAllViewsEnabled(switchView, action.isEnabled());
|
||||
|
||||
boolean shouldEnableView =
|
||||
(action.isEnabled() || action.isClickableWhileDisabled()) && action.isAvailable()
|
||||
&& action.isClickable();
|
||||
switchView.setOnCheckedChangeListener(null);
|
||||
switchView.setEnabled(shouldEnableView);
|
||||
switchView.setChecked(action.isChecked());
|
||||
switchView.setContentDescription(action.getContentDescription());
|
||||
switchView.setOnTouchListener((v, event) -> {
|
||||
if (!action.isEnabled()) {
|
||||
if (event.getActionMasked() == MotionEvent.ACTION_UP) {
|
||||
fireAction(action, new Intent());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
switchView.setOnCheckedChangeListener(
|
||||
(buttonView, isChecked) -> {
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra(QC_ACTION_TOGGLE_STATE, isChecked);
|
||||
fireAction(action, intent);
|
||||
});
|
||||
}
|
||||
|
||||
private void initToggleView(QCActionItem action, ViewGroup root, View actionView) {
|
||||
DrawableStateToggleButton tmpToggleButton =
|
||||
actionView == null ? null : actionView.findViewById(R.id.qc_toggle_button);
|
||||
if (tmpToggleButton == null) {
|
||||
actionView = createActionView(root, actionView, R.layout.qc_action_toggle);
|
||||
tmpToggleButton = actionView.requireViewById(R.id.qc_toggle_button);
|
||||
}
|
||||
DrawableStateToggleButton toggleButton = tmpToggleButton; // must be effectively final
|
||||
boolean shouldEnableView =
|
||||
(action.isEnabled() || action.isClickableWhileDisabled()) && action.isAvailable()
|
||||
&& action.isClickable();
|
||||
toggleButton.setText(null);
|
||||
toggleButton.setTextOn(null);
|
||||
toggleButton.setTextOff(null);
|
||||
toggleButton.setOnCheckedChangeListener(null);
|
||||
Drawable icon = QCViewUtils.getToggleIcon(mContext, action.getIcon(), action.isAvailable());
|
||||
toggleButton.setContentDescription(action.getContentDescription());
|
||||
toggleButton.setButtonDrawable(icon);
|
||||
toggleButton.setChecked(action.isChecked());
|
||||
toggleButton.setEnabled(shouldEnableView);
|
||||
setToggleButtonDrawableState(toggleButton, action.isEnabled(), action.isAvailable());
|
||||
toggleButton.setOnTouchListener((v, event) -> {
|
||||
if (!action.isEnabled()) {
|
||||
if (event.getActionMasked() == MotionEvent.ACTION_UP) {
|
||||
fireAction(action, new Intent());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
toggleButton.setOnCheckedChangeListener(
|
||||
(buttonView, isChecked) -> {
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra(QC_ACTION_TOGGLE_STATE, isChecked);
|
||||
fireAction(action, intent);
|
||||
});
|
||||
}
|
||||
|
||||
private void setToggleButtonDrawableState(DrawableStateToggleButton view,
|
||||
boolean enabled, boolean available) {
|
||||
int[] statesToAdd = null;
|
||||
int[] statesToRemove = null;
|
||||
if (enabled) {
|
||||
if (!available) {
|
||||
statesToAdd =
|
||||
new int[]{android.R.attr.state_enabled, R.attr.state_toggle_unavailable};
|
||||
} else {
|
||||
statesToAdd = new int[]{android.R.attr.state_enabled};
|
||||
statesToRemove = new int[]{R.attr.state_toggle_unavailable};
|
||||
}
|
||||
} else {
|
||||
if (available) {
|
||||
statesToRemove =
|
||||
new int[]{android.R.attr.state_enabled, R.attr.state_toggle_unavailable};
|
||||
} else {
|
||||
statesToAdd = new int[]{R.attr.state_toggle_unavailable};
|
||||
statesToRemove = new int[]{android.R.attr.state_enabled};
|
||||
}
|
||||
}
|
||||
CarUiUtils.applyDrawableStatesToAllViews(view, statesToAdd, statesToRemove);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private View createActionView(@NonNull ViewGroup root, @Nullable View actionView,
|
||||
@LayoutRes int resId) {
|
||||
if (actionView != null) {
|
||||
// remove current action view
|
||||
root.removeView(actionView);
|
||||
}
|
||||
actionView = mLayoutInflater.inflate(resId, root, /* attachToRoot= */ false);
|
||||
root.addView(actionView);
|
||||
return actionView;
|
||||
}
|
||||
|
||||
private void initSlider(QCSlider slider) {
|
||||
mQCSlider = slider;
|
||||
CarUiUtils.makeAllViewsEnabled(mSeekBar, slider.isEnabled());
|
||||
|
||||
mSeekBar.setOnSeekBarChangeListener(null);
|
||||
mSeekBar.setMin(slider.getMin());
|
||||
mSeekBar.setMax(slider.getMax());
|
||||
mSeekBar.setProgress(slider.getValue());
|
||||
mSeekBar.setEnabled(slider.isEnabled());
|
||||
mSeekBar.setClickableWhileDisabled(slider.isClickableWhileDisabled());
|
||||
mSeekBar.setDisabledClickListener(seekBar -> fireAction(slider, new Intent()));
|
||||
if (!slider.isEnabled() && mInDirectManipulationMode) {
|
||||
setInDirectManipulationMode(mSeekBarContainer, mSeekBar, false);
|
||||
}
|
||||
if (mSeekbarChangeListener == null) {
|
||||
mSeekbarChangeListener = new QCSeekbarChangeListener();
|
||||
}
|
||||
mSeekbarChangeListener.setSlider(slider);
|
||||
mSeekBar.setOnSeekBarChangeListener(mSeekbarChangeListener);
|
||||
// set up rotary support
|
||||
mSeekBarContainer.setOnKeyListener(mSeekBarKeyListener);
|
||||
mSeekBarContainer.setOnFocusChangeListener(mSeekBarFocusChangeListener);
|
||||
mSeekBarContainer.setOnGenericMotionListener(mSeekBarScrollListener);
|
||||
}
|
||||
|
||||
private void setInDirectManipulationMode(View view, SeekBar seekbar, boolean enable) {
|
||||
mInDirectManipulationMode = enable;
|
||||
DirectManipulationHelper.enableDirectManipulationMode(seekbar, enable);
|
||||
view.setSelected(enable);
|
||||
seekbar.setSelected(enable);
|
||||
}
|
||||
|
||||
private void fireAction(QCItem item, Intent intent) {
|
||||
if (!item.isEnabled()) {
|
||||
if (item.getDisabledClickAction() != null) {
|
||||
try {
|
||||
item.getDisabledClickAction().send(getContext(), 0, intent);
|
||||
if (mActionListener != null) {
|
||||
mActionListener.onQCAction(item, item.getDisabledClickAction());
|
||||
}
|
||||
} catch (PendingIntent.CanceledException e) {
|
||||
Log.d(TAG, "Error sending intent", e);
|
||||
}
|
||||
} else if (item.getDisabledClickActionHandler() != null) {
|
||||
item.getDisabledClickActionHandler().onAction(item, getContext(), intent);
|
||||
if (mActionListener != null) {
|
||||
mActionListener.onQCAction(item, item.getDisabledClickActionHandler());
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.getPrimaryAction() != null) {
|
||||
try {
|
||||
item.getPrimaryAction().send(getContext(), 0, intent);
|
||||
if (mActionListener != null) {
|
||||
mActionListener.onQCAction(item, item.getPrimaryAction());
|
||||
}
|
||||
} catch (PendingIntent.CanceledException e) {
|
||||
Log.d(TAG, "Error sending intent", e);
|
||||
}
|
||||
} else if (item.getActionHandler() != null) {
|
||||
item.getActionHandler().onAction(item, getContext(), intent);
|
||||
if (mActionListener != null) {
|
||||
mActionListener.onQCAction(item, item.getActionHandler());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class QCSeekbarChangeListener implements SeekBar.OnSeekBarChangeListener {
|
||||
// Interval of updates (in ms) sent in response to seekbar moving.
|
||||
private static final int SLIDER_UPDATE_INTERVAL = 200;
|
||||
|
||||
private final Handler mSliderUpdateHandler;
|
||||
private QCSlider mSlider;
|
||||
private int mCurrSliderValue;
|
||||
private boolean mSliderUpdaterRunning;
|
||||
private long mLastSentSliderUpdate;
|
||||
private final Runnable mSliderUpdater = () -> {
|
||||
sendSliderValue();
|
||||
mSliderUpdaterRunning = false;
|
||||
};
|
||||
|
||||
QCSeekbarChangeListener() {
|
||||
mSliderUpdateHandler = new Handler();
|
||||
}
|
||||
|
||||
void setSlider(QCSlider slider) {
|
||||
mSlider = slider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
mCurrSliderValue = progress;
|
||||
long now = System.currentTimeMillis();
|
||||
if (mLastSentSliderUpdate != 0
|
||||
&& now - mLastSentSliderUpdate > SLIDER_UPDATE_INTERVAL) {
|
||||
mSliderUpdaterRunning = false;
|
||||
mSliderUpdateHandler.removeCallbacks(mSliderUpdater);
|
||||
sendSliderValue();
|
||||
} else if (!mSliderUpdaterRunning) {
|
||||
mSliderUpdaterRunning = true;
|
||||
mSliderUpdateHandler.postDelayed(mSliderUpdater, SLIDER_UPDATE_INTERVAL);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
if (mSliderUpdaterRunning) {
|
||||
mSliderUpdaterRunning = false;
|
||||
mSliderUpdateHandler.removeCallbacks(mSliderUpdater);
|
||||
}
|
||||
mCurrSliderValue = seekBar.getProgress();
|
||||
sendSliderValue();
|
||||
}
|
||||
|
||||
private void sendSliderValue() {
|
||||
if (mSlider == null) {
|
||||
return;
|
||||
}
|
||||
mLastSentSliderUpdate = System.currentTimeMillis();
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra(QC_ACTION_SLIDER_VALUE, mCurrSliderValue);
|
||||
fireAction(mSlider, intent);
|
||||
}
|
||||
}
|
||||
}
|
||||
78
car-qc-lib/src/com/android/car/qc/view/QCSeekBarView.java
Normal file
78
car-qc-lib/src/com/android/car/qc/view/QCSeekBarView.java
Normal file
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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.qc.view;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.widget.SeekBar;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.car.ui.uxr.DrawableStateSeekBar;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* A {@link SeekBar} specifically for Quick Controls that allows for a disabled click action
|
||||
* to execute on {@link MotionEvent.ACTION_UP}.
|
||||
*/
|
||||
public class QCSeekBarView extends DrawableStateSeekBar {
|
||||
private boolean mClickableWhileDisabled;
|
||||
private Consumer<SeekBar> mDisabledClickListener;
|
||||
|
||||
public QCSeekBarView(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public QCSeekBarView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public QCSeekBarView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public QCSeekBarView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
// AbsSeekBar will ignore all touch events if not enabled. If this SeekBar should be
|
||||
// clickable while disabled, the touch event will be handled here.
|
||||
if (!isEnabled() && mClickableWhileDisabled) {
|
||||
if (event.getAction() == MotionEvent.ACTION_UP && mDisabledClickListener != null) {
|
||||
mDisabledClickListener.accept(this);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return super.onTouchEvent(event);
|
||||
}
|
||||
|
||||
public void setClickableWhileDisabled(boolean clickable) {
|
||||
mClickableWhileDisabled = clickable;
|
||||
}
|
||||
|
||||
public void setDisabledClickListener(@Nullable Consumer<SeekBar> disabledClickListener) {
|
||||
mDisabledClickListener = disabledClickListener;
|
||||
}
|
||||
|
||||
public boolean isClickableWhileDisabled() {
|
||||
return mClickableWhileDisabled;
|
||||
}
|
||||
}
|
||||
151
car-qc-lib/src/com/android/car/qc/view/QCTileView.java
Normal file
151
car-qc-lib/src/com/android/car/qc/view/QCTileView.java
Normal file
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.car.qc.view;
|
||||
|
||||
import static com.android.car.qc.QCItem.QC_ACTION_TOGGLE_STATE;
|
||||
import static com.android.car.qc.view.QCView.QCActionListener;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.lifecycle.Observer;
|
||||
|
||||
import com.android.car.qc.QCItem;
|
||||
import com.android.car.qc.QCTile;
|
||||
import com.android.car.qc.R;
|
||||
import com.android.car.ui.utils.CarUiUtils;
|
||||
import com.android.car.ui.uxr.DrawableStateToggleButton;
|
||||
|
||||
/**
|
||||
* Quick Controls view for {@link QCTile} instances.
|
||||
*/
|
||||
public class QCTileView extends FrameLayout implements Observer<QCItem> {
|
||||
private static final String TAG = "QCTileView";
|
||||
|
||||
private View mTileWrapper;
|
||||
private DrawableStateToggleButton mToggleButton;
|
||||
private TextView mSubtitle;
|
||||
private QCActionListener mActionListener;
|
||||
|
||||
public QCTileView(Context context) {
|
||||
super(context);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public QCTileView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public QCTileView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public QCTileView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
init(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the tile's {@link QCActionListener}.
|
||||
*/
|
||||
public void setActionListener(QCActionListener listener) {
|
||||
mActionListener = listener;
|
||||
}
|
||||
|
||||
private void init(Context context) {
|
||||
View.inflate(context, R.layout.qc_tile_view, /* root= */ this);
|
||||
mTileWrapper = findViewById(R.id.qc_tile_wrapper);
|
||||
mToggleButton = findViewById(R.id.qc_tile_toggle_button);
|
||||
mSubtitle = findViewById(android.R.id.summary);
|
||||
mToggleButton.setText(null);
|
||||
mToggleButton.setTextOn(null);
|
||||
mToggleButton.setTextOff(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChanged(QCItem qcItem) {
|
||||
if (qcItem == null) {
|
||||
removeAllViews();
|
||||
return;
|
||||
}
|
||||
if (!qcItem.getType().equals(QCItem.QC_TYPE_TILE)) {
|
||||
throw new IllegalArgumentException("Expected QCTile type for QCTileView but got "
|
||||
+ qcItem.getType());
|
||||
}
|
||||
QCTile qcTile = (QCTile) qcItem;
|
||||
mSubtitle.setText(qcTile.getSubtitle());
|
||||
CarUiUtils.makeAllViewsEnabled(mToggleButton, qcTile.isEnabled());
|
||||
mToggleButton.setOnCheckedChangeListener(null);
|
||||
mToggleButton.setChecked(qcTile.isChecked());
|
||||
mToggleButton.setEnabled(qcTile.isEnabled() || qcTile.isClickableWhileDisabled());
|
||||
mTileWrapper.setEnabled(
|
||||
(qcTile.isEnabled() || qcTile.isClickableWhileDisabled()) && qcTile.isAvailable());
|
||||
mTileWrapper.setOnClickListener(v -> {
|
||||
if (!qcTile.isEnabled()) {
|
||||
if (qcTile.getDisabledClickAction() != null) {
|
||||
try {
|
||||
qcTile.getDisabledClickAction().send(getContext(), 0, new Intent());
|
||||
if (mActionListener != null) {
|
||||
mActionListener.onQCAction(qcTile, qcTile.getDisabledClickAction());
|
||||
}
|
||||
} catch (PendingIntent.CanceledException e) {
|
||||
Log.d(TAG, "Error sending intent", e);
|
||||
}
|
||||
} else if (qcTile.getDisabledClickActionHandler() != null) {
|
||||
qcTile.getDisabledClickActionHandler().onAction(qcTile, getContext(),
|
||||
new Intent());
|
||||
if (mActionListener != null) {
|
||||
mActionListener.onQCAction(qcTile, qcTile.getDisabledClickActionHandler());
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
mToggleButton.toggle();
|
||||
});
|
||||
Drawable icon = QCViewUtils.getToggleIcon(mContext, qcTile.getIcon(), qcTile.isAvailable());
|
||||
mToggleButton.setButtonDrawable(icon);
|
||||
mToggleButton.setOnCheckedChangeListener(
|
||||
(buttonView, isChecked) -> {
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra(QC_ACTION_TOGGLE_STATE, isChecked);
|
||||
if (qcTile.getPrimaryAction() != null) {
|
||||
try {
|
||||
qcTile.getPrimaryAction().send(getContext(), 0, intent);
|
||||
if (mActionListener != null) {
|
||||
mActionListener.onQCAction(qcTile, qcTile.getPrimaryAction());
|
||||
}
|
||||
} catch (PendingIntent.CanceledException e) {
|
||||
Log.d(TAG, "Error sending intent", e);
|
||||
}
|
||||
} else if (qcTile.getActionHandler() != null) {
|
||||
qcTile.getActionHandler().onAction(qcTile, getContext(), intent);
|
||||
if (mActionListener != null) {
|
||||
mActionListener.onQCAction(qcTile, qcTile.getActionHandler());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
120
car-qc-lib/src/com/android/car/qc/view/QCView.java
Normal file
120
car-qc-lib/src/com/android/car/qc/view/QCView.java
Normal file
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.car.qc.view;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Gravity;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.Observer;
|
||||
|
||||
import com.android.car.qc.QCItem;
|
||||
|
||||
/**
|
||||
* Base Quick Controls View - supports {@link QCItem.QC_TYPE_TILE} and {@link QCItem.QC_TYPE_LIST}
|
||||
*/
|
||||
public class QCView extends FrameLayout implements Observer<QCItem> {
|
||||
@QCItem.QCItemType
|
||||
private String mType;
|
||||
private Observer<QCItem> mChildObserver;
|
||||
private QCActionListener mActionListener;
|
||||
|
||||
public QCView(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public QCView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public QCView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public QCView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the view's {@link QCActionListener}. This listener will propagate to all sub-views.
|
||||
*/
|
||||
public void setActionListener(QCActionListener listener) {
|
||||
mActionListener = listener;
|
||||
if (mChildObserver instanceof QCTileView) {
|
||||
((QCTileView) mChildObserver).setActionListener(mActionListener);
|
||||
} else if (mChildObserver instanceof QCListView) {
|
||||
((QCListView) mChildObserver).setActionListener(mActionListener);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChanged(QCItem qcItem) {
|
||||
if (qcItem == null) {
|
||||
removeAllViews();
|
||||
mChildObserver = null;
|
||||
mType = null;
|
||||
return;
|
||||
}
|
||||
if (!isValidQCItemType(qcItem)) {
|
||||
throw new IllegalArgumentException("Expected QCTile or QCList type but got "
|
||||
+ qcItem.getType());
|
||||
}
|
||||
if (qcItem.getType().equals(mType)) {
|
||||
mChildObserver.onChanged(qcItem);
|
||||
return;
|
||||
}
|
||||
removeAllViews();
|
||||
mType = qcItem.getType();
|
||||
if (mType.equals(QCItem.QC_TYPE_TILE)) {
|
||||
QCTileView view = new QCTileView(getContext());
|
||||
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
|
||||
LayoutParams.WRAP_CONTENT,
|
||||
LayoutParams.WRAP_CONTENT,
|
||||
Gravity.CENTER_HORIZONTAL);
|
||||
view.onChanged(qcItem);
|
||||
view.setActionListener(mActionListener);
|
||||
addView(view, params);
|
||||
mChildObserver = view;
|
||||
} else {
|
||||
QCListView view = new QCListView(getContext());
|
||||
view.onChanged(qcItem);
|
||||
view.setActionListener(mActionListener);
|
||||
addView(view);
|
||||
mChildObserver = view;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isValidQCItemType(QCItem qcItem) {
|
||||
String type = qcItem.getType();
|
||||
return type.equals(QCItem.QC_TYPE_TILE) || type.equals(QCItem.QC_TYPE_LIST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener to be called when an action occurs on a QCView.
|
||||
*/
|
||||
public interface QCActionListener {
|
||||
/**
|
||||
* Called when an interaction has occurred with an element in this view.
|
||||
* @param item the specific item within the {@link QCItem} that was interacted with.
|
||||
* @param action the action that was executed - is generally either a
|
||||
* {@link android.app.PendingIntent} or {@link QCItem.ActionHandler}
|
||||
*/
|
||||
void onQCAction(@NonNull QCItem item, @NonNull Object action);
|
||||
}
|
||||
}
|
||||
74
car-qc-lib/src/com/android/car/qc/view/QCViewUtils.java
Normal file
74
car-qc-lib/src/com/android/car/qc/view/QCViewUtils.java
Normal file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.car.qc.view;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.car.qc.R;
|
||||
|
||||
/**
|
||||
* Utility class used by {@link QCTileView} and {@link QCRowView}
|
||||
*/
|
||||
public class QCViewUtils {
|
||||
|
||||
/**
|
||||
* Create a return a Quick Control toggle icon - used for tiles and action toggles.
|
||||
*/
|
||||
public static Drawable getToggleIcon(@NonNull Context context, @Nullable Icon icon,
|
||||
boolean available) {
|
||||
Drawable defaultToggleBackground = context.getDrawable(R.drawable.qc_toggle_background);
|
||||
Drawable unavailableToggleBackground = context.getDrawable(
|
||||
R.drawable.qc_toggle_unavailable_background);
|
||||
int toggleForegroundIconInset = context.getResources()
|
||||
.getDimensionPixelSize(R.dimen.qc_toggle_foreground_icon_inset);
|
||||
|
||||
Drawable background = available
|
||||
? defaultToggleBackground.getConstantState().newDrawable().mutate()
|
||||
: unavailableToggleBackground.getConstantState().newDrawable().mutate();
|
||||
if (icon == null) {
|
||||
return background;
|
||||
}
|
||||
|
||||
Drawable iconDrawable = icon.loadDrawable(context);
|
||||
if (iconDrawable == null) {
|
||||
return background;
|
||||
}
|
||||
|
||||
if (!available) {
|
||||
int unavailableToggleIconTint = context.getColor(R.color.qc_toggle_unavailable_color);
|
||||
iconDrawable.setTint(unavailableToggleIconTint);
|
||||
} else {
|
||||
ColorStateList defaultToggleIconTint = context.getColorStateList(
|
||||
R.color.qc_toggle_icon_fill_color);
|
||||
iconDrawable.setTintList(defaultToggleIconTint);
|
||||
}
|
||||
|
||||
Drawable[] layers = {background, iconDrawable};
|
||||
LayerDrawable drawable = new LayerDrawable(layers);
|
||||
drawable.setLayerInsetRelative(/* index= */ 1, toggleForegroundIconInset,
|
||||
toggleForegroundIconInset, toggleForegroundIconInset,
|
||||
toggleForegroundIconInset);
|
||||
return drawable;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user