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