1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.carrierdefaultapp;
17 
18 import android.annotation.NonNull;
19 import android.annotation.Nullable;
20 import android.app.Notification;
21 import android.app.NotificationChannel;
22 import android.app.NotificationManager;
23 import android.app.PendingIntent;
24 import android.content.BroadcastReceiver;
25 import android.content.ComponentName;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.res.Configuration;
29 import android.content.res.Resources;
30 import android.graphics.drawable.Icon;
31 import android.os.LocaleList;
32 import android.os.SystemProperties;
33 import android.os.UserHandle;
34 import android.telephony.AnomalyReporter;
35 import android.telephony.SubscriptionManager;
36 import android.telephony.TelephonyManager;
37 import android.text.TextUtils;
38 import android.util.Log;
39 import android.webkit.URLUtil;
40 import android.webkit.WebView;
41 
42 import com.android.internal.annotations.VisibleForTesting;
43 import com.android.phone.slice.SlicePurchaseController;
44 
45 import java.net.MalformedURLException;
46 import java.net.URISyntaxException;
47 import java.net.URL;
48 import java.util.HashMap;
49 import java.util.Locale;
50 import java.util.Map;
51 import java.util.UUID;
52 
53 /**
54  * The SlicePurchaseBroadcastReceiver listens for
55  * {@link SlicePurchaseController#ACTION_START_SLICE_PURCHASE_APP} from the SlicePurchaseController
56  * in the phone process to start the slice purchase application. It displays the performance boost
57  * notification to the user and will start the {@link SlicePurchaseActivity} to display the
58  * {@link WebView} to purchase performance boosts from the user's carrier.
59  */
60 public class SlicePurchaseBroadcastReceiver extends BroadcastReceiver{
61     private static final String TAG = "SlicePurchaseBroadcastReceiver";
62 
63     /**
64      * UUID to report an anomaly when receiving a PendingIntent from an application or process
65      * other than the Phone process.
66      */
67     private static final String UUID_BAD_PENDING_INTENT = "c360246e-95dc-4abf-9dc1-929a76cd7e53";
68 
69     /** Channel ID for the performance boost notification. */
70     private static final String PERFORMANCE_BOOST_NOTIFICATION_CHANNEL_ID = "performance_boost";
71     /** Tag for the performance boost notification. */
72     public static final String PERFORMANCE_BOOST_NOTIFICATION_TAG = "SlicePurchaseApp.Notification";
73     /**
74      * Action for when the user clicks the "Not now" button on the performance boost notification.
75      */
76     private static final String ACTION_NOTIFICATION_CANCELED =
77             "com.android.phone.slice.action.NOTIFICATION_CANCELED";
78 
79     /**
80      * A map of Intents sent by {@link SlicePurchaseController} for each capability.
81      * If this map contains an Intent for a given capability, the performance boost notification to
82      * purchase the capability is visible to the user.
83      * If this map does not contain an Intent for a given capability, either the capability was
84      * never requested or the {@link SlicePurchaseActivity} is visible to the user.
85      * An Intent is added to this map when the performance boost notification is displayed to the
86      * user and removed from the map when the notification is canceled.
87      */
88     private static final Map<Integer, Intent> sIntents = new HashMap<>();
89 
90     /**
91      * Cancel the performance boost notification for the given capability and
92      * remove the corresponding notification intent from the map.
93      *
94      * @param context The context to cancel the notification in.
95      * @param capability The premium capability to cancel the notification for.
96      */
cancelNotification(@onNull Context context, @TelephonyManager.PremiumCapability int capability)97     public static void cancelNotification(@NonNull Context context,
98             @TelephonyManager.PremiumCapability int capability) {
99         context.getSystemService(NotificationManager.class).cancelAsUser(
100                 PERFORMANCE_BOOST_NOTIFICATION_TAG, capability, UserHandle.ALL);
101         sIntents.remove(capability);
102     }
103 
104     /**
105      * Send the PendingIntent containing the corresponding slice purchase application response.
106      *
107      * @param intent The Intent containing the PendingIntent extra.
108      * @param extra The extra to get the PendingIntent to send.
109      */
sendSlicePurchaseAppResponse(@onNull Intent intent, @NonNull String extra)110     public static void sendSlicePurchaseAppResponse(@NonNull Intent intent, @NonNull String extra) {
111         PendingIntent pendingIntent = intent.getParcelableExtra(extra, PendingIntent.class);
112         if (pendingIntent == null) {
113             loge("PendingIntent does not exist for extra: " + extra);
114             return;
115         }
116         try {
117             pendingIntent.send();
118         } catch (PendingIntent.CanceledException e) {
119             loge("Unable to send " + getPendingIntentType(extra) + " intent: " + e);
120         }
121     }
122 
123     /**
124      * Send the PendingIntent containing the corresponding slice purchase application response
125      * with additional data.
126      *
127      * @param context The Context to use to send the PendingIntent.
128      * @param intent The Intent containing the PendingIntent extra.
129      * @param extra The extra to get the PendingIntent to send.
130      * @param data The Intent containing additional data to send with the PendingIntent.
131      */
sendSlicePurchaseAppResponseWithData(@onNull Context context, @NonNull Intent intent, @NonNull String extra, @NonNull Intent data)132     public static void sendSlicePurchaseAppResponseWithData(@NonNull Context context,
133             @NonNull Intent intent, @NonNull String extra, @NonNull Intent data) {
134         PendingIntent pendingIntent = intent.getParcelableExtra(extra, PendingIntent.class);
135         if (pendingIntent == null) {
136             loge("PendingIntent does not exist for extra: " + extra);
137             return;
138         }
139         try {
140             pendingIntent.send(context, 0 /* unused */, data);
141         } catch (PendingIntent.CanceledException e) {
142             loge("Unable to send " + getPendingIntentType(extra) + " intent: " + e);
143         }
144     }
145 
146     /**
147      * Check whether the Intent is valid and can be used to complete purchases in the slice purchase
148      * application. This checks that all necessary extras exist and that the values are valid.
149      *
150      * @param intent The intent to check.
151      * @return {@code true} if the intent is valid and {@code false} otherwise.
152      */
isIntentValid(@onNull Intent intent)153     public static boolean isIntentValid(@NonNull Intent intent) {
154         int phoneId = intent.getIntExtra(SlicePurchaseController.EXTRA_PHONE_ID,
155                 SubscriptionManager.INVALID_PHONE_INDEX);
156         if (phoneId == SubscriptionManager.INVALID_PHONE_INDEX) {
157             loge("isIntentValid: invalid phone index: " + phoneId);
158             return false;
159         }
160 
161         int subId = intent.getIntExtra(SlicePurchaseController.EXTRA_SUB_ID,
162                 SubscriptionManager.INVALID_SUBSCRIPTION_ID);
163         if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
164             loge("isIntentValid: invalid subscription ID: " + subId);
165             return false;
166         }
167 
168         int capability = intent.getIntExtra(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY,
169                 SlicePurchaseController.PREMIUM_CAPABILITY_INVALID);
170         if (capability == SlicePurchaseController.PREMIUM_CAPABILITY_INVALID) {
171             loge("isIntentValid: invalid premium capability: " + capability);
172             return false;
173         }
174 
175         String purchaseUrl = intent.getStringExtra(SlicePurchaseController.EXTRA_PURCHASE_URL);
176         String userData = intent.getStringExtra(SlicePurchaseController.EXTRA_USER_DATA);
177         String contentsType = intent.getStringExtra(SlicePurchaseController.EXTRA_CONTENTS_TYPE);
178         if (getPurchaseUrl(purchaseUrl, userData, TextUtils.isEmpty(contentsType)) == null) {
179             loge("isIntentValid: invalid purchase URL: " + purchaseUrl);
180             return false;
181         }
182 
183         String carrier = intent.getStringExtra(SlicePurchaseController.EXTRA_CARRIER);
184         if (TextUtils.isEmpty(carrier)) {
185             loge("isIntentValid: empty carrier: " + carrier);
186             return false;
187         }
188 
189         return isPendingIntentValid(intent, SlicePurchaseController.EXTRA_INTENT_CANCELED)
190                 && isPendingIntentValid(intent, SlicePurchaseController.EXTRA_INTENT_CARRIER_ERROR)
191                 && isPendingIntentValid(intent, SlicePurchaseController.EXTRA_INTENT_REQUEST_FAILED)
192                 && isPendingIntentValid(intent,
193                         SlicePurchaseController.EXTRA_INTENT_NOT_DEFAULT_DATA_SUBSCRIPTION)
194                 && isPendingIntentValid(intent,
195                         SlicePurchaseController.EXTRA_INTENT_NOTIFICATIONS_DISABLED)
196                 && isPendingIntentValid(intent, SlicePurchaseController.EXTRA_INTENT_SUCCESS)
197                 && isPendingIntentValid(intent,
198                         SlicePurchaseController.EXTRA_INTENT_NOTIFICATION_SHOWN);
199     }
200 
201     /**
202      * Get the {@link URL} from the given purchase URL String and user data, if it is valid.
203      *
204      * @param purchaseUrl The purchase URL String to use to create the URL.
205      * @param userData The user data parameter from the entitlement server.
206      * @param shouldAppendUserData If this is {@code true} and the {@code userData} exists,
207      *        the {@code userData} should be appended to the {@code purchaseUrl} to create the URL.
208      *        If this is false, only the {@code purchaseUrl} should be used and the {@code userData}
209      *        will be sent as data to the POST request instead.
210      * @return The URL from the given purchase URL and user data or {@code null} if it is invalid.
211      */
getPurchaseUrl(@ullable String purchaseUrl, @Nullable String userData, boolean shouldAppendUserData)212     @Nullable public static URL getPurchaseUrl(@Nullable String purchaseUrl,
213             @Nullable String userData, boolean shouldAppendUserData) {
214         if (purchaseUrl == null) {
215             return null;
216         }
217         // Only append user data if it exists, otherwise just return the purchase URL
218         if (!shouldAppendUserData || TextUtils.isEmpty(userData)) {
219             return getPurchaseUrl(purchaseUrl);
220         }
221         URL url = getPurchaseUrl(purchaseUrl + "?" + userData);
222         if (url == null) {
223             url = getPurchaseUrl(purchaseUrl);
224         }
225         return url;
226     }
227 
228     /**
229      * Get the {@link URL} from the given purchase URL String, if it is valid.
230      *
231      * @param purchaseUrl The purchase URL String to use to create the URL.
232      * @return The purchase URL from the given String or {@code null} if it is invalid.
233      */
getPurchaseUrl(@ullable String purchaseUrl)234     @Nullable private static URL getPurchaseUrl(@Nullable String purchaseUrl) {
235         if (!URLUtil.isValidUrl(purchaseUrl)) {
236             return null;
237         }
238         if (URLUtil.isAssetUrl(purchaseUrl)
239                 && !purchaseUrl.equals(SlicePurchaseController.SLICE_PURCHASE_TEST_FILE)) {
240             return null;
241         }
242         URL url = null;
243         try {
244             url = new URL(purchaseUrl);
245             url.toURI();
246         } catch (MalformedURLException | URISyntaxException e) {
247             loge("Invalid purchase URL: " + purchaseUrl + ", " + e);
248         }
249         return url;
250     }
251 
isPendingIntentValid(@onNull Intent intent, @NonNull String extra)252     private static boolean isPendingIntentValid(@NonNull Intent intent, @NonNull String extra) {
253         String intentType = getPendingIntentType(extra);
254         PendingIntent pendingIntent = intent.getParcelableExtra(extra, PendingIntent.class);
255         if (pendingIntent == null) {
256             loge("isPendingIntentValid: " + intentType + " intent not found.");
257             return false;
258         }
259         String creatorPackage = pendingIntent.getCreatorPackage();
260         if (!creatorPackage.equals(TelephonyManager.PHONE_PROCESS_NAME)) {
261             String logStr = "isPendingIntentValid: " + intentType + " intent was created by "
262                     + creatorPackage + " instead of the phone process.";
263             loge(logStr);
264             AnomalyReporter.reportAnomaly(UUID.fromString(UUID_BAD_PENDING_INTENT), logStr);
265             return false;
266         }
267         if (!pendingIntent.isBroadcast()) {
268             loge("isPendingIntentValid: " + intentType + " intent is not a broadcast.");
269             return false;
270         }
271         return true;
272     }
273 
getPendingIntentType(@onNull String extra)274     @NonNull private static String getPendingIntentType(@NonNull String extra) {
275         switch (extra) {
276             case SlicePurchaseController.EXTRA_INTENT_CANCELED: return "canceled";
277             case SlicePurchaseController.EXTRA_INTENT_CARRIER_ERROR: return "carrier error";
278             case SlicePurchaseController.EXTRA_INTENT_REQUEST_FAILED: return "request failed";
279             case SlicePurchaseController.EXTRA_INTENT_NOT_DEFAULT_DATA_SUBSCRIPTION:
280                 return "not default data subscription";
281             case SlicePurchaseController.EXTRA_INTENT_NOTIFICATIONS_DISABLED:
282                 return "notifications disabled";
283             case SlicePurchaseController.EXTRA_INTENT_SUCCESS: return "success";
284             case SlicePurchaseController.EXTRA_INTENT_NOTIFICATION_SHOWN:
285                 return "notification shown";
286             default: {
287                 loge("Unknown pending intent extra: " + extra);
288                 return "unknown(" + extra + ")";
289             }
290         }
291     }
292 
293     @Override
onReceive(@onNull Context context, @NonNull Intent intent)294     public void onReceive(@NonNull Context context, @NonNull Intent intent) {
295         logd("onReceive intent: " + intent.getAction());
296         switch (intent.getAction()) {
297             case Intent.ACTION_LOCALE_CHANGED:
298                 onLocaleChanged(context);
299                 break;
300             case SlicePurchaseController.ACTION_START_SLICE_PURCHASE_APP:
301                 onDisplayPerformanceBoostNotification(context, intent, false);
302                 break;
303             case SlicePurchaseController.ACTION_SLICE_PURCHASE_APP_RESPONSE_TIMEOUT:
304                 onTimeout(context, intent);
305                 break;
306             case ACTION_NOTIFICATION_CANCELED:
307                 onUserCanceled(context, intent);
308                 break;
309             default:
310                 loge("Received unknown action: " + intent.getAction());
311         }
312     }
313 
onLocaleChanged(@onNull Context context)314     private void onLocaleChanged(@NonNull Context context) {
315         if (sIntents.isEmpty()) return;
316 
317         for (int capability : new int[]{TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY}) {
318             if (sIntents.get(capability) != null) {
319                 // Notification is active -- update notification for new locale
320                 context.getSystemService(NotificationManager.class).cancelAsUser(
321                         PERFORMANCE_BOOST_NOTIFICATION_TAG, capability, UserHandle.ALL);
322                 onDisplayPerformanceBoostNotification(context, sIntents.get(capability), true);
323             }
324         }
325     }
326 
onDisplayPerformanceBoostNotification(@onNull Context context, @NonNull Intent intent, boolean localeChanged)327     private void onDisplayPerformanceBoostNotification(@NonNull Context context,
328             @NonNull Intent intent, boolean localeChanged) {
329         if (!localeChanged && !isIntentValid(intent)) {
330             sendSlicePurchaseAppResponse(intent,
331                     SlicePurchaseController.EXTRA_INTENT_REQUEST_FAILED);
332             return;
333         }
334 
335         Resources res = getResources(context);
336         NotificationManager notificationManager =
337                 context.getSystemService(NotificationManager.class);
338         NotificationChannel channel = notificationManager.getNotificationChannel(
339                 PERFORMANCE_BOOST_NOTIFICATION_CHANNEL_ID);
340         if (channel == null) {
341             channel = new NotificationChannel(
342                     PERFORMANCE_BOOST_NOTIFICATION_CHANNEL_ID,
343                     res.getString(R.string.performance_boost_notification_channel),
344                     NotificationManager.IMPORTANCE_DEFAULT);
345             // CarrierDefaultApp notifications are unblockable by default.
346             // Make this channel blockable to allow users to disable notifications posted to this
347             // channel without affecting other notifications in this application.
348             channel.setBlockable(true);
349             context.getSystemService(NotificationManager.class).createNotificationChannel(channel);
350         } else if (localeChanged) {
351             // If the channel already exists but the locale has changed, update the channel name.
352             channel.setName(res.getString(R.string.performance_boost_notification_channel));
353         }
354 
355         boolean channelNotificationsDisabled =
356                 channel.getImportance() == NotificationManager.IMPORTANCE_NONE;
357         if (channelNotificationsDisabled || !notificationManager.areNotificationsEnabled()) {
358             // If notifications are disabled for the app or channel, fail the purchase request.
359             logd("Purchase request failed because notifications are disabled for the "
360                     + (channelNotificationsDisabled ? "channel." : "application."));
361             sendSlicePurchaseAppResponse(intent,
362                     SlicePurchaseController.EXTRA_INTENT_NOTIFICATIONS_DISABLED);
363             return;
364         }
365 
366         String carrier = intent.getStringExtra(SlicePurchaseController.EXTRA_CARRIER);
367         Notification notification =
368                 new Notification.Builder(context, PERFORMANCE_BOOST_NOTIFICATION_CHANNEL_ID)
369                         .setContentTitle(res.getString(
370                                 R.string.performance_boost_notification_title))
371                         .setContentText(String.format(res.getString(
372                                 R.string.performance_boost_notification_detail), carrier))
373                         .setSmallIcon(R.drawable.ic_performance_boost)
374                         .setContentIntent(createContentIntent(context, intent, 1))
375                         .setDeleteIntent(intent.getParcelableExtra(
376                                 SlicePurchaseController.EXTRA_INTENT_CANCELED, PendingIntent.class))
377                         // Add an action for the "Not now" button, which has the same behavior as
378                         // the user canceling or closing the notification.
379                         .addAction(new Notification.Action.Builder(
380                                 Icon.createWithResource(context, R.drawable.ic_performance_boost),
381                                 res.getString(
382                                         R.string.performance_boost_notification_button_not_now),
383                                 createCanceledIntent(context, intent)).build())
384                         // Add an action for the "Manage" button, which has the same behavior as
385                         // the user clicking on the notification.
386                         .addAction(new Notification.Action.Builder(
387                                 Icon.createWithResource(context, R.drawable.ic_performance_boost),
388                                 res.getString(
389                                         R.string.performance_boost_notification_button_manage),
390                                 createContentIntent(context, intent, 2)).build())
391                         .build();
392 
393         int capability = intent.getIntExtra(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY,
394                 SlicePurchaseController.PREMIUM_CAPABILITY_INVALID);
395         logd((localeChanged ? "Update" : "Display")
396                 + " the performance boost notification for capability "
397                 + TelephonyManager.convertPremiumCapabilityToString(capability));
398         context.getSystemService(NotificationManager.class).notifyAsUser(
399                 PERFORMANCE_BOOST_NOTIFICATION_TAG, capability, notification, UserHandle.ALL);
400         if (!localeChanged) {
401             sIntents.put(capability, intent);
402             sendSlicePurchaseAppResponse(intent,
403                     SlicePurchaseController.EXTRA_INTENT_NOTIFICATION_SHOWN);
404         }
405     }
406 
407     /**
408      * Get the {@link Resources} for the current locale.
409      *
410      * @param context The context to get the resources in.
411      *
412      * @return The resources in the current locale.
413      */
414     @VisibleForTesting
getResources(@onNull Context context)415     @NonNull public Resources getResources(@NonNull Context context) {
416         Resources resources = context.getResources();
417         Configuration config = resources.getConfiguration();
418         config.setLocale(getCurrentLocale());
419         return new Resources(resources.getAssets(), resources.getDisplayMetrics(), config);
420     }
421 
422     /**
423      * Get the current {@link Locale} from the system property {@code persist.sys.locale}.
424      *
425      * @return The user's default/preferred language.
426      */
427     @VisibleForTesting
getCurrentLocale()428     @NonNull public Locale getCurrentLocale() {
429         String languageTag = SystemProperties.get("persist.sys.locale");
430         if (TextUtils.isEmpty(languageTag)) {
431             return LocaleList.getAdjustedDefault().get(0);
432         }
433         return Locale.forLanguageTag(languageTag);
434     }
435 
436     /**
437      * Create the intent for when the user clicks on the "Manage" button on the performance boost
438      * notification or the notification itself. This will open {@link SlicePurchaseActivity}.
439      *
440      * @param context The Context to create the intent for.
441      * @param intent The source Intent used to launch the slice purchase application.
442      * @param requestCode The request code for the PendingIntent.
443      *
444      * @return The intent to start {@link SlicePurchaseActivity}.
445      */
446     @VisibleForTesting
createContentIntent(@onNull Context context, @NonNull Intent intent, int requestCode)447     @NonNull public PendingIntent createContentIntent(@NonNull Context context,
448             @NonNull Intent intent, int requestCode) {
449         Intent i = new Intent(context, SlicePurchaseActivity.class);
450         i.setComponent(ComponentName.unflattenFromString(
451                 "com.android.carrierdefaultapp/.SlicePurchaseActivity"));
452         i.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT
453                 | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
454         i.putExtras(intent);
455         return PendingIntent.getActivityAsUser(context, requestCode, i,
456                 PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE, null /* options */,
457                 UserHandle.CURRENT);
458     }
459 
460     /**
461      * Create the canceled intent for when the user clicks the "Not now" button on the performance
462      * boost notification. This will send {@link #ACTION_NOTIFICATION_CANCELED} and has the same
463      * behavior as if the user had canceled or removed the notification.
464      *
465      * @param context The Context to create the intent for.
466      * @param intent The source Intent used to launch the slice purchase application.
467      *
468      * @return The canceled intent.
469      */
470     @VisibleForTesting
createCanceledIntent(@onNull Context context, @NonNull Intent intent)471     @NonNull public PendingIntent createCanceledIntent(@NonNull Context context,
472             @NonNull Intent intent) {
473         Intent i = new Intent(ACTION_NOTIFICATION_CANCELED);
474         i.setComponent(ComponentName.unflattenFromString(
475                 "com.android.carrierdefaultapp/.SlicePurchaseBroadcastReceiver"));
476         i.putExtras(intent);
477         return PendingIntent.getBroadcast(context, 0, i,
478                 PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE);
479     }
480 
onTimeout(@onNull Context context, @NonNull Intent intent)481     private void onTimeout(@NonNull Context context, @NonNull Intent intent) {
482         int capability = intent.getIntExtra(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY,
483                 SlicePurchaseController.PREMIUM_CAPABILITY_INVALID);
484         logd("Purchase capability " + TelephonyManager.convertPremiumCapabilityToString(capability)
485                 + " timed out.");
486         if (sIntents.get(capability) != null) {
487             // Notification is still active -- cancel pending notification
488             logd("Closing performance boost notification since the user did not respond in time.");
489             cancelNotification(context, capability);
490         } else {
491             // SlicePurchaseActivity is still active -- ignore timer
492             logd("Ignoring timeout since the SlicePurchaseActivity is still active.");
493         }
494     }
495 
onUserCanceled(@onNull Context context, @NonNull Intent intent)496     private void onUserCanceled(@NonNull Context context, @NonNull Intent intent) {
497         if (!isIntentValid(intent)) {
498             loge("Ignoring onUserCanceled called with invalid intent.");
499             return;
500         }
501         int capability = intent.getIntExtra(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY,
502                 SlicePurchaseController.PREMIUM_CAPABILITY_INVALID);
503         logd("onUserCanceled: " + TelephonyManager.convertPremiumCapabilityToString(capability));
504         cancelNotification(context, capability);
505         sendSlicePurchaseAppResponse(intent, SlicePurchaseController.EXTRA_INTENT_CANCELED);
506     }
507 
logd(String s)508     private static void logd(String s) {
509         Log.d(TAG, s);
510     }
511 
loge(String s)512     private static void loge(String s) {
513         Log.e(TAG, s);
514     }
515 }
516