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