diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java index 26cead2063104..c9f2401fd16a9 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java @@ -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(']'); } diff --git a/packages/SystemUI/res/layout/qs_tile_label.xml b/packages/SystemUI/res/layout/qs_tile_label.xml index 1ce87c1c02360..f8812ba894faa 100644 --- a/packages/SystemUI/res/layout/qs_tile_label.xml +++ b/packages/SystemUI/res/layout/qs_tile_label.xml @@ -16,8 +16,9 @@ --> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 7fc9ab1b0f240..5a21b30b9aab4 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1437,4 +1437,6 @@ 208dp 1dp 24dp + 32dp + 50dp diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewHorizontal.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewHorizontal.kt index ea6876f5999b0..7a8b2c696fa0e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewHorizontal.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewHorizontal.kt @@ -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) {} } \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java index 60c5d1cafde95..032e47e657301 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java @@ -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 { + 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 { 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 { mKeyguardStateController = keyguardStateController; mPackageManager = packageManager; mSecureSettings = secureSettings; + mExecutor = executor; mFeatureFlags = featureFlags; } @@ -86,6 +106,14 @@ public class QuickAccessWalletTile extends QSTileImpl { 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 { } else { state.state = Tile.STATE_UNAVAILABLE; } + state.sideViewDrawable = mCardViewDrawable; } @Override @@ -133,4 +162,40 @@ public class QuickAccessWalletTile extends QSTileImpl { 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 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(); + } + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QuickAccessWalletTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QuickAccessWalletTileTest.java index 33166ccadc27d..96d797903a23d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QuickAccessWalletTileTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QuickAccessWalletTileTest.java @@ -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 mRequestCaptor; + @Captor + ArgumentCaptor 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(); + } }