• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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 
17 package com.android.networkstack.tethering;
18 
19 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
20 import static android.text.TextUtils.isEmpty;
21 
22 import android.app.Notification;
23 import android.app.Notification.Action;
24 import android.app.NotificationChannel;
25 import android.app.NotificationManager;
26 import android.app.PendingIntent;
27 import android.content.ComponentName;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.pm.PackageManager;
31 import android.content.res.Configuration;
32 import android.content.res.Resources;
33 import android.net.NetworkCapabilities;
34 import android.os.Handler;
35 import android.os.Looper;
36 import android.os.Message;
37 import android.os.UserHandle;
38 import android.provider.Settings;
39 import android.telephony.SubscriptionManager;
40 import android.telephony.TelephonyManager;
41 import android.util.SparseArray;
42 
43 import androidx.annotation.DrawableRes;
44 import androidx.annotation.IntDef;
45 import androidx.annotation.IntRange;
46 import androidx.annotation.NonNull;
47 import androidx.annotation.Nullable;
48 
49 import com.android.internal.annotations.VisibleForTesting;
50 
51 import java.lang.annotation.Retention;
52 import java.lang.annotation.RetentionPolicy;
53 
54 /**
55  * A class to display tethering-related notifications.
56  *
57  * <p>This class is not thread safe, it is intended to be used only from the tethering handler
58  * thread. However the constructor is an exception, as it is called on another thread ;
59  * therefore for thread safety all members of this class MUST either be final or initialized
60  * to their default value (0, false or null).
61  *
62  * @hide
63  */
64 public class TetheringNotificationUpdater {
65     private static final String TAG = TetheringNotificationUpdater.class.getSimpleName();
66     private static final String CHANNEL_ID = "TETHERING_STATUS";
67     private static final String WIFI_DOWNSTREAM = "WIFI";
68     private static final String USB_DOWNSTREAM = "USB";
69     private static final String BLUETOOTH_DOWNSTREAM = "BT";
70     @VisibleForTesting
71     static final String ACTION_DISABLE_TETHERING =
72             "com.android.server.connectivity.tethering.DISABLE_TETHERING";
73     private static final boolean NOTIFY_DONE = true;
74     private static final boolean NO_NOTIFY = false;
75     @VisibleForTesting
76     static final int EVENT_SHOW_NO_UPSTREAM = 1;
77     // Id to update and cancel restricted notification. Must be unique within the tethering app.
78     @VisibleForTesting
79     static final int RESTRICTED_NOTIFICATION_ID = 1001;
80     // Id to update and cancel no upstream notification. Must be unique within the tethering app.
81     @VisibleForTesting
82     static final int NO_UPSTREAM_NOTIFICATION_ID = 1002;
83     // Id to update and cancel roaming notification. Must be unique within the tethering app.
84     @VisibleForTesting
85     static final int ROAMING_NOTIFICATION_ID = 1003;
86     @VisibleForTesting
87     static final int NO_ICON_ID = 0;
88     static final int DOWNSTREAM_NONE = 0;
89     // Refer to TelephonyManager#getSimCarrierId for more details about carrier id.
90     @VisibleForTesting
91     static final int VERIZON_CARRIER_ID = 1839;
92     private final Context mContext;
93     private final NotificationManager mNotificationManager;
94     private final NotificationChannel mChannel;
95     private final Handler mHandler;
96 
97     // WARNING : the constructor is called on a different thread. Thread safety therefore
98     // relies on these values being initialized to 0, false or null, and not any other value. If you
99     // need to change this, you will need to change the thread where the constructor is invoked, or
100     // to introduce synchronization.
101     // Downstream type is one of ConnectivityManager.TETHERING_* constants, 0 1 or 2.
102     // This value has to be made 1 2 and 4, and OR'd with the others.
103     private int mDownstreamTypesMask = DOWNSTREAM_NONE;
104     private boolean mNoUpstream = false;
105     private boolean mRoaming = false;
106 
107     // WARNING : this value is not able to being initialized to 0 and must have volatile because
108     // telephony service is not guaranteed that is up before tethering service starts. If telephony
109     // is up later than tethering, TetheringNotificationUpdater will use incorrect and valid
110     // subscription id(0) to query resources. Therefore, initialized subscription id must be
111     // INVALID_SUBSCRIPTION_ID.
112     private volatile int mActiveDataSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
113 
114     @Retention(RetentionPolicy.SOURCE)
115     @IntDef(value = {
116             RESTRICTED_NOTIFICATION_ID,
117             NO_UPSTREAM_NOTIFICATION_ID,
118             ROAMING_NOTIFICATION_ID
119     })
120     @interface NotificationId {}
121 
122     private static final class MccMncOverrideInfo {
123         public final String visitedMccMnc;
124         public final int homeMcc;
125         public final int homeMnc;
MccMncOverrideInfo(String visitedMccMnc, int mcc, int mnc)126         MccMncOverrideInfo(String visitedMccMnc, int mcc, int mnc) {
127             this.visitedMccMnc = visitedMccMnc;
128             this.homeMcc = mcc;
129             this.homeMnc = mnc;
130         }
131     }
132 
133     private static final SparseArray<MccMncOverrideInfo> sCarrierIdToMccMnc = new SparseArray<>();
134 
135     static {
sCarrierIdToMccMnc.put(VERIZON_CARRIER_ID, new MccMncOverrideInfo("20404", 311, 480))136         sCarrierIdToMccMnc.put(VERIZON_CARRIER_ID, new MccMncOverrideInfo("20404", 311, 480));
137     }
138 
TetheringNotificationUpdater(@onNull final Context context, @NonNull final Looper looper)139     public TetheringNotificationUpdater(@NonNull final Context context,
140             @NonNull final Looper looper) {
141         mContext = context;
142         mNotificationManager = (NotificationManager) context.createContextAsUser(UserHandle.ALL, 0)
143                 .getSystemService(Context.NOTIFICATION_SERVICE);
144         mChannel = new NotificationChannel(
145                 CHANNEL_ID,
146                 context.getResources().getString(R.string.notification_channel_tethering_status),
147                 NotificationManager.IMPORTANCE_LOW);
148         mNotificationManager.createNotificationChannel(mChannel);
149         mHandler = new NotificationHandler(looper);
150     }
151 
152     private class NotificationHandler extends Handler {
NotificationHandler(Looper looper)153         NotificationHandler(Looper looper) {
154             super(looper);
155         }
156 
157         @Override
handleMessage(Message msg)158         public void handleMessage(Message msg) {
159             switch(msg.what) {
160                 case EVENT_SHOW_NO_UPSTREAM:
161                     notifyTetheringNoUpstream();
162                     break;
163             }
164         }
165     }
166 
167     /** Called when downstream has changed */
onDownstreamChanged(@ntRangefrom = 0, to = 7) final int downstreamTypesMask)168     public void onDownstreamChanged(@IntRange(from = 0, to = 7) final int downstreamTypesMask) {
169         updateActiveNotifications(
170                 mActiveDataSubId, downstreamTypesMask, mNoUpstream, mRoaming);
171     }
172 
173     /** Called when active data subscription id changed */
onActiveDataSubscriptionIdChanged(final int subId)174     public void onActiveDataSubscriptionIdChanged(final int subId) {
175         updateActiveNotifications(subId, mDownstreamTypesMask, mNoUpstream, mRoaming);
176     }
177 
178     /** Called when upstream network capabilities changed */
onUpstreamCapabilitiesChanged(@ullable final NetworkCapabilities capabilities)179     public void onUpstreamCapabilitiesChanged(@Nullable final NetworkCapabilities capabilities) {
180         final boolean isNoUpstream = (capabilities == null);
181         final boolean isRoaming = capabilities != null
182                 && !capabilities.hasCapability(NET_CAPABILITY_NOT_ROAMING);
183         updateActiveNotifications(
184                 mActiveDataSubId, mDownstreamTypesMask, isNoUpstream, isRoaming);
185     }
186 
187     @NonNull
188     @VisibleForTesting
getHandler()189     final Handler getHandler() {
190         return mHandler;
191     }
192 
193     @NonNull
194     @VisibleForTesting
getResourcesForSubId(@onNull final Context context, final int subId)195     Resources getResourcesForSubId(@NonNull final Context context, final int subId) {
196         final Resources res = SubscriptionManager.getResourcesForSubId(context, subId);
197         final TelephonyManager tm =
198                 ((TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE))
199                         .createForSubscriptionId(mActiveDataSubId);
200         final int carrierId = tm.getSimCarrierId();
201         final String mccmnc = tm.getSimOperator();
202         final MccMncOverrideInfo overrideInfo = sCarrierIdToMccMnc.get(carrierId);
203         if (overrideInfo != null && overrideInfo.visitedMccMnc.equals(mccmnc)) {
204             // Re-configure MCC/MNC value to specific carrier to get right resources.
205             final Configuration config = res.getConfiguration();
206             config.mcc = overrideInfo.homeMcc;
207             config.mnc = overrideInfo.homeMnc;
208             return context.createConfigurationContext(config).getResources();
209         }
210         return res;
211     }
212 
updateActiveNotifications(final int subId, final int downstreamTypes, final boolean noUpstream, final boolean isRoaming)213     private void updateActiveNotifications(final int subId, final int downstreamTypes,
214             final boolean noUpstream, final boolean isRoaming) {
215         final boolean tetheringActiveChanged =
216                 (downstreamTypes == DOWNSTREAM_NONE) != (mDownstreamTypesMask == DOWNSTREAM_NONE);
217         final boolean subIdChanged = subId != mActiveDataSubId;
218         final boolean upstreamChanged = noUpstream != mNoUpstream;
219         final boolean roamingChanged = isRoaming != mRoaming;
220         final boolean updateAll = tetheringActiveChanged || subIdChanged;
221         mActiveDataSubId = subId;
222         mDownstreamTypesMask = downstreamTypes;
223         mNoUpstream = noUpstream;
224         mRoaming = isRoaming;
225 
226         if (updateAll || upstreamChanged) updateNoUpstreamNotification();
227         if (updateAll || roamingChanged) updateRoamingNotification();
228     }
229 
updateNoUpstreamNotification()230     private void updateNoUpstreamNotification() {
231         final boolean tetheringInactive = mDownstreamTypesMask == DOWNSTREAM_NONE;
232 
233         if (tetheringInactive || !mNoUpstream || setupNoUpstreamNotification() == NO_NOTIFY) {
234             clearNotification(NO_UPSTREAM_NOTIFICATION_ID);
235             mHandler.removeMessages(EVENT_SHOW_NO_UPSTREAM);
236         }
237     }
238 
updateRoamingNotification()239     private void updateRoamingNotification() {
240         final boolean tetheringInactive = mDownstreamTypesMask == DOWNSTREAM_NONE;
241 
242         if (tetheringInactive || !mRoaming || setupRoamingNotification() == NO_NOTIFY) {
243             clearNotification(ROAMING_NOTIFICATION_ID);
244         }
245     }
246 
247     @VisibleForTesting
tetheringRestrictionLifted()248     void tetheringRestrictionLifted() {
249         clearNotification(RESTRICTED_NOTIFICATION_ID);
250     }
251 
clearNotification(@otificationId final int id)252     private void clearNotification(@NotificationId final int id) {
253         mNotificationManager.cancel(null /* tag */, id);
254     }
255 
256     @VisibleForTesting
getSettingsPackageName(@onNull final PackageManager pm)257     static String getSettingsPackageName(@NonNull final PackageManager pm) {
258         final Intent settingsIntent = new Intent(Settings.ACTION_SETTINGS);
259         final ComponentName settingsComponent = settingsIntent.resolveActivity(pm);
260         return settingsComponent != null
261                 ? settingsComponent.getPackageName() : "com.android.settings";
262     }
263 
264     @VisibleForTesting
notifyTetheringDisabledByRestriction()265     void notifyTetheringDisabledByRestriction() {
266         final Resources res = getResourcesForSubId(mContext, mActiveDataSubId);
267         final String title = res.getString(R.string.disable_tether_notification_title);
268         final String message = res.getString(R.string.disable_tether_notification_message);
269         if (isEmpty(title) || isEmpty(message)) return;
270 
271         final PendingIntent pi = PendingIntent.getActivity(
272                 mContext.createContextAsUser(UserHandle.CURRENT, 0 /* flags */),
273                 0 /* requestCode */,
274                 new Intent(Settings.ACTION_TETHER_SETTINGS)
275                         .setPackage(getSettingsPackageName(mContext.getPackageManager()))
276                         .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
277                 PendingIntent.FLAG_IMMUTABLE,
278                 null /* options */);
279 
280         showNotification(R.drawable.stat_sys_tether_general, title, message,
281                 RESTRICTED_NOTIFICATION_ID, false /* ongoing */, pi, new Action[0]);
282     }
283 
notifyTetheringNoUpstream()284     private void notifyTetheringNoUpstream() {
285         final Resources res = getResourcesForSubId(mContext, mActiveDataSubId);
286         final String title = res.getString(R.string.no_upstream_notification_title);
287         final String message = res.getString(R.string.no_upstream_notification_message);
288         final String disableButton =
289                 res.getString(R.string.no_upstream_notification_disable_button);
290         if (isEmpty(title) || isEmpty(message) || isEmpty(disableButton)) return;
291 
292         final Intent intent = new Intent(ACTION_DISABLE_TETHERING);
293         intent.setPackage(mContext.getPackageName());
294         final PendingIntent pi = PendingIntent.getBroadcast(
295                 mContext.createContextAsUser(UserHandle.CURRENT, 0 /* flags */),
296                 0 /* requestCode */,
297                 intent,
298                 PendingIntent.FLAG_IMMUTABLE);
299         final Action action = new Action.Builder(NO_ICON_ID, disableButton, pi).build();
300 
301         showNotification(R.drawable.stat_sys_tether_general, title, message,
302                 NO_UPSTREAM_NOTIFICATION_ID, true /* ongoing */, null /* pendingIntent */, action);
303     }
304 
setupRoamingNotification()305     private boolean setupRoamingNotification() {
306         final Resources res = getResourcesForSubId(mContext, mActiveDataSubId);
307         final boolean upstreamRoamingNotification =
308                 res.getBoolean(R.bool.config_upstream_roaming_notification);
309 
310         if (!upstreamRoamingNotification) return NO_NOTIFY;
311 
312         final String title = res.getString(R.string.upstream_roaming_notification_title);
313         final String message = res.getString(R.string.upstream_roaming_notification_message);
314         if (isEmpty(title) || isEmpty(message)) return NO_NOTIFY;
315 
316         final PendingIntent pi = PendingIntent.getActivity(
317                 mContext.createContextAsUser(UserHandle.CURRENT, 0 /* flags */),
318                 0 /* requestCode */,
319                 new Intent(Settings.ACTION_TETHER_SETTINGS)
320                         .setPackage(getSettingsPackageName(mContext.getPackageManager()))
321                         .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
322                 PendingIntent.FLAG_IMMUTABLE,
323                 null /* options */);
324 
325         showNotification(R.drawable.stat_sys_tether_general, title, message,
326                 ROAMING_NOTIFICATION_ID, true /* ongoing */, pi, new Action[0]);
327         return NOTIFY_DONE;
328     }
329 
setupNoUpstreamNotification()330     private boolean setupNoUpstreamNotification() {
331         final Resources res = getResourcesForSubId(mContext, mActiveDataSubId);
332         final int delayToShowUpstreamNotification =
333                 res.getInteger(R.integer.delay_to_show_no_upstream_after_no_backhaul);
334 
335         if (delayToShowUpstreamNotification < 0) return NO_NOTIFY;
336 
337         mHandler.sendMessageDelayed(mHandler.obtainMessage(EVENT_SHOW_NO_UPSTREAM),
338                 delayToShowUpstreamNotification);
339         return NOTIFY_DONE;
340     }
341 
showNotification(@rawableRes final int iconId, @NonNull final String title, @NonNull final String message, @NotificationId final int id, final boolean ongoing, @Nullable PendingIntent pi, @NonNull final Action... actions)342     private void showNotification(@DrawableRes final int iconId, @NonNull final String title,
343             @NonNull final String message, @NotificationId final int id, final boolean ongoing,
344             @Nullable PendingIntent pi, @NonNull final Action... actions) {
345         final Notification notification =
346                 new Notification.Builder(mContext, mChannel.getId())
347                         .setSmallIcon(iconId)
348                         .setContentTitle(title)
349                         .setContentText(message)
350                         .setOngoing(ongoing)
351                         .setColor(mContext.getColor(
352                                 android.R.color.system_notification_accent_color))
353                         .setVisibility(Notification.VISIBILITY_PUBLIC)
354                         .setCategory(Notification.CATEGORY_STATUS)
355                         .setContentIntent(pi)
356                         .setActions(actions)
357                         .build();
358 
359         mNotificationManager.notify(null /* tag */, id, notification);
360     }
361 }
362