Add an activity for wallet card carousel screen.
The WalletActivity initializes a WalletScreenController to query the wallet cards and present on the content view. The next step is to launch the new activity from QuickAccessWalletTile. Test: manually test on deivce Test: atest Bug: b/182964813 Change-Id: Idbf0918e111db0d00f3b60b8c26b968c961df202
This commit is contained in:
@@ -762,6 +762,17 @@
|
||||
android:visibleToInstantApps="true">
|
||||
</activity>
|
||||
|
||||
<activity android:name=".wallet.ui.WalletActivity"
|
||||
android:label="@string/wallet_title"
|
||||
android:theme="@style/Wallet.Theme"
|
||||
android:excludeFromRecents="true"
|
||||
android:showWhenLocked="true"
|
||||
android:showForAllUsers="true"
|
||||
android:launchMode="singleInstance"
|
||||
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboard|keyboardHidden"
|
||||
android:visibleToInstantApps="true">
|
||||
</activity>
|
||||
|
||||
<receiver android:name=".controls.management.ControlsRequestReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2019 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<size
|
||||
android:height="50dp"
|
||||
android:width="50dp" />
|
||||
<solid android:color="#1A73E8" />
|
||||
</shape>
|
||||
21
packages/SystemUI/res/drawable/rounded_corners.xml
Normal file
21
packages/SystemUI/res/drawable/rounded_corners.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2019 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License
|
||||
-->
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#202124"/>
|
||||
<corners android:radius="10dp"/>
|
||||
</shape>
|
||||
27
packages/SystemUI/res/drawable/wallet_button.xml
Normal file
27
packages/SystemUI/res/drawable/wallet_button.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2020 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License
|
||||
-->
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="@color/GM2_blue_600"/>
|
||||
<solid android:color="@color/GM2_blue_600"/>
|
||||
<corners android:radius="24dp"/>
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
33
packages/SystemUI/res/drawable/wallet_empty_state_bg.xml
Normal file
33
packages/SystemUI/res/drawable/wallet_empty_state_bg.xml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2020 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="?android:attr/colorControlHighlight">
|
||||
<item android:id="@android:id/mask">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#DADCE0" />
|
||||
<corners android:radius="@dimen/wallet_empty_state_corner_radius" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape>
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="@color/GM2_grey_900" />
|
||||
<corners android:radius="@dimen/wallet_empty_state_corner_radius" />
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
22
packages/SystemUI/res/layout/quick_access_wallet.xml
Normal file
22
packages/SystemUI/res/layout/quick_access_wallet.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2020 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<com.android.systemui.wallet.ui.WalletView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/wallet_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
47
packages/SystemUI/res/layout/wallet_card_view.xml
Normal file
47
packages/SystemUI/res/layout/wallet_card_view.xml
Normal file
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2020 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
<com.android.systemui.wallet.ui.WalletCardView
|
||||
android:id="@+id/card"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginHorizontal="@dimen/card_margin"
|
||||
android:foreground="?android:attr/selectableItemBackground"
|
||||
app:cardBackgroundColor="@android:color/transparent"
|
||||
app:cardElevation="12dp">
|
||||
<ImageView
|
||||
android:id="@+id/card_image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:adjustViewBounds="true"
|
||||
android:contentDescription="@null"
|
||||
android:scaleType="fitXY"/>
|
||||
<ImageView
|
||||
android:id="@+id/add_card_logo"
|
||||
android:layout_width="28dp"
|
||||
android:layout_height="28dp"
|
||||
android:layout_gravity="center"
|
||||
android:drawable="@drawable/ic_qs_plus"
|
||||
android:contentDescription="@null"
|
||||
android:scaleType="fitCenter"
|
||||
android:visibility="gone"/>
|
||||
</com.android.systemui.wallet.ui.WalletCardView>
|
||||
</FrameLayout>
|
||||
47
packages/SystemUI/res/layout/wallet_empty_state.xml
Normal file
47
packages/SystemUI/res/layout/wallet_empty_state.xml
Normal file
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
**
|
||||
** Copyright 2020, The Android Open Source Project
|
||||
**
|
||||
** Licensed under the Apache License, Version 2.0 (the "License");
|
||||
** you may not use this file except in compliance with the License.
|
||||
** You may obtain a copy of the License at
|
||||
**
|
||||
** http://www.apache.org/licenses/LICENSE-2.0
|
||||
**
|
||||
** Unless required by applicable law or agreed to in writing, software
|
||||
** distributed under the License is distributed on an "AS IS" BASIS,
|
||||
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
** See the License for the specific language governing permissions and
|
||||
** limitations under the License.
|
||||
*/
|
||||
-->
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<LinearLayout
|
||||
android:id="@+id/wallet_empty_state"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="48dp"
|
||||
android:background="@drawable/wallet_empty_state_bg"
|
||||
android:orientation="vertical"
|
||||
android:paddingVertical="52dp"
|
||||
android:theme="@style/Wallet.Theme"
|
||||
android:clipChildren="false"
|
||||
android:visibility="gone">
|
||||
<ImageView
|
||||
android:id="@+id/empty_state_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="28dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:contentDescription="@null"
|
||||
android:scaleType="fitCenter"/>
|
||||
<TextView
|
||||
style="@style/Wallet.TextAppearance"
|
||||
android:id="@+id/empty_state_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"/>
|
||||
</LinearLayout>
|
||||
</merge>
|
||||
86
packages/SystemUI/res/layout/wallet_fullscreen.xml
Normal file
86
packages/SystemUI/res/layout/wallet_fullscreen.xml
Normal file
@@ -0,0 +1,86 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipChildren="false">
|
||||
<LinearLayout
|
||||
android:id="@+id/card_carousel_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="36dp"
|
||||
android:orientation="vertical">
|
||||
<ImageView
|
||||
android:id="@+id/icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginVertical="10dp"
|
||||
android:background="@drawable/circle_wallet_primary_50dp"
|
||||
android:scaleType="center"
|
||||
android:contentDescription="@null"/>
|
||||
<TextView
|
||||
android:id="@+id/label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/Wallet.TextAppearance"
|
||||
android:textAlignment="center"/>
|
||||
|
||||
<com.android.systemui.wallet.ui.WalletCardCarousel
|
||||
android:id="@+id/card_carousel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:transitionName="dotIndicator"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:layout_marginVertical="24dp"/>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/wallet_button"
|
||||
android:background="@drawable/wallet_button"
|
||||
android:textColor="@color/wallet_white"
|
||||
android:textAlignment="center"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="24dp"
|
||||
android:paddingVertical="8dp"
|
||||
android:layout_marginVertical="24dp"
|
||||
android:layout_gravity="center_horizontal"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<include layout="@layout/wallet_empty_state"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/error_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="80dp"
|
||||
android:layout_marginHorizontal="40dp"
|
||||
android:textSize="14sp"
|
||||
android:textColor="#DADCE0"
|
||||
android:elevation="7dp"
|
||||
android:background="@drawable/rounded_corners"
|
||||
android:gravity="center"
|
||||
android:visibility="gone"/>
|
||||
</FrameLayout>
|
||||
@@ -288,4 +288,9 @@
|
||||
<!-- Accessibility floating menu -->
|
||||
<color name="accessibility_floating_menu_background">#CCFFFFFF</color> <!-- 80% -->
|
||||
<color name="accessibility_floating_menu_stroke_dark">#26FFFFFF</color> <!-- 15% -->
|
||||
|
||||
<!-- Wallet screen -->
|
||||
<color name="wallet_white">#FFFFFF</color>
|
||||
<color name="wallet_card_border">#33FFFFFF</color>
|
||||
<color name="wallet_primary_text">@color/GM2_grey_900</color>
|
||||
</resources>
|
||||
|
||||
@@ -1421,4 +1421,15 @@
|
||||
For devices with sensors on the back of the device, this corresponds to the location on the
|
||||
screen directly in front of the sensor. -->
|
||||
<dimen name="physical_fingerprint_sensor_center_screen_location_y">610px</dimen>
|
||||
|
||||
<!-- Wallet activity screen specs -->
|
||||
<dimen name="wallet_icon_size">36sp</dimen>
|
||||
<dimen name="card_margin">16dp</dimen>
|
||||
<dimen name="card_carousel_dot_offset">24dp</dimen>
|
||||
<dimen name="card_carousel_dot_unselected_radius">2dp</dimen>
|
||||
<dimen name="card_carousel_dot_selected_radius">3dp</dimen>
|
||||
<dimen name="card_carousel_dot_margin">4dp</dimen>
|
||||
<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>
|
||||
</resources>
|
||||
|
||||
@@ -1633,8 +1633,14 @@
|
||||
<!-- Wallet strings -->
|
||||
<!-- Wallet empty state, title [CHAR LIMIT=32] -->
|
||||
<string name="wallet_title">Wallet</string>
|
||||
<!-- Label of the button underneath the card carousel when device is unlocked. [CHAR LIMIT=NONE] -->
|
||||
<string name="wallet_button_label_device_unlocked">Show all</string>
|
||||
<!-- Label of the button underneath the card carousel when device is locked. [CHAR LIMIT=NONE] -->
|
||||
<string name="wallet_button_label_device_locked">Unlock to pay</string>
|
||||
<!-- Secondary label of the quick access wallet tile. [CHAR LIMIT=32] -->
|
||||
<string name="wallet_secondary_label">Ready</string>
|
||||
<!-- Message shown when an unknown failure occurred when fetching cards. [CHAR LIMIT=NONE] -->
|
||||
<string name="wallet_error_generic">There was a problem getting your cards, please try again later</string>
|
||||
|
||||
<!-- Name of the work status bar icon. -->
|
||||
<string name="status_bar_work">Work profile</string>
|
||||
|
||||
@@ -792,4 +792,18 @@
|
||||
<item name="android:mirrorForRtl">false</item>
|
||||
<item name="android:progressDrawable">@drawable/udfps_progress_bar</item>
|
||||
</style>
|
||||
|
||||
<!-- Wallet styles -->
|
||||
<style name="Wallet" />
|
||||
|
||||
<style name="Wallet.TextAppearance">
|
||||
<item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
|
||||
<item name="android:textColor">@color/wallet_primary_text</item>
|
||||
<item name="android:singleLine">true</item>
|
||||
<item name="android:textSize">14sp</item>
|
||||
</style>
|
||||
|
||||
<style name="Wallet.Theme">
|
||||
<item name="android:colorControlHighlight">@*android:color/primary_text_material_dark</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@@ -31,7 +31,6 @@ import android.os.HandlerThread;
|
||||
import android.os.Looper;
|
||||
import android.os.ServiceManager;
|
||||
import android.os.UserHandle;
|
||||
import android.service.quickaccesswallet.QuickAccessWalletClient;
|
||||
import android.view.Choreographer;
|
||||
import android.view.IWindowManager;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -376,11 +375,4 @@ public class DependencyProvider {
|
||||
public ModeSwitchesController providesModeSwitchesController(Context context) {
|
||||
return new ModeSwitchesController(context);
|
||||
}
|
||||
|
||||
/** */
|
||||
@Provides
|
||||
@SysUISingleton
|
||||
public QuickAccessWalletClient provideQuickAccessWalletClient(Context context) {
|
||||
return QuickAccessWalletClient.create(context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ import com.android.systemui.util.settings.SettingsUtilModule;
|
||||
import com.android.systemui.util.time.SystemClock;
|
||||
import com.android.systemui.util.time.SystemClockImpl;
|
||||
import com.android.systemui.volume.dagger.VolumeModule;
|
||||
import com.android.systemui.wallet.dagger.WalletModule;
|
||||
import com.android.systemui.wmshell.BubblesManager;
|
||||
import com.android.wm.shell.bubbles.Bubbles;
|
||||
|
||||
@@ -109,7 +110,8 @@ import dagger.Provides;
|
||||
TunerModule.class,
|
||||
UserModule.class,
|
||||
UtilModule.class,
|
||||
VolumeModule.class
|
||||
VolumeModule.class,
|
||||
WalletModule.class
|
||||
},
|
||||
subcomponents = {
|
||||
StatusBarComponent.class,
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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.systemui.wallet.dagger;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.service.quickaccesswallet.QuickAccessWalletClient;
|
||||
|
||||
import com.android.systemui.dagger.SysUISingleton;
|
||||
import com.android.systemui.wallet.ui.WalletActivity;
|
||||
|
||||
import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import dagger.multibindings.ClassKey;
|
||||
import dagger.multibindings.IntoMap;
|
||||
|
||||
|
||||
/**
|
||||
* Module for injecting classes in Wallet.
|
||||
*/
|
||||
@Module
|
||||
public abstract class WalletModule {
|
||||
|
||||
/** */
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(WalletActivity.class)
|
||||
public abstract Activity provideWalletActivity(WalletActivity activity);
|
||||
|
||||
/** */
|
||||
@SysUISingleton
|
||||
@Provides
|
||||
public static QuickAccessWalletClient provideQuickAccessWalletClient(Context context) {
|
||||
return QuickAccessWalletClient.create(context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* 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.systemui.wallet.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.text.TextPaint;
|
||||
import android.util.MathUtils;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.graphics.ColorUtils;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.android.systemui.R;
|
||||
|
||||
final class DotIndicatorDecoration extends RecyclerView.ItemDecoration {
|
||||
private final int mUnselectedRadius;
|
||||
private final int mSelectedRadius;
|
||||
private final int mDotMargin;
|
||||
@ColorInt private final int mUnselectedColor;
|
||||
@ColorInt private final int mSelectedColor;
|
||||
private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final TextPaint mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
|
||||
private WalletCardCarousel mCardCarousel;
|
||||
|
||||
DotIndicatorDecoration(Context context) {
|
||||
super();
|
||||
|
||||
mUnselectedRadius =
|
||||
context.getResources().getDimensionPixelSize(
|
||||
R.dimen.card_carousel_dot_unselected_radius);
|
||||
mSelectedRadius =
|
||||
context.getResources().getDimensionPixelSize(
|
||||
R.dimen.card_carousel_dot_selected_radius);
|
||||
mDotMargin = context.getResources().getDimensionPixelSize(R.dimen.card_carousel_dot_margin);
|
||||
|
||||
TextView textView = new TextView(context);
|
||||
mTextPaint.set(textView.getPaint());
|
||||
// Text color is not copied from text appearance.
|
||||
mTextPaint.setColor(ContextCompat.getColor(context, R.color.GM2_blue_600));
|
||||
|
||||
mUnselectedColor = ContextCompat.getColor(context, R.color.GM2_grey_300);
|
||||
mSelectedColor = ContextCompat.getColor(context, R.color.GM2_blue_600);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getItemOffsets(
|
||||
Rect rect, View view, RecyclerView recyclerView, RecyclerView.State state) {
|
||||
super.getItemOffsets(rect, view, recyclerView, state);
|
||||
if (recyclerView.getAdapter().getItemCount() > 1) {
|
||||
rect.bottom =
|
||||
view.getResources().getDimensionPixelSize(R.dimen.card_carousel_dot_offset);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrawOver(Canvas canvas, RecyclerView recyclerView, RecyclerView.State state) {
|
||||
super.onDrawOver(canvas, recyclerView, state);
|
||||
|
||||
mCardCarousel = (WalletCardCarousel) recyclerView;
|
||||
int itemCount = recyclerView.getAdapter().getItemCount();
|
||||
if (itemCount <= 1) {
|
||||
// Only shown if there are at least 2 items, and it's not a shimmer loader
|
||||
return;
|
||||
}
|
||||
canvas.save();
|
||||
|
||||
float animationStartOffset = recyclerView.getWidth() / 6f;
|
||||
// 0 when a card is still very prominent, ie. edgeToCenterDistance is greater than
|
||||
// animationStartOffset
|
||||
// 1 when the two cards are equidistant from the center ie. edgeToCenterDistance == 0
|
||||
float interpolatedProgress =
|
||||
1 - Math.min(Math.abs(mCardCarousel.mEdgeToCenterDistance), animationStartOffset)
|
||||
/ animationStartOffset;
|
||||
|
||||
float totalWidth =
|
||||
mDotMargin * (itemCount - 1)
|
||||
+ 2 * mUnselectedRadius * (itemCount - 2)
|
||||
+ 2 * mSelectedRadius;
|
||||
// Translate the canvas so the drawing can always start at (0, 0) coordinates.
|
||||
canvas.translate(
|
||||
(recyclerView.getWidth() - totalWidth) / 2f,
|
||||
recyclerView.getHeight() - mDotMargin);
|
||||
|
||||
int itemsDrawn = 0;
|
||||
while (itemsDrawn < itemCount) {
|
||||
// count up from 0 to itemCount - 1 if LTR; count down from itemCount - 1 to 0 if RTL.
|
||||
int i = isLayoutLtr() ? itemsDrawn : itemCount - itemsDrawn - 1;
|
||||
|
||||
if (isSelectedItem(i)) {
|
||||
drawSelectedDot(canvas, interpolatedProgress, i);
|
||||
} else if (isNextItemInScrollingDirection(i)) {
|
||||
drawFadingUnselectedDot(canvas, interpolatedProgress, i);
|
||||
} else {
|
||||
drawUnselectedDot(canvas);
|
||||
}
|
||||
canvas.translate(mDotMargin, 0);
|
||||
itemsDrawn++;
|
||||
}
|
||||
|
||||
canvas.restore();
|
||||
this.mCardCarousel = null; // No need to hold a reference.
|
||||
}
|
||||
|
||||
private void drawSelectedDot(Canvas canvas, float progress, int position) {
|
||||
// Divide progress by 2 because the other half of the animation is done by
|
||||
// drawFadingUnselectedDot.
|
||||
mPaint.setColor(
|
||||
getTransitionAdjustedColor(
|
||||
ColorUtils.blendARGB(mSelectedColor, mUnselectedColor, progress / 2)));
|
||||
float radius = MathUtils.lerp(mSelectedRadius, mUnselectedRadius, progress / 2);
|
||||
canvas.drawCircle(radius, 0, radius, mPaint);
|
||||
canvas.translate(radius * 2, 0);
|
||||
}
|
||||
|
||||
private void drawFadingUnselectedDot(Canvas canvas, float progress, int position) {
|
||||
// Divide progress by 2 because the first half of the animation is done by drawSelectedDot.
|
||||
int blendedColor =
|
||||
ColorUtils.blendARGB(
|
||||
mUnselectedColor, mSelectedColor, progress / 2);
|
||||
mPaint.setColor(getTransitionAdjustedColor(blendedColor));
|
||||
float radius = MathUtils.lerp(mSelectedRadius, mUnselectedRadius, progress / 2);
|
||||
canvas.drawCircle(radius, 0, radius, mPaint);
|
||||
canvas.translate(radius * 2, 0);
|
||||
}
|
||||
|
||||
private void drawUnselectedDot(Canvas canvas) {
|
||||
mPaint.setColor(mUnselectedColor);
|
||||
canvas.drawCircle(mUnselectedRadius, 0, mUnselectedRadius, mPaint);
|
||||
canvas.translate(mUnselectedRadius * 2, 0);
|
||||
}
|
||||
|
||||
private int getTransitionAdjustedColor(int color) {
|
||||
int transitionAlphaOverride = 0xff;
|
||||
return ColorUtils.setAlphaComponent(color, transitionAlphaOverride);
|
||||
}
|
||||
|
||||
private boolean isSelectedItem(int position) {
|
||||
return mCardCarousel.mCenteredAdapterPosition == position;
|
||||
}
|
||||
|
||||
private boolean isNextItemInScrollingDirection(int position) {
|
||||
if (isLayoutLtr()) {
|
||||
return (mCardCarousel.mCenteredAdapterPosition + 1 == position
|
||||
&& mCardCarousel.mEdgeToCenterDistance >= 0f)
|
||||
|| (mCardCarousel.mCenteredAdapterPosition - 1 == position
|
||||
&& mCardCarousel.mEdgeToCenterDistance < 0f);
|
||||
}
|
||||
return (mCardCarousel.mCenteredAdapterPosition - 1 == position
|
||||
&& mCardCarousel.mEdgeToCenterDistance >= 0f)
|
||||
|| (mCardCarousel.mCenteredAdapterPosition + 1 == position
|
||||
&& mCardCarousel.mEdgeToCenterDistance < 0f);
|
||||
}
|
||||
|
||||
private boolean isLayoutLtr() {
|
||||
if (mCardCarousel == null) {
|
||||
// Shouldn't happen, but assume LTR for now.
|
||||
return true;
|
||||
}
|
||||
return mCardCarousel.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* 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.systemui.wallet.ui;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.service.quickaccesswallet.QuickAccessWalletClient;
|
||||
import android.view.MenuItem;
|
||||
import android.view.Window;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.systemui.R;
|
||||
import com.android.systemui.dagger.qualifiers.Background;
|
||||
import com.android.systemui.plugins.ActivityStarter;
|
||||
import com.android.systemui.settings.UserTracker;
|
||||
import com.android.systemui.statusbar.policy.KeyguardStateController;
|
||||
import com.android.systemui.util.LifecycleActivity;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
/**
|
||||
* Displays Wallet carousel screen inside an activity.
|
||||
*/
|
||||
public class WalletActivity extends LifecycleActivity {
|
||||
|
||||
private final QuickAccessWalletClient mQuickAccessWalletClient;
|
||||
private final KeyguardStateController mKeyguardStateController;
|
||||
private final ActivityStarter mActivityStarter;
|
||||
private final Executor mExecutor;
|
||||
private final Handler mHandler;
|
||||
private final UserTracker mUserTracker;
|
||||
private WalletScreenController mWalletScreenController;
|
||||
|
||||
@Inject
|
||||
public WalletActivity(
|
||||
QuickAccessWalletClient quickAccessWalletClient,
|
||||
KeyguardStateController keyguardStateController,
|
||||
ActivityStarter activityStarter,
|
||||
@Background Executor executor,
|
||||
@Background Handler handler,
|
||||
UserTracker userTracker) {
|
||||
mQuickAccessWalletClient = quickAccessWalletClient;
|
||||
mKeyguardStateController = keyguardStateController;
|
||||
mActivityStarter = activityStarter;
|
||||
mExecutor = executor;
|
||||
mHandler = handler;
|
||||
mUserTracker = userTracker;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
getWindow().requestFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.quick_access_wallet);
|
||||
|
||||
getWindow().getDecorView().setBackgroundColor(getColor(R.color.wallet_white));
|
||||
setTitle("");
|
||||
getActionBar().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
|
||||
getActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getActionBar().setHomeAsUpIndicator(R.drawable.ic_close);
|
||||
getActionBar().setHomeActionContentDescription(R.string.accessibility_desc_close);
|
||||
WalletView walletView = requireViewById(R.id.wallet_view);
|
||||
mWalletScreenController = new WalletScreenController(
|
||||
this,
|
||||
walletView,
|
||||
mQuickAccessWalletClient,
|
||||
mActivityStarter,
|
||||
mExecutor,
|
||||
mHandler,
|
||||
mUserTracker,
|
||||
!mKeyguardStateController.isUnlocked());
|
||||
walletView.getWalletButton().setOnClickListener(
|
||||
v -> mActivityStarter.startActivity(
|
||||
mQuickAccessWalletClient.createWalletIntent(), true));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||
int itemId = item.getItemId();
|
||||
if (itemId == android.R.id.home) {
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
mWalletScreenController.onDismissed();
|
||||
super.onDestroy();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,473 @@
|
||||
/*
|
||||
* 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.systemui.wallet.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Rect;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.cardview.widget.CardView;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.LinearSmoothScroller;
|
||||
import androidx.recyclerview.widget.PagerSnapHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;
|
||||
|
||||
import com.android.systemui.R;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Card Carousel for displaying Quick Access Wallet cards.
|
||||
*/
|
||||
public class WalletCardCarousel extends RecyclerView {
|
||||
|
||||
// A negative card margin is required because card shrinkage pushes the cards too far apart
|
||||
private static final float CARD_MARGIN_RATIO = -.03f;
|
||||
// Size of the unselected card as a ratio to size of selected card.
|
||||
private static final float UNSELECTED_CARD_SCALE = .83f;
|
||||
private static final float CORNER_RADIUS_RATIO = 25f / 700f;
|
||||
private static final float CARD_ASPECT_RATIO = 700f / 440f;
|
||||
private static final float CARD_VIEW_WIDTH_RATIO = 0.69f;
|
||||
|
||||
|
||||
static final int CARD_ANIM_ALPHA_DURATION = 100;
|
||||
static final int CARD_ANIM_ALPHA_DELAY = 50;
|
||||
|
||||
private final Rect mSystemGestureExclusionZone = new Rect();
|
||||
private WalletCardCarouselAdapter mWalletCardCarouselAdapter;
|
||||
private int mExpectedViewWidth;
|
||||
private int mCardMarginPx;
|
||||
private int mCardWidthPx;
|
||||
private int mCardHeightPx;
|
||||
private float mCornerRadiusPx;
|
||||
private int mTotalCardWidth;
|
||||
private float mCardEdgeToCenterDistance;
|
||||
|
||||
private OnSelectionListener mSelectionListener;
|
||||
private OnCardScrollListener mCardScrollListener;
|
||||
// Adapter position of the child that is closest to the center of the recycler view, will also
|
||||
// be used in DotIndicatorDecoration.
|
||||
int mCenteredAdapterPosition = RecyclerView.NO_POSITION;
|
||||
// Pixel distance, along y-axis, from the center of the recycler view to the nearest child, will
|
||||
// also be used in DotIndicatorDecoration.
|
||||
float mEdgeToCenterDistance = Float.MAX_VALUE;
|
||||
private float mCardCenterToScreenCenterDistancePx = Float.MAX_VALUE;
|
||||
// When card data is loaded, this many cards should be animated as data is bound to them.
|
||||
private int mNumCardsToAnimate;
|
||||
// When card data is loaded, this is the position of the leftmost card to be animated.
|
||||
private int mCardAnimationStartPosition;
|
||||
// When card data is loaded, the animations may be delayed so that other animations can complete
|
||||
private int mExtraAnimationDelay;
|
||||
|
||||
interface OnSelectionListener {
|
||||
/**
|
||||
* The card was moved to the center, thus selecting it.
|
||||
*/
|
||||
void onCardSelected(@NonNull WalletCardViewInfo card);
|
||||
|
||||
/**
|
||||
* The card was clicked.
|
||||
*/
|
||||
void onCardClicked(@NonNull WalletCardViewInfo card);
|
||||
|
||||
/**
|
||||
* Cards should be re-queried due to a layout change
|
||||
*/
|
||||
void queryWalletCards();
|
||||
}
|
||||
|
||||
interface OnCardScrollListener {
|
||||
void onCardScroll(WalletCardViewInfo centerCard, WalletCardViewInfo nextCard,
|
||||
float percentDistanceFromCenter);
|
||||
}
|
||||
|
||||
public WalletCardCarousel(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public WalletCardCarousel(Context context, @Nullable AttributeSet attributeSet) {
|
||||
super(context, attributeSet);
|
||||
|
||||
setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false));
|
||||
addOnScrollListener(new CardCarouselScrollListener());
|
||||
new CarouselSnapHelper().attachToRecyclerView(this);
|
||||
mWalletCardCarouselAdapter = new WalletCardCarouselAdapter();
|
||||
mWalletCardCarouselAdapter.setHasStableIds(true);
|
||||
setAdapter(mWalletCardCarouselAdapter);
|
||||
ViewCompat.setAccessibilityDelegate(this, new CardCarouselAccessibilityDelegate(this));
|
||||
|
||||
addItemDecoration(new DotIndicatorDecoration(getContext()));
|
||||
}
|
||||
|
||||
/**
|
||||
* We need to know the card width before we query cards. Card width depends on layout width.
|
||||
* But the carousel isn't laid out until set to visible, which only happens after cards are
|
||||
* returned. Setting the expected view width breaks the chicken-and-egg problem.
|
||||
*/
|
||||
void setExpectedViewWidth(int width) {
|
||||
if (mExpectedViewWidth == width) {
|
||||
return;
|
||||
}
|
||||
mExpectedViewWidth = width;
|
||||
Resources res = getResources();
|
||||
DisplayMetrics metrics = res.getDisplayMetrics();
|
||||
int screenWidth = Math.min(metrics.widthPixels, metrics.heightPixels);
|
||||
mCardWidthPx = Math.round(Math.min(width, screenWidth) * CARD_VIEW_WIDTH_RATIO);
|
||||
mCardHeightPx = Math.round(mCardWidthPx / CARD_ASPECT_RATIO);
|
||||
mCornerRadiusPx = mCardWidthPx * CORNER_RADIUS_RATIO;
|
||||
mCardMarginPx = Math.round(mCardWidthPx * CARD_MARGIN_RATIO);
|
||||
mTotalCardWidth = mCardWidthPx + res.getDimensionPixelSize(R.dimen.card_margin) * 2;
|
||||
mCardEdgeToCenterDistance = mTotalCardWidth / 2f;
|
||||
updatePadding(width);
|
||||
if (mSelectionListener != null) {
|
||||
mSelectionListener.queryWalletCards();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewAdded(View child) {
|
||||
super.onViewAdded(child);
|
||||
LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
|
||||
layoutParams.leftMargin = mCardMarginPx;
|
||||
layoutParams.rightMargin = mCardMarginPx;
|
||||
child.addOnLayoutChangeListener((v, l, t, r, b, ol, ot, or, ob) -> updateCardView(child));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
int width = getWidth();
|
||||
if (mWalletCardCarouselAdapter.getItemCount() > 1 && width < mTotalCardWidth * 1.5) {
|
||||
// When 2 or more cards are available but only one whole card can be shown on screen at
|
||||
// a time, the entire carousel is opted out from system gesture to help users swipe
|
||||
// between cards without accidentally performing the 'back' gesture. When there is only
|
||||
// one card or when the carousel is large enough to accommodate several whole cards,
|
||||
// there is no need to disable the back gesture since either the user can't swipe or has
|
||||
// plenty of room with which to do so.
|
||||
mSystemGestureExclusionZone.set(0, 0, width, getHeight());
|
||||
setSystemGestureExclusionRects(Collections.singletonList(mSystemGestureExclusionZone));
|
||||
}
|
||||
if (width != mExpectedViewWidth) {
|
||||
updatePadding(width);
|
||||
}
|
||||
}
|
||||
|
||||
void setExtraAnimationDelay(int extraAnimationDelay) {
|
||||
mExtraAnimationDelay = extraAnimationDelay;
|
||||
}
|
||||
|
||||
void setSelectionListener(OnSelectionListener selectionListener) {
|
||||
mSelectionListener = selectionListener;
|
||||
}
|
||||
|
||||
void setCardScrollListener(OnCardScrollListener scrollListener) {
|
||||
mCardScrollListener = scrollListener;
|
||||
}
|
||||
|
||||
int getCardWidthPx() {
|
||||
return mCardWidthPx;
|
||||
}
|
||||
|
||||
int getCardHeightPx() {
|
||||
return mCardHeightPx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set card data. Returns true if carousel was empty, indicating that views will be animated
|
||||
*/
|
||||
boolean setData(List<WalletCardViewInfo> data, int selectedIndex) {
|
||||
boolean wasEmpty = mWalletCardCarouselAdapter.getItemCount() == 0;
|
||||
mWalletCardCarouselAdapter.setData(data);
|
||||
if (wasEmpty) {
|
||||
scrollToPosition(selectedIndex);
|
||||
mNumCardsToAnimate = numCardsOnScreen(data.size(), selectedIndex);
|
||||
mCardAnimationStartPosition = Math.max(selectedIndex - 1, 0);
|
||||
}
|
||||
WalletCardViewInfo selectedCard = data.get(selectedIndex);
|
||||
mCardScrollListener.onCardScroll(selectedCard, selectedCard, 0);
|
||||
return wasEmpty;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scrollToPosition(int position) {
|
||||
super.scrollToPosition(position);
|
||||
mSelectionListener.onCardSelected(mWalletCardCarouselAdapter.mData.get(position));
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of cards shown on screen when one of the cards is position in the center. This is
|
||||
* also the num
|
||||
*/
|
||||
private static int numCardsOnScreen(int numCards, int selectedIndex) {
|
||||
if (numCards <= 2) {
|
||||
return numCards;
|
||||
}
|
||||
// When there are 3 or more cards, 3 cards will be shown unless the first or last card is
|
||||
// centered on screen.
|
||||
return selectedIndex > 0 && selectedIndex < (numCards - 1) ? 3 : 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* The padding pushes the first and last cards in the list to the center when they are
|
||||
* selected.
|
||||
*/
|
||||
private void updatePadding(int viewWidth) {
|
||||
int paddingHorizontal = (viewWidth - mTotalCardWidth) / 2 - mCardMarginPx;
|
||||
paddingHorizontal = Math.max(0, paddingHorizontal); // just in case
|
||||
setPadding(paddingHorizontal, getPaddingTop(), paddingHorizontal, getPaddingBottom());
|
||||
|
||||
// re-center selected card after changing padding (if card is selected)
|
||||
if (mWalletCardCarouselAdapter != null
|
||||
&& mWalletCardCarouselAdapter.getItemCount() > 0
|
||||
&& mCenteredAdapterPosition != NO_POSITION) {
|
||||
ViewHolder viewHolder = findViewHolderForAdapterPosition(mCenteredAdapterPosition);
|
||||
if (viewHolder != null) {
|
||||
View cardView = viewHolder.itemView;
|
||||
int cardCenter = (cardView.getLeft() + cardView.getRight()) / 2;
|
||||
int viewCenter = (getLeft() + getRight()) / 2;
|
||||
int scrollX = cardCenter - viewCenter;
|
||||
scrollBy(scrollX, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateCardView(View view) {
|
||||
WalletCardViewHolder viewHolder = (WalletCardViewHolder) view.getTag();
|
||||
CardView cardView = viewHolder.mCardView;
|
||||
float center = (float) getWidth() / 2f;
|
||||
float viewCenter = (view.getRight() + view.getLeft()) / 2f;
|
||||
float viewWidth = view.getWidth();
|
||||
float position = (viewCenter - center) / viewWidth;
|
||||
float scaleFactor = Math.max(UNSELECTED_CARD_SCALE, 1f - Math.abs(position));
|
||||
|
||||
cardView.setScaleX(scaleFactor);
|
||||
cardView.setScaleY(scaleFactor);
|
||||
|
||||
// a card is the "centered card" until its edge has moved past the center of the recycler
|
||||
// view. note that we also need to factor in the negative margin.
|
||||
// Find the edge that is closer to the center.
|
||||
int edgePosition =
|
||||
viewCenter < center ? view.getRight() + mCardMarginPx
|
||||
: view.getLeft() - mCardMarginPx;
|
||||
|
||||
if (Math.abs(viewCenter - center) < mCardCenterToScreenCenterDistancePx) {
|
||||
int childAdapterPosition = getChildAdapterPosition(view);
|
||||
if (childAdapterPosition == RecyclerView.NO_POSITION) {
|
||||
return;
|
||||
}
|
||||
mCenteredAdapterPosition = getChildAdapterPosition(view);
|
||||
mEdgeToCenterDistance = edgePosition - center;
|
||||
mCardCenterToScreenCenterDistancePx = Math.abs(viewCenter - center);
|
||||
}
|
||||
}
|
||||
|
||||
private class CardCarouselScrollListener extends OnScrollListener {
|
||||
|
||||
private int mOldState = -1;
|
||||
|
||||
@Override
|
||||
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
|
||||
if (newState == RecyclerView.SCROLL_STATE_IDLE && newState != mOldState) {
|
||||
performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
|
||||
}
|
||||
mOldState = newState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback method to be invoked when the RecyclerView has been scrolled. This will be
|
||||
* called after the scroll has completed.
|
||||
*
|
||||
* <p>This callback will also be called if visible item range changes after a layout
|
||||
* calculation. In that case, dx and dy will be 0.
|
||||
*
|
||||
* @param recyclerView The RecyclerView which scrolled.
|
||||
* @param dx The amount of horizontal scroll.
|
||||
* @param dy The amount of vertical scroll.
|
||||
*/
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||
mCenteredAdapterPosition = RecyclerView.NO_POSITION;
|
||||
mEdgeToCenterDistance = Float.MAX_VALUE;
|
||||
mCardCenterToScreenCenterDistancePx = Float.MAX_VALUE;
|
||||
for (int i = 0; i < getChildCount(); i++) {
|
||||
updateCardView(getChildAt(i));
|
||||
}
|
||||
if (mCenteredAdapterPosition == RecyclerView.NO_POSITION || dx == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int nextAdapterPosition =
|
||||
mCenteredAdapterPosition + (mEdgeToCenterDistance > 0 ? 1 : -1);
|
||||
if (nextAdapterPosition < 0
|
||||
|| nextAdapterPosition >= mWalletCardCarouselAdapter.mData.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the label text based on the currently selected card and the next one
|
||||
WalletCardViewInfo centerCard =
|
||||
mWalletCardCarouselAdapter.mData.get(mCenteredAdapterPosition);
|
||||
WalletCardViewInfo nextCard = mWalletCardCarouselAdapter.mData.get(nextAdapterPosition);
|
||||
float percentDistanceFromCenter =
|
||||
Math.abs(mEdgeToCenterDistance) / mCardEdgeToCenterDistance;
|
||||
mCardScrollListener.onCardScroll(centerCard, nextCard, percentDistanceFromCenter);
|
||||
}
|
||||
}
|
||||
|
||||
private class CarouselSnapHelper extends PagerSnapHelper {
|
||||
|
||||
private static final float MILLISECONDS_PER_INCH = 200.0F;
|
||||
private static final int MAX_SCROLL_ON_FLING_DURATION = 80; // ms
|
||||
|
||||
@Override
|
||||
public View findSnapView(LayoutManager layoutManager) {
|
||||
View view = super.findSnapView(layoutManager);
|
||||
if (view == null) {
|
||||
// implementation decides not to snap
|
||||
return null;
|
||||
}
|
||||
WalletCardViewHolder viewHolder = (WalletCardViewHolder) view.getTag();
|
||||
WalletCardViewInfo card = viewHolder.mCardViewInfo;
|
||||
mSelectionListener.onCardSelected(card);
|
||||
mCardScrollListener.onCardScroll(card, card, 0);
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* The default SnapScroller is a little sluggish
|
||||
*/
|
||||
@Override
|
||||
protected LinearSmoothScroller createScroller(LayoutManager layoutManager) {
|
||||
return new LinearSmoothScroller(getContext()) {
|
||||
@Override
|
||||
protected void onTargetFound(View targetView, State state, Action action) {
|
||||
int[] snapDistances = calculateDistanceToFinalSnap(layoutManager, targetView);
|
||||
final int dx = snapDistances[0];
|
||||
final int dy = snapDistances[1];
|
||||
final int time = calculateTimeForDeceleration(
|
||||
Math.max(Math.abs(dx), Math.abs(dy)));
|
||||
if (time > 0) {
|
||||
action.update(dx, dy, time, mDecelerateInterpolator);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
|
||||
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int calculateTimeForScrolling(int dx) {
|
||||
return Math.min(MAX_SCROLL_ON_FLING_DURATION,
|
||||
super.calculateTimeForScrolling(dx));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private class WalletCardCarouselAdapter extends Adapter<WalletCardViewHolder> {
|
||||
|
||||
private List<WalletCardViewInfo> mData = Collections.EMPTY_LIST;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public WalletCardViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
|
||||
LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext());
|
||||
View view = inflater.inflate(R.layout.wallet_card_view, viewGroup, false);
|
||||
WalletCardViewHolder viewHolder = new WalletCardViewHolder(view);
|
||||
CardView cardView = viewHolder.mCardView;
|
||||
cardView.setRadius(mCornerRadiusPx);
|
||||
ViewGroup.LayoutParams layoutParams = cardView.getLayoutParams();
|
||||
layoutParams.width = mCardWidthPx;
|
||||
layoutParams.height = mCardHeightPx;
|
||||
view.setTag(viewHolder);
|
||||
return viewHolder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull WalletCardViewHolder viewHolder, int position) {
|
||||
WalletCardViewInfo cardViewInfo = mData.get(position);
|
||||
viewHolder.mCardViewInfo = cardViewInfo;
|
||||
if (cardViewInfo.getCardId().isEmpty()) {
|
||||
viewHolder.mImageView.setScaleType(ImageView.ScaleType.CENTER);
|
||||
}
|
||||
viewHolder.mImageView.setImageDrawable(cardViewInfo.getCardDrawable());
|
||||
viewHolder.mCardView.setContentDescription(cardViewInfo.getContentDescription());
|
||||
viewHolder.mCardView.setOnClickListener(
|
||||
v -> {
|
||||
if (position != mCenteredAdapterPosition) {
|
||||
smoothScrollToPosition(position);
|
||||
} else {
|
||||
mSelectionListener.onCardClicked(cardViewInfo);
|
||||
}
|
||||
});
|
||||
if (mNumCardsToAnimate > 0 && (position - mCardAnimationStartPosition < 2)) {
|
||||
mNumCardsToAnimate--;
|
||||
int startDelay = (position - mCardAnimationStartPosition) * CARD_ANIM_ALPHA_DELAY
|
||||
+ mExtraAnimationDelay;
|
||||
viewHolder.itemView.setAlpha(0f);
|
||||
viewHolder.itemView.animate().alpha(1f)
|
||||
.setStartDelay(Math.max(0, startDelay))
|
||||
.setDuration(CARD_ANIM_ALPHA_DURATION).start();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mData.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return mData.get(position).getCardId().hashCode();
|
||||
}
|
||||
|
||||
void setData(List<WalletCardViewInfo> data) {
|
||||
mData = data;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private class CardCarouselAccessibilityDelegate extends RecyclerViewAccessibilityDelegate {
|
||||
|
||||
private CardCarouselAccessibilityDelegate(@NonNull RecyclerView recyclerView) {
|
||||
super(recyclerView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onRequestSendAccessibilityEvent(
|
||||
ViewGroup viewGroup, View view, AccessibilityEvent accessibilityEvent) {
|
||||
int eventType = accessibilityEvent.getEventType();
|
||||
if (eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
|
||||
scrollToPosition(getChildAdapterPosition(view));
|
||||
}
|
||||
return super.onRequestSendAccessibilityEvent(viewGroup, view, accessibilityEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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.systemui.wallet.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.cardview.widget.CardView;
|
||||
|
||||
import com.android.systemui.R;
|
||||
|
||||
/** Customized card view of the wallet card carousel. */
|
||||
public class WalletCardView extends CardView {
|
||||
private final Paint mBorderPaint;
|
||||
|
||||
public WalletCardView(@NonNull Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public WalletCardView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
mBorderPaint = new Paint();
|
||||
mBorderPaint.setColor(context.getColor(R.color.wallet_card_border));
|
||||
mBorderPaint.setStrokeWidth(
|
||||
context.getResources().getDimension(R.dimen.wallet_card_border_width));
|
||||
mBorderPaint.setStyle(Paint.Style.STROKE);
|
||||
mBorderPaint.setAntiAlias(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
super.draw(canvas);
|
||||
float radius = getRadius();
|
||||
canvas.drawRoundRect(0, 0, getWidth(), getHeight(), radius, radius, mBorderPaint);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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.systemui.wallet.ui;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.cardview.widget.CardView;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.android.systemui.R;
|
||||
|
||||
/**
|
||||
* View holder for the quick access wallet card.
|
||||
*/
|
||||
class WalletCardViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
final CardView mCardView;
|
||||
final ImageView mImageView;
|
||||
WalletCardViewInfo mCardViewInfo;
|
||||
|
||||
WalletCardViewHolder(View view) {
|
||||
super(view);
|
||||
mCardView = view.requireViewById(R.id.card);
|
||||
mImageView = mCardView.requireViewById(R.id.card_image);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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.systemui.wallet.ui;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
interface WalletCardViewInfo {
|
||||
String getCardId();
|
||||
|
||||
/**
|
||||
* Image of the card.
|
||||
*/
|
||||
@NonNull
|
||||
Drawable getCardDrawable();
|
||||
|
||||
/**
|
||||
* Content description for the card.
|
||||
*/
|
||||
@Nullable
|
||||
CharSequence getContentDescription();
|
||||
|
||||
/**
|
||||
* Icon shown above the card.
|
||||
*/
|
||||
@Nullable
|
||||
Drawable getIcon();
|
||||
|
||||
/**
|
||||
* Text shown above the card.
|
||||
*/
|
||||
@NonNull
|
||||
CharSequence getLabel();
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
/*
|
||||
* 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.systemui.wallet.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.drawable.Drawable;
|
||||
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.SelectWalletCardRequest;
|
||||
import android.service.quickaccesswallet.WalletCard;
|
||||
import android.service.quickaccesswallet.WalletServiceEvent;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.systemui.R;
|
||||
import com.android.systemui.plugins.ActivityStarter;
|
||||
import com.android.systemui.settings.UserTracker;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/** Controller for the wallet card carousel screen. */
|
||||
public class WalletScreenController implements
|
||||
WalletCardCarousel.OnSelectionListener,
|
||||
QuickAccessWalletClient.OnWalletCardsRetrievedCallback,
|
||||
QuickAccessWalletClient.WalletServiceEventListener {
|
||||
|
||||
private static final String TAG = "WalletScreenCtrl";
|
||||
private static final String PREFS_HAS_CARDS = "has_cards";
|
||||
private static final String PREFS_WALLET_VIEW_HEIGHT = "wallet_view_height";
|
||||
private static final int MAX_CARDS = 10;
|
||||
private static final long SELECTION_DELAY_MILLIS = TimeUnit.SECONDS.toMillis(30);
|
||||
|
||||
private Context mContext;
|
||||
private final QuickAccessWalletClient mWalletClient;
|
||||
private final ActivityStarter mActivityStarter;
|
||||
private final Executor mExecutor;
|
||||
private final Handler mHandler;
|
||||
private final Runnable mSelectionRunnable = this::selectCard;
|
||||
private final SharedPreferences mPrefs;
|
||||
private final WalletView mWalletView;
|
||||
private final WalletCardCarousel mCardCarousel;
|
||||
|
||||
@VisibleForTesting String mSelectedCardId;
|
||||
@VisibleForTesting boolean mIsDismissed;
|
||||
private boolean mIsDeviceLocked;
|
||||
private boolean mHasRegisteredListener;
|
||||
|
||||
public WalletScreenController(
|
||||
Context context,
|
||||
WalletView walletView,
|
||||
QuickAccessWalletClient walletClient,
|
||||
ActivityStarter activityStarter,
|
||||
Executor executor,
|
||||
Handler handler,
|
||||
UserTracker userTracker,
|
||||
boolean isDeviceLocked) {
|
||||
mContext = context;
|
||||
mWalletClient = walletClient;
|
||||
mActivityStarter = activityStarter;
|
||||
mExecutor = executor;
|
||||
mHandler = handler;
|
||||
mPrefs = userTracker.getUserContext().getSharedPreferences(TAG, Context.MODE_PRIVATE);
|
||||
mWalletView = walletView;
|
||||
mWalletView.setMinimumHeight(getExpectedMinHeight());
|
||||
mWalletView.setLayoutParams(
|
||||
new FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
mCardCarousel = mWalletView.getCardCarousel();
|
||||
if (mCardCarousel != null) {
|
||||
mCardCarousel.setSelectionListener(this);
|
||||
}
|
||||
|
||||
if (!mPrefs.getBoolean(PREFS_HAS_CARDS, false)) {
|
||||
// The empty state view is shown preemptively when cards were not returned last time
|
||||
// to decrease perceived latency.
|
||||
showEmptyStateView();
|
||||
}
|
||||
mIsDeviceLocked = isDeviceLocked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@link QuickAccessWalletClient.OnWalletCardsRetrievedCallback}. Called when cards
|
||||
* are retrieved successfully from the service. This is called on {@link #mExecutor}.
|
||||
*/
|
||||
@Override
|
||||
public void onWalletCardsRetrieved(@NonNull GetWalletCardsResponse response) {
|
||||
if (mIsDismissed) {
|
||||
return;
|
||||
}
|
||||
List<WalletCard> walletCards = response.getWalletCards();
|
||||
List<WalletCardViewInfo> data = new ArrayList<>(walletCards.size());
|
||||
for (WalletCard card : walletCards) {
|
||||
data.add(new QAWalletCardViewInfo(mContext, card));
|
||||
}
|
||||
mHandler.post(() -> {
|
||||
if (data.isEmpty()) {
|
||||
showEmptyStateView();
|
||||
} else {
|
||||
mWalletView.showCardCarousel(data, response.getSelectedIndex(), mIsDeviceLocked);
|
||||
}
|
||||
// The empty state view will not be shown preemptively next time if cards were returned
|
||||
mPrefs.edit().putBoolean(PREFS_HAS_CARDS, !data.isEmpty()).apply();
|
||||
removeMinHeightAndRecordHeightOnLayout();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@link QuickAccessWalletClient.OnWalletCardsRetrievedCallback}. Called when there
|
||||
* is an error during card retrieval. This will be run on the {@link #mExecutor}.
|
||||
*/
|
||||
@Override
|
||||
public void onWalletCardRetrievalError(@NonNull GetWalletCardsError error) {
|
||||
if (mIsDismissed) {
|
||||
return;
|
||||
}
|
||||
mHandler.post(() -> {
|
||||
mWalletView.showErrorMessage(error.getMessage());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@link QuickAccessWalletClient.WalletServiceEventListener}. Called when the wallet
|
||||
* application propagates an event, such as an NFC tap, to the quick access wallet view.
|
||||
*/
|
||||
@Override
|
||||
public void onWalletServiceEvent(WalletServiceEvent event) {
|
||||
if (mIsDismissed) {
|
||||
return;
|
||||
}
|
||||
switch (event.getEventType()) {
|
||||
case WalletServiceEvent.TYPE_NFC_PAYMENT_STARTED:
|
||||
onDismissed();
|
||||
break;
|
||||
case WalletServiceEvent.TYPE_WALLET_CARDS_UPDATED:
|
||||
queryWalletCards();
|
||||
break;
|
||||
default:
|
||||
Log.w(TAG, "onWalletServiceEvent: Unknown event type");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCardSelected(@NonNull WalletCardViewInfo card) {
|
||||
if (mIsDismissed) {
|
||||
return;
|
||||
}
|
||||
mSelectedCardId = card.getCardId();
|
||||
selectCard();
|
||||
}
|
||||
|
||||
private void selectCard() {
|
||||
mHandler.removeCallbacks(mSelectionRunnable);
|
||||
String selectedCardId = mSelectedCardId;
|
||||
if (mIsDismissed || selectedCardId == null) {
|
||||
return;
|
||||
}
|
||||
mWalletClient.selectWalletCard(new SelectWalletCardRequest(selectedCardId));
|
||||
// Re-selecting the card keeps the connection bound so we continue to get service events
|
||||
// even if the user keeps it open for a long time.
|
||||
mHandler.postDelayed(mSelectionRunnable, SELECTION_DELAY_MILLIS);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void onCardClicked(@NonNull WalletCardViewInfo cardInfo) {
|
||||
if (!(cardInfo instanceof QAWalletCardViewInfo)
|
||||
|| ((QAWalletCardViewInfo) cardInfo).mWalletCard == null
|
||||
|| ((QAWalletCardViewInfo) cardInfo).mWalletCard.getPendingIntent() == null) {
|
||||
return;
|
||||
}
|
||||
mActivityStarter.startActivity(
|
||||
((QAWalletCardViewInfo) cardInfo).mWalletCard.getPendingIntent().getIntent(),
|
||||
true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queryWalletCards() {
|
||||
if (mIsDismissed) {
|
||||
return;
|
||||
}
|
||||
if (!mHasRegisteredListener) {
|
||||
// Listener is registered even when device is locked. Should only be registered once.
|
||||
mWalletClient.addWalletServiceEventListener(this);
|
||||
mHasRegisteredListener = true;
|
||||
}
|
||||
|
||||
mWalletView.show();
|
||||
mWalletView.hideErrorMessage();
|
||||
int iconSizePx = mContext.getResources().getDimensionPixelSize(R.dimen.wallet_icon_size);
|
||||
int cardWidthPx = mCardCarousel.getCardWidthPx();
|
||||
int cardHeightPx = mCardCarousel.getCardHeightPx();
|
||||
GetWalletCardsRequest request =
|
||||
new GetWalletCardsRequest(cardWidthPx, cardHeightPx, iconSizePx, MAX_CARDS);
|
||||
mWalletClient.getWalletCards(mExecutor, request, this);
|
||||
}
|
||||
|
||||
void onDismissed() {
|
||||
if (mIsDismissed) {
|
||||
return;
|
||||
}
|
||||
mIsDismissed = true;
|
||||
mSelectedCardId = null;
|
||||
mHandler.removeCallbacks(mSelectionRunnable);
|
||||
mWalletClient.notifyWalletDismissed();
|
||||
mWalletClient.removeWalletServiceEventListener(this);
|
||||
mWalletView.animateDismissal();
|
||||
// clear refs to the Wallet Activity
|
||||
mContext = null;
|
||||
}
|
||||
|
||||
private void showEmptyStateView() {
|
||||
Drawable logo = mWalletClient.getLogo();
|
||||
CharSequence logoContentDesc = mWalletClient.getServiceLabel();
|
||||
CharSequence label = mWalletClient.getShortcutLongLabel();
|
||||
Intent intent = mWalletClient.createWalletIntent();
|
||||
if (logo == null
|
||||
|| TextUtils.isEmpty(logoContentDesc)
|
||||
|| TextUtils.isEmpty(label)
|
||||
|| intent == null) {
|
||||
Log.w(TAG, "QuickAccessWalletService manifest entry mis-configured");
|
||||
// Issue is not likely to be resolved until manifest entries are enabled.
|
||||
// Hide wallet feature until then.
|
||||
mWalletView.hide();
|
||||
mPrefs.edit().putInt(PREFS_WALLET_VIEW_HEIGHT, 0).apply();
|
||||
} else {
|
||||
logo.setTint(mContext.getColor(R.color.GM2_grey_900));
|
||||
mWalletView.showEmptyStateView(
|
||||
logo,
|
||||
logoContentDesc,
|
||||
label,
|
||||
v -> mActivityStarter.startActivity(intent, true));
|
||||
}
|
||||
}
|
||||
|
||||
private int getExpectedMinHeight() {
|
||||
int expectedHeight = mPrefs.getInt(PREFS_WALLET_VIEW_HEIGHT, -1);
|
||||
if (expectedHeight == -1) {
|
||||
Resources res = mContext.getResources();
|
||||
expectedHeight = res.getDimensionPixelSize(R.dimen.min_wallet_empty_height);
|
||||
}
|
||||
return expectedHeight;
|
||||
}
|
||||
|
||||
private void removeMinHeightAndRecordHeightOnLayout() {
|
||||
mWalletView.setMinimumHeight(0);
|
||||
mWalletView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
|
||||
@Override
|
||||
public void onLayoutChange(View v, int left, int top, int right, int bottom,
|
||||
int oldLeft, int oldTop, int oldRight, int oldBottom) {
|
||||
mWalletView.removeOnLayoutChangeListener(this);
|
||||
mPrefs.edit().putInt(PREFS_WALLET_VIEW_HEIGHT, bottom - top).apply();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static class QAWalletCardViewInfo implements WalletCardViewInfo {
|
||||
|
||||
private final WalletCard mWalletCard;
|
||||
private final Drawable mCardDrawable;
|
||||
private final Drawable mIconDrawable;
|
||||
|
||||
/**
|
||||
* Constructor is called on background executor, so it is safe to load drawables
|
||||
* synchronously.
|
||||
*/
|
||||
QAWalletCardViewInfo(Context context, WalletCard walletCard) {
|
||||
mWalletCard = walletCard;
|
||||
mCardDrawable = mWalletCard.getCardImage().loadDrawable(context);
|
||||
Icon icon = mWalletCard.getCardIcon();
|
||||
mIconDrawable = icon == null ? null : icon.loadDrawable(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCardId() {
|
||||
return mWalletCard.getCardId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawable getCardDrawable() {
|
||||
return mCardDrawable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getContentDescription() {
|
||||
return mWalletCard.getContentDescription();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawable getIcon() {
|
||||
return mIconDrawable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getLabel() {
|
||||
return mWalletCard.getCardLabel();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
* 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.systemui.wallet.ui;
|
||||
|
||||
import static com.android.systemui.wallet.ui.WalletCardCarousel.CARD_ANIM_ALPHA_DELAY;
|
||||
import static com.android.systemui.wallet.ui.WalletCardCarousel.CARD_ANIM_ALPHA_DURATION;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.annotation.Nullable;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.view.animation.Interpolator;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.systemui.R;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** Layout for the wallet screen. */
|
||||
public class WalletView extends FrameLayout implements WalletCardCarousel.OnCardScrollListener {
|
||||
|
||||
private static final int CAROUSEL_IN_ANIMATION_DURATION = 300;
|
||||
private static final int CAROUSEL_OUT_ANIMATION_DURATION = 200;
|
||||
private static final int CARD_LABEL_ANIM_DELAY = 133;
|
||||
private static final int CONTACTLESS_ICON_SIZE = 90;
|
||||
|
||||
private final WalletCardCarousel mCardCarousel;
|
||||
private final ImageView mIcon;
|
||||
private final TextView mCardLabel;
|
||||
private final Button mWalletButton;
|
||||
private final Interpolator mInInterpolator;
|
||||
private final Interpolator mOutInterpolator;
|
||||
private final float mAnimationTranslationX;
|
||||
private final ViewGroup mCardCarouselContainer;
|
||||
private final TextView mErrorView;
|
||||
private final ViewGroup mEmptyStateView;
|
||||
private CharSequence mCenterCardText;
|
||||
|
||||
public WalletView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public WalletView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
inflate(context, R.layout.wallet_fullscreen, this);
|
||||
mCardCarouselContainer = requireViewById(R.id.card_carousel_container);
|
||||
mCardCarousel = requireViewById(R.id.card_carousel);
|
||||
mCardCarousel.setCardScrollListener(this);
|
||||
mIcon = requireViewById(R.id.icon);
|
||||
mCardLabel = requireViewById(R.id.label);
|
||||
mWalletButton = requireViewById(R.id.wallet_button);
|
||||
mErrorView = requireViewById(R.id.error_view);
|
||||
mEmptyStateView = requireViewById(R.id.wallet_empty_state);
|
||||
mInInterpolator =
|
||||
AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in);
|
||||
mOutInterpolator =
|
||||
AnimationUtils.loadInterpolator(context, android.R.interpolator.accelerate_cubic);
|
||||
mAnimationTranslationX = mCardCarousel.getCardWidthPx() / 4f;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
mCardCarousel.setExpectedViewWidth(getWidth());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
// Forward touch events to card carousel to allow for swiping outside carousel bounds.
|
||||
return mCardCarousel.onTouchEvent(event) || super.onTouchEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCardScroll(WalletCardViewInfo centerCard, WalletCardViewInfo nextCard,
|
||||
float percentDistanceFromCenter) {
|
||||
CharSequence centerCardText = centerCard.getLabel();
|
||||
Drawable icon = centerCard.getIcon();
|
||||
if (icon != null) {
|
||||
mIcon.setImageDrawable(resizeDrawable(getResources(), icon));
|
||||
mIcon.setVisibility(VISIBLE);
|
||||
} else {
|
||||
mIcon.setVisibility(INVISIBLE);
|
||||
}
|
||||
if (!TextUtils.equals(mCenterCardText, centerCardText)) {
|
||||
mCenterCardText = centerCardText;
|
||||
mCardLabel.setText(centerCardText);
|
||||
}
|
||||
if (TextUtils.equals(centerCardText, nextCard.getLabel())) {
|
||||
mCardLabel.setAlpha(1f);
|
||||
} else {
|
||||
mCardLabel.setAlpha(percentDistanceFromCenter);
|
||||
mIcon.setAlpha(percentDistanceFromCenter);
|
||||
}
|
||||
}
|
||||
|
||||
void showCardCarousel(
|
||||
List<WalletCardViewInfo> data, int selectedIndex, boolean isDeviceLocked) {
|
||||
boolean shouldAnimate = mCardCarousel.setData(data, selectedIndex);
|
||||
mCardCarouselContainer.setVisibility(VISIBLE);
|
||||
mErrorView.setVisibility(GONE);
|
||||
if (isDeviceLocked) {
|
||||
// TODO(b/182964813): Add click action to prompt device unlock.
|
||||
mWalletButton.setText(R.string.wallet_button_label_device_locked);
|
||||
} else {
|
||||
mWalletButton.setText(R.string.wallet_button_label_device_unlocked);
|
||||
}
|
||||
if (shouldAnimate) {
|
||||
// If the empty state is visible, animate it away and delay the card carousel animation
|
||||
int emptyStateAnimDelay = 0;
|
||||
if (mEmptyStateView.getVisibility() == VISIBLE) {
|
||||
emptyStateAnimDelay = CARD_ANIM_ALPHA_DURATION;
|
||||
mEmptyStateView.animate()
|
||||
.alpha(0)
|
||||
.setDuration(emptyStateAnimDelay)
|
||||
.setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
mEmptyStateView.setVisibility(GONE);
|
||||
}
|
||||
})
|
||||
.start();
|
||||
}
|
||||
mCardLabel.setAlpha(0f);
|
||||
mCardLabel.animate().alpha(1f)
|
||||
.setStartDelay(CARD_LABEL_ANIM_DELAY + emptyStateAnimDelay)
|
||||
.setDuration(CARD_ANIM_ALPHA_DURATION)
|
||||
.start();
|
||||
mCardCarousel.setExtraAnimationDelay(emptyStateAnimDelay);
|
||||
mCardCarousel.setTranslationX(mAnimationTranslationX);
|
||||
mCardCarousel.animate().translationX(0)
|
||||
.setInterpolator(mInInterpolator)
|
||||
.setDuration(CAROUSEL_IN_ANIMATION_DURATION)
|
||||
.setStartDelay(emptyStateAnimDelay)
|
||||
.start();
|
||||
}
|
||||
}
|
||||
|
||||
void animateDismissal() {
|
||||
if (mCardCarouselContainer.getVisibility() != VISIBLE) {
|
||||
return;
|
||||
}
|
||||
mCardCarousel.animate().translationX(mAnimationTranslationX)
|
||||
.setInterpolator(mOutInterpolator)
|
||||
.setDuration(CAROUSEL_OUT_ANIMATION_DURATION)
|
||||
.start();
|
||||
mCardCarouselContainer.animate()
|
||||
.alpha(0f)
|
||||
.setDuration(CARD_ANIM_ALPHA_DURATION)
|
||||
.setStartDelay(CARD_ANIM_ALPHA_DELAY)
|
||||
.start();
|
||||
}
|
||||
|
||||
void showEmptyStateView(Drawable logo, CharSequence logoContentDescription, CharSequence label,
|
||||
OnClickListener clickListener) {
|
||||
mEmptyStateView.setVisibility(VISIBLE);
|
||||
mErrorView.setVisibility(GONE);
|
||||
mCardCarouselContainer.setVisibility(GONE);
|
||||
ImageView logoView = mEmptyStateView.requireViewById(R.id.empty_state_icon);
|
||||
logoView.setImageDrawable(logo);
|
||||
logoView.setContentDescription(logoContentDescription);
|
||||
mEmptyStateView.<TextView>requireViewById(R.id.empty_state_title).setText(label);
|
||||
mEmptyStateView.setOnClickListener(clickListener);
|
||||
}
|
||||
|
||||
void showErrorMessage(@Nullable CharSequence message) {
|
||||
if (TextUtils.isEmpty(message)) {
|
||||
message = getResources().getText(R.string.wallet_error_generic);
|
||||
}
|
||||
mErrorView.setText(message);
|
||||
mErrorView.setVisibility(VISIBLE);
|
||||
mCardCarouselContainer.setVisibility(GONE);
|
||||
mEmptyStateView.setVisibility(GONE);
|
||||
}
|
||||
|
||||
void hide() {
|
||||
setVisibility(GONE);
|
||||
}
|
||||
|
||||
void show() {
|
||||
setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
void hideErrorMessage() {
|
||||
mErrorView.setVisibility(GONE);
|
||||
}
|
||||
|
||||
WalletCardCarousel getCardCarousel() {
|
||||
return mCardCarousel;
|
||||
}
|
||||
|
||||
Button getWalletButton() {
|
||||
return mWalletButton;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
TextView getErrorView() {
|
||||
return mErrorView;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
ViewGroup getEmptyStateView() {
|
||||
return mEmptyStateView;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
ViewGroup getCardCarouselContainer() {
|
||||
return mCardCarouselContainer;
|
||||
}
|
||||
|
||||
private static Drawable resizeDrawable(Resources resources, Drawable drawable) {
|
||||
Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
|
||||
return new BitmapDrawable(resources, Bitmap.createScaledBitmap(
|
||||
bitmap, CONTACTLESS_ICON_SIZE, CONTACTLESS_ICON_SIZE, true));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
/*
|
||||
* 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.systemui.wallet.ui;
|
||||
|
||||
import static android.view.View.GONE;
|
||||
import static android.view.View.VISIBLE;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.Drawable;
|
||||
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.quickaccesswallet.WalletServiceEvent;
|
||||
import android.testing.AndroidTestingRunner;
|
||||
import android.testing.TestableLooper;
|
||||
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.systemui.SysuiTestCase;
|
||||
import com.android.systemui.plugins.ActivityStarter;
|
||||
import com.android.systemui.settings.UserTracker;
|
||||
|
||||
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
|
||||
@SmallTest
|
||||
public class WalletScreenControllerTest extends SysuiTestCase {
|
||||
|
||||
private static final int MAX_CARDS = 10;
|
||||
private static final String CARD_ID = "card_id";
|
||||
private static final CharSequence SHORTCUT_SHORT_LABEL = "View all";
|
||||
private static final CharSequence SHORTCUT_LONG_LABEL = "Add a payment method";
|
||||
private static final CharSequence SERVICE_LABEL = "Wallet app";
|
||||
private final WalletView mWalletView = new WalletView(mContext);
|
||||
private final Drawable mWalletLogo = mContext.getDrawable(android.R.drawable.ic_lock_lock);
|
||||
private final Intent mWalletIntent = new Intent(QuickAccessWalletService.ACTION_VIEW_WALLET)
|
||||
.setComponent(new ComponentName(mContext.getPackageName(), "WalletActivity"));
|
||||
|
||||
@Mock
|
||||
QuickAccessWalletClient mWalletClient;
|
||||
@Mock
|
||||
ActivityStarter mActivityStarter;
|
||||
@Mock
|
||||
UserTracker mUserTracker;
|
||||
@Captor
|
||||
ArgumentCaptor<Intent> mIntentCaptor;
|
||||
@Captor
|
||||
ArgumentCaptor<GetWalletCardsRequest> mRequestCaptor;
|
||||
@Captor
|
||||
ArgumentCaptor<QuickAccessWalletClient.OnWalletCardsRetrievedCallback> mCallbackCaptor;
|
||||
@Captor
|
||||
ArgumentCaptor<QuickAccessWalletClient.WalletServiceEventListener> mListenerCaptor;
|
||||
private WalletScreenController mController;
|
||||
private TestableLooper mTestableLooper;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mTestableLooper = TestableLooper.get(this);
|
||||
when(mUserTracker.getUserContext()).thenReturn(mContext);
|
||||
when(mWalletClient.getLogo()).thenReturn(mWalletLogo);
|
||||
when(mWalletClient.getShortcutLongLabel()).thenReturn(SHORTCUT_LONG_LABEL);
|
||||
when(mWalletClient.getShortcutShortLabel()).thenReturn(SHORTCUT_SHORT_LABEL);
|
||||
when(mWalletClient.getServiceLabel()).thenReturn(SERVICE_LABEL);
|
||||
when(mWalletClient.createWalletIntent()).thenReturn(mWalletIntent);
|
||||
mController = new WalletScreenController(
|
||||
mContext,
|
||||
mWalletView,
|
||||
mWalletClient,
|
||||
mActivityStarter,
|
||||
MoreExecutors.directExecutor(),
|
||||
new Handler(mTestableLooper.getLooper()),
|
||||
mUserTracker,
|
||||
/* isDeviceLocked= */false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queryCards_hasCards_showCarousel() {
|
||||
GetWalletCardsResponse response =
|
||||
new GetWalletCardsResponse(
|
||||
Collections.singletonList(createWalletCard(mContext)), 0);
|
||||
|
||||
mController.queryWalletCards();
|
||||
mTestableLooper.processAllMessages();
|
||||
|
||||
verify(mWalletClient).getWalletCards(any(), any(), mCallbackCaptor.capture());
|
||||
|
||||
mCallbackCaptor.getValue().onWalletCardsRetrieved(response);
|
||||
mTestableLooper.processAllMessages();
|
||||
|
||||
assertEquals(VISIBLE, mWalletView.getCardCarouselContainer().getVisibility());
|
||||
assertEquals(GONE, mWalletView.getErrorView().getVisibility());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queryCards_noCards_showEmptyState() {
|
||||
GetWalletCardsResponse response = new GetWalletCardsResponse(Collections.EMPTY_LIST, 0);
|
||||
|
||||
mController.queryWalletCards();
|
||||
mTestableLooper.processAllMessages();
|
||||
|
||||
verify(mWalletClient).getWalletCards(any(), any(), mCallbackCaptor.capture());
|
||||
|
||||
mCallbackCaptor.getValue().onWalletCardsRetrieved(response);
|
||||
mTestableLooper.processAllMessages();
|
||||
|
||||
assertEquals(GONE, mWalletView.getCardCarouselContainer().getVisibility());
|
||||
assertEquals(VISIBLE, mWalletView.getEmptyStateView().getVisibility());
|
||||
assertEquals(GONE, mWalletView.getErrorView().getVisibility());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queryCards_error_showErrorView() {
|
||||
String errorMessage = "getWalletCardsError";
|
||||
GetWalletCardsError error = new GetWalletCardsError(createIcon(), errorMessage);
|
||||
|
||||
mController.queryWalletCards();
|
||||
mTestableLooper.processAllMessages();
|
||||
|
||||
verify(mWalletClient).getWalletCards(any(), any(), mCallbackCaptor.capture());
|
||||
|
||||
mCallbackCaptor.getValue().onWalletCardRetrievalError(error);
|
||||
mTestableLooper.processAllMessages();
|
||||
|
||||
assertEquals(GONE, mWalletView.getCardCarouselContainer().getVisibility());
|
||||
assertEquals(GONE, mWalletView.getEmptyStateView().getVisibility());
|
||||
assertEquals(VISIBLE, mWalletView.getErrorView().getVisibility());
|
||||
assertEquals(errorMessage, mWalletView.getErrorView().getText().toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onWalletServiceEvent_nfcPaymentStart_dismiss() {
|
||||
WalletServiceEvent event =
|
||||
new WalletServiceEvent(WalletServiceEvent.TYPE_NFC_PAYMENT_STARTED);
|
||||
|
||||
mController.onWalletServiceEvent(event);
|
||||
mTestableLooper.processAllMessages();
|
||||
|
||||
assertNull(mController.mSelectedCardId);
|
||||
assertTrue(mController.mIsDismissed);
|
||||
verify(mWalletClient).notifyWalletDismissed();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onWalletServiceEvent_walletCardsUpdate_queryCards() {
|
||||
mController.queryWalletCards();
|
||||
|
||||
verify(mWalletClient).addWalletServiceEventListener(mListenerCaptor.capture());
|
||||
|
||||
WalletServiceEvent event =
|
||||
new WalletServiceEvent(WalletServiceEvent.TYPE_WALLET_CARDS_UPDATED);
|
||||
|
||||
QuickAccessWalletClient.WalletServiceEventListener listener = mListenerCaptor.getValue();
|
||||
listener.onWalletServiceEvent(event);
|
||||
mTestableLooper.processAllMessages();
|
||||
|
||||
verify(mWalletClient, times(2))
|
||||
.getWalletCards(any(), mRequestCaptor.capture(), mCallbackCaptor.capture());
|
||||
|
||||
GetWalletCardsRequest request = mRequestCaptor.getValue();
|
||||
|
||||
assertEquals(MAX_CARDS, request.getMaxCards());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onCardSelected() {
|
||||
mController.onCardSelected(createCardViewInfo());
|
||||
|
||||
assertEquals(CARD_ID, mController.mSelectedCardId);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onCardClicked_startIntent() {
|
||||
WalletCardViewInfo walletCardViewInfo = createCardViewInfo();
|
||||
|
||||
mController.onCardClicked(walletCardViewInfo);
|
||||
|
||||
verify(mActivityStarter).startActivity(mIntentCaptor.capture(), eq(true));
|
||||
|
||||
assertEquals(mWalletIntent.getAction(), mIntentCaptor.getValue().getAction());
|
||||
assertEquals(mWalletIntent.getComponent(), mIntentCaptor.getValue().getComponent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onWalletCardsRetrieved_cardDataEmpty_intentIsNull_hidesWallet() {
|
||||
when(mWalletClient.createWalletIntent()).thenReturn(null);
|
||||
GetWalletCardsResponse response = new GetWalletCardsResponse(Collections.emptyList(), 0);
|
||||
|
||||
mController.onWalletCardsRetrieved(response);
|
||||
mTestableLooper.processAllMessages();
|
||||
|
||||
assertEquals(GONE, mWalletView.getVisibility());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onWalletCardsRetrieved_cardDataEmpty_logoIsNull_hidesWallet() {
|
||||
when(mWalletClient.getLogo()).thenReturn(null);
|
||||
GetWalletCardsResponse response = new GetWalletCardsResponse(Collections.emptyList(), 0);
|
||||
|
||||
mController.onWalletCardsRetrieved(response);
|
||||
mTestableLooper.processAllMessages();
|
||||
|
||||
assertEquals(GONE, mWalletView.getVisibility());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onWalletCardsRetrieved_cardDataEmpty_labelIsEmpty_hidesWallet() {
|
||||
when(mWalletClient.getShortcutLongLabel()).thenReturn("");
|
||||
GetWalletCardsResponse response = new GetWalletCardsResponse(Collections.emptyList(), 0);
|
||||
|
||||
mController.onWalletCardsRetrieved(response);
|
||||
mTestableLooper.processAllMessages();
|
||||
|
||||
assertEquals(GONE, mWalletView.getVisibility());
|
||||
}
|
||||
|
||||
private WalletCard createWalletCard(Context context) {
|
||||
PendingIntent pendingIntent =
|
||||
PendingIntent.getActivity(context, 0, mWalletIntent, PendingIntent.FLAG_IMMUTABLE);
|
||||
return new WalletCard.Builder(CARD_ID, createIcon(), "description", pendingIntent).build();
|
||||
}
|
||||
|
||||
private static Icon createIcon() {
|
||||
return Icon.createWithBitmap(Bitmap.createBitmap(70, 44, Bitmap.Config.ARGB_8888));
|
||||
}
|
||||
|
||||
private WalletCardViewInfo createCardViewInfo() {
|
||||
return new WalletScreenController.QAWalletCardViewInfo(
|
||||
mContext, createWalletCard(mContext));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user