blob: d03deda37fdfe47851dc08347a5a39a1d15625ce [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.networkstack.tethering;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
import static android.text.TextUtils.isEmpty;
import android.app.Notification;
import android.app.Notification.Action;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.net.NetworkCapabilities;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.UserHandle;
import android.provider.Settings;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.util.SparseArray;
import androidx.annotation.DrawableRes;
import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* A class to display tethering-related notifications.
*
* <p>This class is not thread safe, it is intended to be used only from the tethering handler
* thread. However the constructor is an exception, as it is called on another thread ;
* therefore for thread safety all members of this class MUST either be final or initialized
* to their default value (0, false or null).
*
* @hide
*/
public class TetheringNotificationUpdater {
private static final String TAG = TetheringNotificationUpdater.class.getSimpleName();
private static final String CHANNEL_ID = "TETHERING_STATUS";
private static final String WIFI_DOWNSTREAM = "WIFI";
private static final String USB_DOWNSTREAM = "USB";
private static final String BLUETOOTH_DOWNSTREAM = "BT";
@VisibleForTesting
static final String ACTION_DISABLE_TETHERING =
"com.android.server.connectivity.tethering.DISABLE_TETHERING";
private static final boolean NOTIFY_DONE = true;
private static final boolean NO_NOTIFY = false;
@VisibleForTesting
static final int EVENT_SHOW_NO_UPSTREAM = 1;
// Id to update and cancel restricted notification. Must be unique within the tethering app.
@VisibleForTesting
static final int RESTRICTED_NOTIFICATION_ID = 1001;
// Id to update and cancel no upstream notification. Must be unique within the tethering app.
@VisibleForTesting
static final int NO_UPSTREAM_NOTIFICATION_ID = 1002;
// Id to update and cancel roaming notification. Must be unique within the tethering app.
@VisibleForTesting
static final int ROAMING_NOTIFICATION_ID = 1003;
@VisibleForTesting
static final int NO_ICON_ID = 0;
@VisibleForTesting
static final int DOWNSTREAM_NONE = 0;
// Refer to TelephonyManager#getSimCarrierId for more details about carrier id.
@VisibleForTesting
static final int VERIZON_CARRIER_ID = 1839;
private final Context mContext;
private final NotificationManager mNotificationManager;
private final NotificationChannel mChannel;
private final Handler mHandler;
// WARNING : the constructor is called on a different thread. Thread safety therefore
// relies on these values being initialized to 0, false or null, and not any other value. If you
// need to change this, you will need to change the thread where the constructor is invoked, or
// to introduce synchronization.
// Downstream type is one of ConnectivityManager.TETHERING_* constants, 0 1 or 2.
// This value has to be made 1 2 and 4, and OR'd with the others.
private int mDownstreamTypesMask = DOWNSTREAM_NONE;
private boolean mNoUpstream = false;
private boolean mRoaming = false;
// WARNING : this value is not able to being initialized to 0 and must have volatile because
// telephony service is not guaranteed that is up before tethering service starts. If telephony
// is up later than tethering, TetheringNotificationUpdater will use incorrect and valid
// subscription id(0) to query resources. Therefore, initialized subscription id must be
// INVALID_SUBSCRIPTION_ID.
private volatile int mActiveDataSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {
RESTRICTED_NOTIFICATION_ID,
NO_UPSTREAM_NOTIFICATION_ID,
ROAMING_NOTIFICATION_ID
})
@interface NotificationId {}
private static final class MccMncOverrideInfo {
public final String visitedMccMnc;
public final int homeMcc;
public final int homeMnc;
MccMncOverrideInfo(String visitedMccMnc, int mcc, int mnc) {
this.visitedMccMnc = visitedMccMnc;
this.homeMcc = mcc;
this.homeMnc = mnc;
}
}
private static final SparseArray<MccMncOverrideInfo> sCarrierIdToMccMnc = new SparseArray<>();
static {
sCarrierIdToMccMnc.put(VERIZON_CARRIER_ID, new MccMncOverrideInfo("20404", 311, 480));
}
public TetheringNotificationUpdater(@NonNull final Context context,
@NonNull final Looper looper) {
mContext = context;
mNotificationManager = (NotificationManager) context.createContextAsUser(UserHandle.ALL, 0)
.getSystemService(Context.NOTIFICATION_SERVICE);
mChannel = new NotificationChannel(
CHANNEL_ID,
context.getResources().getString(R.string.notification_channel_tethering_status),
NotificationManager.IMPORTANCE_LOW);
mNotificationManager.createNotificationChannel(mChannel);
mHandler = new NotificationHandler(looper);
}
private class NotificationHandler extends Handler {
NotificationHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch(msg.what) {
case EVENT_SHOW_NO_UPSTREAM:
notifyTetheringNoUpstream();
break;
}
}
}
/** Called when downstream has changed */
public void onDownstreamChanged(@IntRange(from = 0, to = 7) final int downstreamTypesMask) {
updateActiveNotifications(
mActiveDataSubId, downstreamTypesMask, mNoUpstream, mRoaming);
}
/** Called when active data subscription id changed */
public void onActiveDataSubscriptionIdChanged(final int subId) {
updateActiveNotifications(subId, mDownstreamTypesMask, mNoUpstream, mRoaming);
}
/** Called when upstream network capabilities changed */
public void onUpstreamCapabilitiesChanged(@Nullable final NetworkCapabilities capabilities) {
final boolean isNoUpstream = (capabilities == null);
final boolean isRoaming = capabilities != null
&& !capabilities.hasCapability(NET_CAPABILITY_NOT_ROAMING);
updateActiveNotifications(
mActiveDataSubId, mDownstreamTypesMask, isNoUpstream, isRoaming);
}
@NonNull
@VisibleForTesting
final Handler getHandler() {
return mHandler;
}
@NonNull
@VisibleForTesting
Resources getResourcesForSubId(@NonNull final Context context, final int subId) {
final Resources res = SubscriptionManager.getResourcesForSubId(context, subId);
final TelephonyManager tm =
((TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE))
.createForSubscriptionId(mActiveDataSubId);
final int carrierId = tm.getSimCarrierId();
final String mccmnc = tm.getSimOperator();
final MccMncOverrideInfo overrideInfo = sCarrierIdToMccMnc.get(carrierId);
if (overrideInfo != null && overrideInfo.visitedMccMnc.equals(mccmnc)) {
// Re-configure MCC/MNC value to specific carrier to get right resources.
final Configuration config = res.getConfiguration();
config.mcc = overrideInfo.homeMcc;
config.mnc = overrideInfo.homeMnc;
return context.createConfigurationContext(config).getResources();
}
return res;
}
private void updateActiveNotifications(final int subId, final int downstreamTypes,
final boolean noUpstream, final boolean isRoaming) {
final boolean tetheringActiveChanged =
(downstreamTypes == DOWNSTREAM_NONE) != (mDownstreamTypesMask == DOWNSTREAM_NONE);
final boolean subIdChanged = subId != mActiveDataSubId;
final boolean upstreamChanged = noUpstream != mNoUpstream;
final boolean roamingChanged = isRoaming != mRoaming;
final boolean updateAll = tetheringActiveChanged || subIdChanged;
mActiveDataSubId = subId;
mDownstreamTypesMask = downstreamTypes;
mNoUpstream = noUpstream;
mRoaming = isRoaming;
if (updateAll || upstreamChanged) updateNoUpstreamNotification();
if (updateAll || roamingChanged) updateRoamingNotification();
}
private void updateNoUpstreamNotification() {
final boolean tetheringInactive = mDownstreamTypesMask == DOWNSTREAM_NONE;
if (tetheringInactive || !mNoUpstream || setupNoUpstreamNotification() == NO_NOTIFY) {
clearNotification(NO_UPSTREAM_NOTIFICATION_ID);
mHandler.removeMessages(EVENT_SHOW_NO_UPSTREAM);
}
}
private void updateRoamingNotification() {
final boolean tetheringInactive = mDownstreamTypesMask == DOWNSTREAM_NONE;
if (tetheringInactive || !mRoaming || setupRoamingNotification() == NO_NOTIFY) {
clearNotification(ROAMING_NOTIFICATION_ID);
}
}
@VisibleForTesting
void tetheringRestrictionLifted() {
clearNotification(RESTRICTED_NOTIFICATION_ID);
}
private void clearNotification(@NotificationId final int id) {
mNotificationManager.cancel(null /* tag */, id);
}
@VisibleForTesting
void notifyTetheringDisabledByRestriction() {
final Resources res = getResourcesForSubId(mContext, mActiveDataSubId);
final String title = res.getString(R.string.disable_tether_notification_title);
final String message = res.getString(R.string.disable_tether_notification_message);
if (isEmpty(title) || isEmpty(message)) return;
final PendingIntent pi = PendingIntent.getActivity(
mContext.createContextAsUser(UserHandle.CURRENT, 0 /* flags */),
0 /* requestCode */,
new Intent(Settings.ACTION_TETHER_SETTINGS),
Intent.FLAG_ACTIVITY_NEW_TASK,
null /* options */);
showNotification(R.drawable.stat_sys_tether_general, title, message,
RESTRICTED_NOTIFICATION_ID, false /* ongoing */, pi, new Action[0]);
}
private void notifyTetheringNoUpstream() {
final Resources res = getResourcesForSubId(mContext, mActiveDataSubId);
final String title = res.getString(R.string.no_upstream_notification_title);
final String message = res.getString(R.string.no_upstream_notification_message);
final String disableButton =
res.getString(R.string.no_upstream_notification_disable_button);
if (isEmpty(title) || isEmpty(message) || isEmpty(disableButton)) return;
final Intent intent = new Intent(ACTION_DISABLE_TETHERING);
intent.setPackage(mContext.getPackageName());
final PendingIntent pi = PendingIntent.getBroadcast(
mContext.createContextAsUser(UserHandle.CURRENT, 0 /* flags */),
0 /* requestCode */,
intent,
0 /* flags */);
final Action action = new Action.Builder(NO_ICON_ID, disableButton, pi).build();
showNotification(R.drawable.stat_sys_tether_general, title, message,
NO_UPSTREAM_NOTIFICATION_ID, true /* ongoing */, null /* pendingIntent */, action);
}
private boolean setupRoamingNotification() {
final Resources res = getResourcesForSubId(mContext, mActiveDataSubId);
final boolean upstreamRoamingNotification =
res.getBoolean(R.bool.config_upstream_roaming_notification);
if (!upstreamRoamingNotification) return NO_NOTIFY;
final String title = res.getString(R.string.upstream_roaming_notification_title);
final String message = res.getString(R.string.upstream_roaming_notification_message);
if (isEmpty(title) || isEmpty(message)) return NO_NOTIFY;
final PendingIntent pi = PendingIntent.getActivity(
mContext.createContextAsUser(UserHandle.CURRENT, 0 /* flags */),
0 /* requestCode */,
new Intent(Settings.ACTION_TETHER_SETTINGS),
Intent.FLAG_ACTIVITY_NEW_TASK,
null /* options */);
showNotification(R.drawable.stat_sys_tether_general, title, message,
ROAMING_NOTIFICATION_ID, true /* ongoing */, pi, new Action[0]);
return NOTIFY_DONE;
}
private boolean setupNoUpstreamNotification() {
final Resources res = getResourcesForSubId(mContext, mActiveDataSubId);
final int delayToShowUpstreamNotification =
res.getInteger(R.integer.delay_to_show_no_upstream_after_no_backhaul);
if (delayToShowUpstreamNotification < 0) return NO_NOTIFY;
mHandler.sendMessageDelayed(mHandler.obtainMessage(EVENT_SHOW_NO_UPSTREAM),
delayToShowUpstreamNotification);
return NOTIFY_DONE;
}
private void showNotification(@DrawableRes 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) {
final Notification notification =
new Notification.Builder(mContext, mChannel.getId())
.setSmallIcon(iconId)
.setContentTitle(title)
.setContentText(message)
.setOngoing(ongoing)
.setColor(mContext.getColor(
android.R.color.system_notification_accent_color))
.setVisibility(Notification.VISIBILITY_PUBLIC)
.setCategory(Notification.CATEGORY_STATUS)
.setContentIntent(pi)
.setActions(actions)
.build();
mNotificationManager.notify(null /* tag */, id, notification);
}
}