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