Add card image to the QuickAccessWalletTile.

We fetch the wallet cards on handleUpdateState() of the Tile, if the
card image exists, it will be stored in the QS State and then pass to
the QSTileViewHorizontal so the card image will be presented on the
tile.
The car image won't show on the tile when the user is edittng the tiles.

Test: manually tested on device
locked - https://hsv.googleplex.com/5756150307880960
unlocked - https://hsv.googleplex.com/5767433589620736
Test: atest
Bug: b/182963608

Change-Id: I45bab06528a7154419ae73f9e576cf385bf0f7c1
This commit is contained in:
Silin Huang
2021-04-04 23:09:24 -07:00
parent d0f987eeb0
commit 807cca09df
6 changed files with 243 additions and 2 deletions

View File

@@ -143,6 +143,7 @@ public interface QSTile {
public SlashState slash;
public boolean handlesLongClick = true;
public boolean showRippleEffect = true;
public Drawable sideViewDrawable;
public boolean copyTo(State other) {
if (other == null) throw new IllegalArgumentException();
@@ -163,7 +164,8 @@ public interface QSTile {
|| !Objects.equals(other.dualTarget, dualTarget)
|| !Objects.equals(other.slash, slash)
|| !Objects.equals(other.handlesLongClick, handlesLongClick)
|| !Objects.equals(other.showRippleEffect, showRippleEffect);
|| !Objects.equals(other.showRippleEffect, showRippleEffect)
|| !Objects.equals(other.sideViewDrawable, sideViewDrawable);
other.icon = icon;
other.iconSupplier = iconSupplier;
other.label = label;
@@ -179,6 +181,7 @@ public interface QSTile {
other.slash = slash != null ? slash.copy() : null;
other.handlesLongClick = handlesLongClick;
other.showRippleEffect = showRippleEffect;
other.sideViewDrawable = sideViewDrawable;
return changed;
}
@@ -204,6 +207,7 @@ public interface QSTile {
sb.append(",isTransient=").append(isTransient);
sb.append(",state=").append(state);
sb.append(",slash=\"").append(slash).append("\"");
sb.append(",sideViewDrawable").append(sideViewDrawable);
return sb.append(']');
}

View File

@@ -16,8 +16,9 @@
-->
<com.android.systemui.qs.tileimpl.ButtonRelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:clipChildren="false"
android:clipToPadding="false"
android:paddingTop="12dp">

View File

@@ -1437,4 +1437,6 @@
<dimen name="min_wallet_empty_height">208dp</dimen>
<dimen name="wallet_card_border_width">1dp</dimen>
<dimen name="wallet_empty_state_corner_radius">24dp</dimen>
<dimen name="wallet_tile_card_view_height">32dp</dimen>
<dimen name="wallet_tile_card_view_width">50dp</dimen>
</resources>

View File

@@ -24,6 +24,8 @@ import android.graphics.drawable.Drawable
import android.graphics.drawable.RippleDrawable
import android.service.quicksettings.Tile.STATE_ACTIVE
import android.view.Gravity
import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.RelativeLayout
import com.android.systemui.R
@@ -41,6 +43,7 @@ open class QSTileViewHorizontal(
private var paintColor = Color.WHITE
private var paintAnimator: ValueAnimator? = null
private var labelAnimator: ValueAnimator? = null
private var mSideView: ImageView = ImageView(mContext)
override var heightOverride: Int = HeightOverrideable.NO_OVERRIDE
init {
@@ -56,6 +59,14 @@ open class QSTileViewHorizontal(
val iconSize = context.resources.getDimensionPixelSize(R.dimen.qs_icon_size)
addView(mIcon, 0, LayoutParams(iconSize, iconSize))
mSideView.visibility = View.GONE
addView(
mSideView,
-1,
LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply {
gravity = Gravity.CENTER_VERTICAL
})
mColorLabelActive = ColorStateList.valueOf(getColorForState(getContext(), STATE_ACTIVE))
changeLabelColor(getLabelColor(mState)) // Matches the default state of the tile
}
@@ -128,6 +139,7 @@ open class QSTileViewHorizontal(
}
paintColor = newColor
}
loadSideViewDrawableIfNecessary(state)
}
private fun animateBackground(newBackgroundColor: Int) {
@@ -180,5 +192,21 @@ open class QSTileViewHorizontal(
labelAnimator?.cancel()?.also { labelAnimator = null }
}
private fun loadSideViewDrawableIfNecessary(state: QSTile.State) {
if (state.sideViewDrawable != null) {
(mSideView.layoutParams as MarginLayoutParams).apply {
marginStart =
context.resources.getDimensionPixelSize(R.dimen.qs_label_container_margin)
}
mSideView.setImageDrawable(state.sideViewDrawable)
mSideView.visibility = View.VISIBLE
mSideView.adjustViewBounds = true
mSideView.scaleType = ImageView.ScaleType.FIT_CENTER
} else {
mSideView.setImageDrawable(null)
mSideView.visibility = GONE
}
}
override fun handleExpand(dualTarget: Boolean) {}
}

View File

@@ -20,11 +20,20 @@ import static android.provider.Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Looper;
import android.service.quickaccesswallet.GetWalletCardsError;
import android.service.quickaccesswallet.GetWalletCardsRequest;
import android.service.quickaccesswallet.GetWalletCardsResponse;
import android.service.quickaccesswallet.QuickAccessWalletClient;
import android.service.quickaccesswallet.WalletCard;
import android.service.quicksettings.Tile;
import android.util.Log;
import androidx.annotation.NonNull;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.Background;
@@ -40,20 +49,29 @@ import com.android.systemui.statusbar.FeatureFlags;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.util.settings.SecureSettings;
import java.util.List;
import java.util.concurrent.Executor;
import javax.inject.Inject;
/** Quick settings tile: Quick access wallet **/
public class QuickAccessWalletTile extends QSTileImpl<QSTile.State> {
private static final String TAG = "QuickAccessWalletTile";
private static final String FEATURE_CHROME_OS = "org.chromium.arc";
private final CharSequence mLabel = mContext.getString(R.string.wallet_title);
private final WalletCardRetriever mCardRetriever = new WalletCardRetriever();
// TODO(b/180959290): Re-create the QAW Client when the default NFC payment app changes.
private final QuickAccessWalletClient mQuickAccessWalletClient;
private final KeyguardStateController mKeyguardStateController;
private final PackageManager mPackageManager;
private final SecureSettings mSecureSettings;
private final Executor mExecutor;
private final FeatureFlags mFeatureFlags;
@VisibleForTesting Drawable mCardViewDrawable;
@Inject
public QuickAccessWalletTile(
QSHost host,
@@ -68,6 +86,7 @@ public class QuickAccessWalletTile extends QSTileImpl<QSTile.State> {
KeyguardStateController keyguardStateController,
PackageManager packageManager,
SecureSettings secureSettings,
@Background Executor executor,
FeatureFlags featureFlags) {
super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger,
statusBarStateController, activityStarter, qsLogger);
@@ -75,6 +94,7 @@ public class QuickAccessWalletTile extends QSTileImpl<QSTile.State> {
mKeyguardStateController = keyguardStateController;
mPackageManager = packageManager;
mSecureSettings = secureSettings;
mExecutor = executor;
mFeatureFlags = featureFlags;
}
@@ -86,6 +106,14 @@ public class QuickAccessWalletTile extends QSTileImpl<QSTile.State> {
return state;
}
@Override
protected void handleSetListening(boolean listening) {
super.handleSetListening(listening);
if (listening) {
queryWalletCards();
}
}
@Override
protected void handleClick() {
mActivityStarter.postStartActivityDismissingKeyguard(
@@ -108,6 +136,7 @@ public class QuickAccessWalletTile extends QSTileImpl<QSTile.State> {
} else {
state.state = Tile.STATE_UNAVAILABLE;
}
state.sideViewDrawable = mCardViewDrawable;
}
@Override
@@ -133,4 +162,40 @@ public class QuickAccessWalletTile extends QSTileImpl<QSTile.State> {
CharSequence qawLabel = mQuickAccessWalletClient.getServiceLabel();
return qawLabel == null ? mLabel : qawLabel;
}
private void queryWalletCards() {
int cardWidth =
mContext.getResources().getDimensionPixelSize(R.dimen.wallet_tile_card_view_width);
int cardHeight =
mContext.getResources().getDimensionPixelSize(R.dimen.wallet_tile_card_view_height);
int iconSizePx = mContext.getResources().getDimensionPixelSize(R.dimen.wallet_icon_size);
GetWalletCardsRequest request =
new GetWalletCardsRequest(cardWidth, cardHeight, iconSizePx, /* maxCards= */ 2);
mQuickAccessWalletClient.getWalletCards(mExecutor, request, mCardRetriever);
}
private class WalletCardRetriever implements
QuickAccessWalletClient.OnWalletCardsRetrievedCallback {
@Override
public void onWalletCardsRetrieved(@NonNull GetWalletCardsResponse response) {
Log.i(TAG, "Successfully retrieved wallet cards.");
List<WalletCard> cards = response.getWalletCards();
if (cards.isEmpty()) {
Log.d(TAG, "No wallet cards exist.");
mCardViewDrawable = null;
refreshState();
return;
}
mCardViewDrawable = cards.get(0).getCardImage().loadDrawable(mContext);
refreshState();
}
@Override
public void onWalletCardRetrievalError(@NonNull GetWalletCardsError error) {
Log.w(TAG, "Error retrieve wallet cards");
mCardViewDrawable = null;
refreshState();
}
}
}

View File

@@ -19,21 +19,36 @@ package com.android.systemui.qs.tiles;
import static android.content.pm.PackageManager.FEATURE_NFC_HOST_CARD_EMULATION;
import static android.provider.Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT;
import static com.google.common.truth.Truth.assertThat;
import static junit.framework.TestCase.assertEquals;
import static junit.framework.TestCase.assertFalse;
import static junit.framework.TestCase.assertNotNull;
import static junit.framework.TestCase.assertNull;
import static junit.framework.TestCase.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.drawable.Icon;
import android.os.Handler;
import android.service.quickaccesswallet.GetWalletCardsError;
import android.service.quickaccesswallet.GetWalletCardsRequest;
import android.service.quickaccesswallet.GetWalletCardsResponse;
import android.service.quickaccesswallet.QuickAccessWalletClient;
import android.service.quickaccesswallet.QuickAccessWalletService;
import android.service.quickaccesswallet.WalletCard;
import android.service.quicksettings.Tile;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
@@ -56,17 +71,30 @@ import com.android.systemui.statusbar.FeatureFlags;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.util.settings.SecureSettings;
import com.google.common.util.concurrent.MoreExecutors;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.Collections;
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@SmallTest
public class QuickAccessWalletTileTest extends SysuiTestCase {
private static final String CARD_ID = "card_id";
private static final Icon CARD_IMAGE =
Icon.createWithBitmap(Bitmap.createBitmap(70, 50, Bitmap.Config.ARGB_8888));
private final Intent mWalletIntent = new Intent(QuickAccessWalletService.ACTION_VIEW_WALLET)
.setComponent(new ComponentName(mContext.getPackageName(), "WalletActivity"));
@Mock
private QSTileHost mHost;
@Mock
@@ -88,6 +116,10 @@ public class QuickAccessWalletTileTest extends SysuiTestCase {
private SecureSettings mSecureSettings;
@Mock
private FeatureFlags mFeatureFlags;
@Captor
ArgumentCaptor<GetWalletCardsRequest> mRequestCaptor;
@Captor
ArgumentCaptor<QuickAccessWalletClient.OnWalletCardsRetrievedCallback> mCallbackCaptor;
private TestableLooper mTestableLooper;
private QuickAccessWalletTile mTile;
@@ -115,6 +147,7 @@ public class QuickAccessWalletTileTest extends SysuiTestCase {
mKeyguardStateController,
mPackageManager,
mSecureSettings,
MoreExecutors.directExecutor(),
mFeatureFlags);
}
@@ -204,4 +237,112 @@ public class QuickAccessWalletTileTest extends SysuiTestCase {
assertEquals(Tile.STATE_UNAVAILABLE, state.state);
}
@Test
public void testHandleSetListening_queryCards() {
mTile.handleSetListening(true);
verify(mQuickAccessWalletClient)
.getWalletCards(any(), mRequestCaptor.capture(), mCallbackCaptor.capture());
GetWalletCardsRequest request = mRequestCaptor.getValue();
assertEquals(
mContext.getResources().getDimensionPixelSize(R.dimen.wallet_tile_card_view_width),
request.getCardWidthPx());
assertEquals(
mContext.getResources().getDimensionPixelSize(R.dimen.wallet_tile_card_view_height),
request.getCardHeightPx());
assertEquals(2, request.getMaxCards());
assertThat(mCallbackCaptor.getValue()).isInstanceOf(
QuickAccessWalletClient.OnWalletCardsRetrievedCallback.class);
}
@Test
public void testHandleSetListening_queryCards_hasCards_updateSideViewDrawable() {
GetWalletCardsResponse response =
new GetWalletCardsResponse(
Collections.singletonList(createWalletCard(mContext)), 0);
mTile.handleSetListening(true);
verify(mQuickAccessWalletClient).getWalletCards(any(), any(), mCallbackCaptor.capture());
mCallbackCaptor.getValue().onWalletCardsRetrieved(response);
mTestableLooper.processAllMessages();
assertNotNull(mTile.getState().sideViewDrawable);
}
@Test
public void testState_queryCards_hasCards_then_noCards() {
GetWalletCardsResponse responseWithCards =
new GetWalletCardsResponse(
Collections.singletonList(createWalletCard(mContext)), 0);
GetWalletCardsResponse responseWithoutCards =
new GetWalletCardsResponse(Collections.EMPTY_LIST, 0);
mTile.handleSetListening(true);
verify(mQuickAccessWalletClient).getWalletCards(any(), any(), mCallbackCaptor.capture());
// query wallet cards, has cards
mCallbackCaptor.getValue().onWalletCardsRetrieved(responseWithCards);
mTestableLooper.processAllMessages();
assertNotNull(mTile.getState().sideViewDrawable);
mTile.handleSetListening(true);
verify(mQuickAccessWalletClient, times(2))
.getWalletCards(any(), any(), mCallbackCaptor.capture());
// query wallet cards, has no cards
mCallbackCaptor.getValue().onWalletCardsRetrieved(responseWithoutCards);
mTestableLooper.processAllMessages();
assertNull(mTile.getState().sideViewDrawable);
}
@Test
public void testHandleSetListening_queryCards_noCards_notUpdateSideViewDrawable() {
QSTile.State state = new QSTile.State();
GetWalletCardsResponse response = new GetWalletCardsResponse(Collections.EMPTY_LIST, 0);
mTile.handleSetListening(true);
verify(mQuickAccessWalletClient).getWalletCards(any(), any(), mCallbackCaptor.capture());
mCallbackCaptor.getValue().onWalletCardsRetrieved(response);
mTestableLooper.processAllMessages();
assertNull(mTile.getState().sideViewDrawable);
}
@Test
public void testHandleSetListening_queryCards_error_notUpdateSideViewDrawable() {
String errorMessage = "getWalletCardsError";
GetWalletCardsError error = new GetWalletCardsError(CARD_IMAGE, errorMessage);
mTile.handleSetListening(true);
verify(mQuickAccessWalletClient).getWalletCards(any(), any(), mCallbackCaptor.capture());
mCallbackCaptor.getValue().onWalletCardRetrievalError(error);
mTestableLooper.processAllMessages();
assertNull(mTile.getState().sideViewDrawable);
}
@Test
public void testHandleSetListening_notListening_notQueryCards() {
mTile.handleSetListening(false);
verifyZeroInteractions(mQuickAccessWalletClient);
}
private WalletCard createWalletCard(Context context) {
PendingIntent pendingIntent =
PendingIntent.getActivity(context, 0, mWalletIntent, PendingIntent.FLAG_IMMUTABLE);
return new WalletCard.Builder(CARD_ID, CARD_IMAGE, "description", pendingIntent).build();
}
}