OSDN Git Service

NFC payment settings.
authorMartijn Coenen <maco@google.com>
Fri, 2 Aug 2013 01:13:33 +0000 (18:13 -0700)
committerMartijn Coenen <maco@google.com>
Wed, 7 Aug 2013 02:16:54 +0000 (19:16 -0700)
First version, pending final UX.

Change-Id: I357e900c3f2012b35814ae197c49a8c9b97b7148

AndroidManifest.xml
proguard.flags
res/drawable-xhdpi/ic_settings_nfc_payment.png [new file with mode: 0644]
res/layout/nfc_payment_option.xml [new file with mode: 0644]
res/values/strings.xml
res/xml/nfc_payment_settings.xml [new file with mode: 0644]
res/xml/settings_headers.xml
src/com/android/settings/Settings.java
src/com/android/settings/nfc/PaymentBackend.java [new file with mode: 0644]
src/com/android/settings/nfc/PaymentDefaultDialog.java [new file with mode: 0644]
src/com/android/settings/nfc/PaymentSettings.java [new file with mode: 0644]

index 10c66d4..5769305 100644 (file)
                 android:resource="@id/user_settings" />
         </activity>
 
+        <activity android:name="Settings$PaymentSettingsActivity"
+                android:uiOptions="splitActionBarWhenNarrow"
+                android:label="@string/nfc_payment_settings_title"
+                android:taskAffinity=""
+                android:excludeFromRecents="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <action android:name="android.settings.NFC_PAYMENT_SETTINGS" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <meta-data android:name="com.android.settings.FRAGMENT_CLASS"
+                android:value="com.android.settings.nfc.PaymentSettings" />
+            <meta-data android:name="com.android.settings.TOP_LEVEL_HEADER_ID"
+                android:resource="@id/nfc_payment_settings" />
+        </activity>
+        <activity android:name=".nfc.PaymentDefaultDialog"
+                  android:label="@string/nfc_payment_set_default"
+                  android:excludeFromRecents="true"
+                  android:theme="@*android:style/Theme.Holo.Light.Dialog.Alert">
+            <intent-filter>
+                <action android:name="android.nfc.cardemulation.ACTION_CHANGE_DEFAULT" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+
         <activity android:name="Settings$NotificationAccessSettingsActivity"
                   android:label="@string/manage_notification_access"
                   android:taskAffinity=""
index 8a6c2cb..211a5a4 100644 (file)
@@ -13,6 +13,7 @@
 -keep class com.android.settings.fuelgauge.*
 -keep class com.android.settings.users.*
 -keep class com.android.settings.NotificationStation
+-keep class com.android.settings.nfc.*
 
 # Keep click responders
 -keepclassmembers class com.android.settings.inputmethod.UserDictionaryAddWordActivity {
diff --git a/res/drawable-xhdpi/ic_settings_nfc_payment.png b/res/drawable-xhdpi/ic_settings_nfc_payment.png
new file mode 100644 (file)
index 0000000..3590a15
Binary files /dev/null and b/res/drawable-xhdpi/ic_settings_nfc_payment.png differ
diff --git a/res/layout/nfc_payment_option.xml b/res/layout/nfc_payment_option.xml
new file mode 100644 (file)
index 0000000..122e041
--- /dev/null
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_weight="1"
+    android:id="@+id/nfc_payment_pref"
+    android:focusable="true"
+    android:clickable="true"
+    android:gravity="center_vertical"
+    android:minHeight="?android:attr/listPreferredItemHeight"
+    android:background="?android:attr/selectableItemBackground">
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:gravity="center"
+        android:minWidth="@*android:dimen/preference_icon_minWidth"
+        android:orientation="horizontal">
+        <ImageView
+            android:id="@+android:id/icon"
+            android:layout_width="48dp"
+            android:layout_height="48dp"
+            android:layout_gravity="center"
+            android:minWidth="48dp"
+            android:scaleType="centerInside"
+            android:layout_marginEnd="@*android:dimen/preference_item_padding_inner"
+                />
+    </LinearLayout>
+    <RelativeLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="6dip"
+        android:layout_marginTop="6dip"
+        android:layout_marginBottom="6dip"
+        android:layout_weight="1">
+        <TextView
+            android:id="@+android:id/title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:singleLine="true"
+            android:textAppearance="?android:attr/textAppearanceMedium"
+            android:ellipsize="marquee"
+            android:fadingEdge="horizontal"/>
+        <TextView
+            android:id="@android:id/summary"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@android:id/title"
+            android:layout_alignStart="@android:id/title"
+            android:paddingBottom="3dip"
+            android:visibility="gone"
+            android:textAppearance="?android:attr/textAppearanceSmall"
+            android:textSize="13sp"
+            android:textColor="?android:attr/textColorSecondary"
+            android:focusable="false"
+            android:maxLines="4" />
+    </RelativeLayout>
+    <RadioButton
+        android:id="@android:id/button1"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:layout_alignParentEnd="true"
+        android:layout_centerVertical="true"
+        android:duplicateParentState="true"
+        android:clickable="false"
+        android:focusable="false" />
+</LinearLayout>
index a4a3aec..9d20fab 100644 (file)
     <!-- Warning message title for global font change [CHAR LIMIT=40] -->
     <string name="global_font_change_title">Change font size</string>
 
+    <!-- NFC payment settings --><skip/>
+    <string name="nfc_payment_settings_title">Tap and Pay</string>
+    <!-- Option to tell Android to ask the user which payment app to use every time
+         a payment terminal is tapped -->
+    <string name="nfc_payment_ask">Ask every time</string>
+    <!-- Label for the dialog that is shown when the user is asked to set a
+         preferred payment application -->
+    <string name="nfc_payment_set_default">Set as your preference?</string>
+
     <!-- Restrictions settings --><skip/>
 
     <!-- Restriction settings title [CHAR LIMIT=35] -->
diff --git a/res/xml/nfc_payment_settings.xml b/res/xml/nfc_payment_settings.xml
new file mode 100644 (file)
index 0000000..9b47dda
--- /dev/null
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+</PreferenceScreen>
index 867fc19..65c42ef 100644 (file)
         android:title="@string/user_settings_title"
         android:id="@+id/user_settings" />
 
+    <!-- Manage NFC payment apps -->
+    <header
+        android:fragment="com.android.settings.nfc.PaymentSettings"
+        android:icon="@drawable/ic_settings_nfc_payment"
+        android:title="@string/nfc_payment_settings_title"
+        android:id="@+id/nfc_payment_settings" />
+
     <!-- Manufacturer hook -->
     <header
         android:fragment="com.android.settings.WirelessSettings"
index 4221059..c327f4a 100644 (file)
@@ -143,7 +143,8 @@ public class Settings extends PreferenceActivity
             R.id.date_time_settings,
             R.id.about_settings,
             R.id.accessibility_settings,
-            R.id.print_settings
+            R.id.print_settings,
+            R.id.nfc_payment_settings
     };
 
     private SharedPreferences mDevelopmentPreferences;
@@ -551,6 +552,10 @@ public class Settings extends PreferenceActivity
                         || Utils.isMonkeyRunning()) {
                     target.remove(i);
                 }
+            } else if (id == R.id.nfc_payment_settings) {
+                if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_NFC_HCE)) {
+                    target.remove(i);
+                }
             } else if (id == R.id.development_settings) {
                 if (!showDev) {
                     target.remove(i);
@@ -945,4 +950,5 @@ public class Settings extends PreferenceActivity
     public static class UserSettingsActivity extends Settings { /* empty */ }
     public static class NotificationAccessSettingsActivity extends Settings { /* empty */ }
     public static class UsbSettingsActivity extends Settings { /* empty */ }
+    public static class NfcPaymentActivity extends Settings { /* empty */ }
 }
diff --git a/src/com/android/settings/nfc/PaymentBackend.java b/src/com/android/settings/nfc/PaymentBackend.java
new file mode 100644 (file)
index 0000000..fc0f4a3
--- /dev/null
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2013 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.settings.nfc;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
+import android.nfc.NfcAdapter;
+import android.nfc.cardemulation.ApduServiceInfo;
+import android.nfc.cardemulation.CardEmulationManager;
+import android.provider.Settings;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PaymentBackend {
+    public static final String TAG = "Settings.PaymentBackend";
+
+    public static class PaymentAppInfo {
+        CharSequence caption;
+        Drawable icon;
+        boolean isDefault;
+        public ComponentName componentName;
+    }
+
+    private final Context mContext;
+    private final NfcAdapter mAdapter;
+    private final CardEmulationManager mCardEmuManager;
+
+    public PaymentBackend(Context context) {
+        mContext = context;
+
+        mAdapter = NfcAdapter.getDefaultAdapter(context);
+        mCardEmuManager = CardEmulationManager.getInstance(mAdapter);
+    }
+
+    public List<PaymentAppInfo> getPaymentAppInfos() {
+        PackageManager pm = mContext.getPackageManager();
+        List<ApduServiceInfo> serviceInfos =
+                mCardEmuManager.getServices(CardEmulationManager.CATEGORY_PAYMENT);
+        List<PaymentAppInfo> appInfos = new ArrayList<PaymentAppInfo>();
+
+        if (serviceInfos == null) return appInfos;
+
+        ComponentName defaultApp = getDefaultPaymentApp();
+
+        for (ApduServiceInfo service : serviceInfos) {
+            PaymentAppInfo appInfo = new PaymentAppInfo();
+            appInfo.caption = service.loadLabel(pm);
+            appInfo.icon = service.loadIcon(pm);
+            appInfo.isDefault = service.getComponent().equals(defaultApp);
+            appInfo.componentName = service.getComponent();
+            appInfos.add(appInfo);
+        }
+
+        return appInfos;
+    }
+
+    ComponentName getDefaultPaymentApp() {
+        String componentString = Settings.Secure.getString(mContext.getContentResolver(),
+                Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT);
+        if (componentString != null) {
+            return ComponentName.unflattenFromString(componentString);
+        } else {
+            return null;
+        }
+    }
+
+    public void setDefaultPaymentApp(ComponentName app) {
+        Settings.Secure.putString(mContext.getContentResolver(),
+                Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT,
+                app != null ? app.flattenToString() : null);
+    }
+
+    public boolean isAutoPaymentMode() {
+        String mode = Settings.Secure.getString(mContext.getContentResolver(),
+                Settings.Secure.NFC_PAYMENT_MODE);
+        return (!CardEmulationManager.PAYMENT_MODE_MANUAL.equals(mode));
+    }
+
+    public void setAutoPaymentMode(boolean enable) {
+        Settings.Secure.putString(mContext.getContentResolver(),
+                Settings.Secure.NFC_PAYMENT_MODE,
+                enable ? CardEmulationManager.PAYMENT_MODE_AUTO
+                       : CardEmulationManager.PAYMENT_MODE_MANUAL);
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/nfc/PaymentDefaultDialog.java b/src/com/android/settings/nfc/PaymentDefaultDialog.java
new file mode 100644 (file)
index 0000000..f3217dd
--- /dev/null
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2013 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.settings.nfc;
+
+import android.content.ComponentName;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.nfc.cardemulation.CardEmulationManager;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.android.internal.app.AlertActivity;
+import com.android.internal.app.AlertController;
+import com.android.settings.R;
+import com.android.settings.nfc.PaymentBackend.PaymentAppInfo;
+
+import java.util.List;
+
+public final class PaymentDefaultDialog extends AlertActivity implements
+        DialogInterface.OnClickListener {
+
+    public static final String TAG = "PaymentDefaultDialog";
+
+    private PaymentBackend mBackend;
+    private ComponentName mNewDefault;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mBackend = new PaymentBackend(this);
+        Intent intent = getIntent();
+        ComponentName component = intent.getParcelableExtra(
+                CardEmulationManager.EXTRA_SERVICE_COMPONENT);
+        String category = intent.getStringExtra(CardEmulationManager.EXTRA_CATEGORY);
+
+        if (!buildDialog(component, category)) {
+            finish();
+        }
+
+    }
+
+    @Override
+    public void onClick(DialogInterface dialog, int which) {
+        switch (which) {
+            case BUTTON_POSITIVE:
+                mBackend.setDefaultPaymentApp(mNewDefault);
+                mBackend.setAutoPaymentMode(true);
+                break;
+            case BUTTON_NEGATIVE:
+                break;
+        }
+    }
+
+    private boolean buildDialog(ComponentName component, String category) {
+        if (component == null || category == null) {
+            Log.e(TAG, "Component or category are null");
+            return false;
+        }
+
+        if (!CardEmulationManager.CATEGORY_PAYMENT.equals(category)) {
+            Log.e(TAG, "Don't support defaults for category " + category);
+            return false;
+        }
+
+        // Check if passed in service exists
+        boolean found = false;
+
+        List<PaymentAppInfo> services = mBackend.getPaymentAppInfos();
+        for (PaymentAppInfo service : services) {
+            if (component.equals(service.componentName)) {
+                found = true;
+                break;
+            }
+        }
+
+        if (!found) {
+            Log.e(TAG, "Component " + component + " is not a registered payment service.");
+            return false;
+        }
+
+        // Get current mode and default component
+        boolean isAuto = mBackend.isAutoPaymentMode();
+        ComponentName defaultComponent = mBackend.getDefaultPaymentApp();
+        if (defaultComponent != null && defaultComponent.equals(component)) {
+            Log.e(TAG, "Component " + component + " is already default.");
+            return false;
+        }
+
+        PackageManager pm = getPackageManager();
+        ApplicationInfo newAppInfo;
+        try {
+            newAppInfo = pm.getApplicationInfo(component.getPackageName(), 0);
+        } catch (NameNotFoundException e) {
+            Log.e(TAG, "PM could not load app info for " + component);
+            return false;
+        }
+        ApplicationInfo defaultAppInfo = null;
+        try {
+            if (defaultComponent != null) {
+                defaultAppInfo = pm.getApplicationInfo(defaultComponent.getPackageName(), 0);
+            }
+        } catch (NameNotFoundException e) {
+            Log.e(TAG, "PM could not load app info for " + defaultComponent);
+            // Continue intentionally
+        }
+
+        mNewDefault = component;
+
+        // Compose dialog; get
+        final AlertController.AlertParams p = mAlertParams;
+        p.mTitle = getString(R.string.nfc_payment_set_default);
+        if (defaultAppInfo == null || !isAuto) {
+            p.mMessage = "Always use " + newAppInfo.loadLabel(pm) + " when you tap and pay?";
+        } else {
+            p.mMessage = "Always use " + newAppInfo.loadLabel(pm) + " instead of " +
+                    defaultAppInfo.loadLabel(pm) + " when you tap and pay?";
+        }
+        p.mPositiveButtonText = getString(R.string.yes);
+        p.mNegativeButtonText = getString(R.string.no);
+        p.mPositiveButtonListener = this;
+        p.mNegativeButtonListener = this;
+        setupAlert();
+
+        return true;
+    }
+
+}
diff --git a/src/com/android/settings/nfc/PaymentSettings.java b/src/com/android/settings/nfc/PaymentSettings.java
new file mode 100644 (file)
index 0000000..a1ed883
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2013 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.settings.nfc;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceManager;
+import android.preference.PreferenceScreen;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.RadioButton;
+
+import com.android.settings.R;
+import com.android.settings.SettingsPreferenceFragment;
+import com.android.settings.nfc.PaymentBackend.PaymentAppInfo;
+
+import java.util.List;
+
+public class PaymentSettings extends SettingsPreferenceFragment implements
+        OnClickListener {
+    public static final String TAG = "PaymentSettings";
+    private PaymentBackend mPaymentBackend;
+
+    @Override
+    public void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+
+        setHasOptionsMenu(false);
+        mPaymentBackend = new PaymentBackend(getActivity());
+    }
+
+    public void refresh() {
+        PreferenceManager manager = getPreferenceManager();
+        PreferenceScreen screen = manager.createPreferenceScreen(getActivity());
+
+        boolean isAuto = mPaymentBackend.isAutoPaymentMode();
+
+        // Get all payment services
+        List<PaymentAppInfo> appInfos = mPaymentBackend.getPaymentAppInfos();
+        if (appInfos != null && appInfos.size() > 0) {
+            // Add all payment apps
+            for (PaymentAppInfo appInfo : appInfos) {
+                PaymentAppPreference preference =
+                        new PaymentAppPreference(getActivity(), appInfo, this);
+                // If for some reason isAuto gets out of sync, clear out app default
+                appInfo.isDefault &= isAuto;
+                preference.setIcon(appInfo.icon);
+                preference.setTitle(appInfo.caption);
+                screen.addPreference(preference);
+            }
+            if (appInfos.size() > 1) {
+                PaymentAppInfo appInfo = new PaymentAppInfo();
+                appInfo.icon = null;
+                appInfo.componentName = null;
+                appInfo.isDefault = !isAuto;
+                // Add "Ask every time" option
+                PaymentAppPreference preference =
+                        new PaymentAppPreference(getActivity(), appInfo, this);
+                preference.setIcon(null);
+                preference.setTitle(R.string.nfc_payment_ask);
+                screen.addPreference(preference);
+            }
+        }
+        setPreferenceScreen(screen);
+    }
+
+    @Override
+    public void onClick(View v) {
+        if (v.getTag() instanceof PaymentAppInfo) {
+            PaymentAppInfo appInfo = (PaymentAppInfo) v.getTag();
+            if (appInfo.componentName != null) {
+                mPaymentBackend.setDefaultPaymentApp(appInfo.componentName);
+                mPaymentBackend.setAutoPaymentMode(true);
+            } else {
+                mPaymentBackend.setDefaultPaymentApp(null);
+                mPaymentBackend.setAutoPaymentMode(false);
+            }
+            refresh();
+        }
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        refresh();
+    }
+
+    public static class PaymentAppPreference extends Preference {
+        private final OnClickListener listener;
+        private final PaymentAppInfo appInfo;
+
+        public PaymentAppPreference(Context context, PaymentAppInfo appInfo,
+                OnClickListener listener) {
+            super(context);
+            setLayoutResource(R.layout.nfc_payment_option);
+            this.appInfo = appInfo;
+            this.listener = listener;
+        }
+
+        @Override
+        protected void onBindView(View view) {
+            super.onBindView(view);
+
+            view.setOnClickListener(listener);
+            view.setTag(appInfo);
+
+            RadioButton radioButton = (RadioButton) view.findViewById(android.R.id.button1);
+            radioButton.setChecked(appInfo.isDefault);
+        }
+    }
+}
\ No newline at end of file