// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.media;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.util.SparseIntArray;
import androidx.annotation.Nullable;
import org.chromium.base.ContextUtils;
import org.chromium.base.shared_preferences.SharedPreferencesManager;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.app.tabmodel.TabWindowManagerSingleton;
import org.chromium.chrome.browser.notifications.NotificationUmaTracker;
import org.chromium.chrome.browser.notifications.NotificationWrapperBuilderFactory;
import org.chromium.chrome.browser.notifications.channels.ChromeChannelDefinitions;
import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.ui.MediaCaptureOverlayController;
import org.chromium.components.browser_ui.notifications.BaseNotificationManagerProxy;
import org.chromium.components.browser_ui.notifications.BaseNotificationManagerProxyFactory;
import org.chromium.components.browser_ui.notifications.NotificationMetadata;
import org.chromium.components.browser_ui.notifications.NotificationWrapper;
import org.chromium.components.browser_ui.notifications.NotificationWrapperBuilder;
import org.chromium.components.browser_ui.notifications.PendingIntentProvider;
import org.chromium.components.webrtc.MediaCaptureNotificationUtil;
import org.chromium.components.webrtc.MediaCaptureNotificationUtil.MediaType;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.url.GURL;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
/** Service that creates/destroys the WebRTC notification when media capture starts/stops. */
public class MediaCaptureNotificationServiceImpl extends MediaCaptureNotificationService.Impl {
private static final String ACTION_MEDIA_CAPTURE_UPDATE =
"org.chromium.chrome.browser.media.SCREEN_CAPTURE_UPDATE";
private static final String ACTION_SCREEN_CAPTURE_STOP =
"org.chromium.chrome.browser.media.SCREEN_CAPTURE_STOP";
private static final String NOTIFICATION_NAMESPACE = "MediaCaptureNotificationService";
private static final String NOTIFICATION_ID_EXTRA = "NotificationId";
private static final String NOTIFICATION_MEDIA_IS_INCOGNITO = "NotificationIsIncognito";
private static final String NOTIFICATION_MEDIA_TYPE_EXTRA = "NotificationMediaType";
private static final String NOTIFICATION_MEDIA_URL_EXTRA = "NotificationMediaUrl";
private BaseNotificationManagerProxy mNotificationManager;
private SharedPreferencesManager mSharedPreferences;
private final SparseIntArray mNotifications = new SparseIntArray();
@Override
public void onCreate() {
mNotificationManager =
BaseNotificationManagerProxyFactory.create(ContextUtils.getApplicationContext());
mSharedPreferences = ChromeSharedPreferences.getInstance();
super.onCreate();
}
/**
* @param notificationId Unique id of the notification.
* @param mediaType Media type of the notification.
* @return Whether the notification has already been created for provided notification id and
* mediaType.
*/
private boolean doesNotificationNeedUpdate(int notificationId, @MediaType int mediaType) {
return mNotifications.get(notificationId) != mediaType;
}
/**
* @param notificationId Unique id of the notification.
* @return Whether the notification has already been created for the provided notification id.
*/
private boolean doesNotificationExist(int notificationId) {
return mNotifications.indexOfKey(notificationId) >= 0;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent == null || intent.getExtras() == null) {
cancelPreviousWebRtcNotifications();
getService().stopSelf();
} else {
String action = intent.getAction();
int notificationId = intent.getIntExtra(NOTIFICATION_ID_EXTRA, Tab.INVALID_TAB_ID);
int mediaType = intent.getIntExtra(NOTIFICATION_MEDIA_TYPE_EXTRA, MediaType.NO_MEDIA);
String url = intent.getStringExtra(NOTIFICATION_MEDIA_URL_EXTRA);
boolean isIncognito = intent.getBooleanExtra(NOTIFICATION_MEDIA_IS_INCOGNITO, false);
if (ACTION_MEDIA_CAPTURE_UPDATE.equals(action)) {
updateNotification(notificationId, mediaType, url, isIncognito, startId);
} else if (ACTION_SCREEN_CAPTURE_STOP.equals(action)) {
// Notify native to stop screen capture when the STOP button in notification
// is clicked.
final Tab tab = TabWindowManagerSingleton.getInstance().getTabById(notificationId);
if (tab != null) {
MediaCaptureDevicesDispatcherAndroid.notifyStopped(tab.getWebContents());
}
}
}
return super.onStartCommand(intent, flags, startId);
}
/**
* Cancel all previously existing notifications. Essential while doing a clean start (may be
* after a browser crash which caused old notifications to exist).
*/
private void cancelPreviousWebRtcNotifications() {
Set<String> notificationIds =
mSharedPreferences.readStringSet(
ChromePreferenceKeys.MEDIA_WEBRTC_NOTIFICATION_IDS, null);
if (notificationIds == null) return;
Iterator<String> iterator = notificationIds.iterator();
while (iterator.hasNext()) {
mNotificationManager.cancel(NOTIFICATION_NAMESPACE, Integer.parseInt(iterator.next()));
}
mSharedPreferences.removeKey(ChromePreferenceKeys.MEDIA_WEBRTC_NOTIFICATION_IDS);
}
/**
* Updates the extisting notification or creates one if none exist for the provided
* notificationId and mediaType.
* @param notificationId Unique id of the notification.
* @param mediaType Media type of the notification.
* @param url Url of the current webrtc call.
* @param startId Id for the service start request
*/
private void updateNotification(
int notificationId,
@MediaType int mediaType,
String url,
boolean isIncognito,
int startId) {
if (doesNotificationExist(notificationId)
&& !doesNotificationNeedUpdate(notificationId, mediaType)) {
return;
}
destroyNotification(notificationId);
if (mediaType != MediaType.NO_MEDIA) {
createNotification(notificationId, mediaType, url, isIncognito);
}
if (mNotifications.size() == 0) getService().stopSelf(startId);
}
/**
* Destroys the notification for the id notificationId.
* @param notificationId Unique id of the notification.
*/
private void destroyNotification(int notificationId) {
if (doesNotificationExist(notificationId)) {
if (mNotifications.get(notificationId) == MediaType.SCREEN_CAPTURE) {
final Tab tab = TabWindowManagerSingleton.getInstance().getTabById(notificationId);
if (tab != null) {
WindowAndroid window = tab.getWebContents().getTopLevelNativeWindow();
MediaCaptureOverlayController overlayController =
MediaCaptureOverlayController.from(window);
if (overlayController != null) {
overlayController.stopCapture(tab);
}
}
}
mNotificationManager.cancel(NOTIFICATION_NAMESPACE, notificationId);
mNotifications.delete(notificationId);
updateSharedPreferencesEntry(notificationId, true);
}
}
private void createNotification(
int notificationId, @MediaType int mediaType, String url, boolean isIncognito) {
final String channelId =
mediaType == MediaType.SCREEN_CAPTURE
? ChromeChannelDefinitions.ChannelId.SCREEN_CAPTURE
: ChromeChannelDefinitions.ChannelId.WEBRTC_CAM_AND_MIC;
Context appContext = ContextUtils.getApplicationContext();
NotificationWrapperBuilder builder =
NotificationWrapperBuilderFactory.createNotificationWrapperBuilder(
channelId,
new NotificationMetadata(
NotificationUmaTracker.SystemNotificationType.MEDIA_CAPTURE,
NOTIFICATION_NAMESPACE,
notificationId));
Intent tabIntent =
IntentHandler.createTrustedBringTabToFrontIntent(
notificationId, IntentHandler.BringToFrontSource.NOTIFICATION);
PendingIntentProvider contentIntent =
tabIntent == null
? null
: PendingIntentProvider.getActivity(
appContext, notificationId, tabIntent, 0);
// Add a "Stop" button to the screen capture notification and turn the notification
// into a high priority one.
PendingIntent stopIntent =
mediaType == MediaType.SCREEN_CAPTURE
? buildStopCapturePendingIntent(notificationId)
: null;
NotificationWrapper notification =
MediaCaptureNotificationUtil.createNotification(
builder,
mediaType,
isIncognito ? null : url,
appContext.getString(R.string.app_name),
contentIntent,
stopIntent);
mNotificationManager.notify(notification);
mNotifications.put(notificationId, mediaType);
updateSharedPreferencesEntry(notificationId, false);
NotificationUmaTracker.getInstance()
.onNotificationShown(
NotificationUmaTracker.SystemNotificationType.MEDIA_CAPTURE,
notification.getNotification());
if (mediaType == MediaType.SCREEN_CAPTURE) {
final Tab tab = TabWindowManagerSingleton.getInstance().getTabById(notificationId);
if (tab != null) {
WindowAndroid window = tab.getWebContents().getTopLevelNativeWindow();
MediaCaptureOverlayController overlayController =
MediaCaptureOverlayController.from(window);
if (overlayController != null) {
overlayController.startCapture(tab);
}
}
}
}
/**
* Update shared preferences entry with ids of the visible notifications.
* @param notificationId Id of the notification.
* @param remove Boolean describing if the notification was added or removed.
*/
private void updateSharedPreferencesEntry(int notificationId, boolean remove) {
Set<String> notificationIds =
new HashSet<>(
mSharedPreferences.readStringSet(
ChromePreferenceKeys.MEDIA_WEBRTC_NOTIFICATION_IDS,
new HashSet<>()));
if (remove
&& !notificationIds.isEmpty()
&& notificationIds.contains(String.valueOf(notificationId))) {
notificationIds.remove(String.valueOf(notificationId));
} else if (!remove) {
notificationIds.add(String.valueOf(notificationId));
}
mSharedPreferences.writeStringSet(
ChromePreferenceKeys.MEDIA_WEBRTC_NOTIFICATION_IDS, notificationIds);
}
@Override
public void onDestroy() {
cancelPreviousWebRtcNotifications();
super.onDestroy();
}
@Override
public boolean onUnbind(Intent intent) {
cancelPreviousWebRtcNotifications();
return super.onUnbind(intent);
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
/**
* @param webContents the webContents for the tab. Used to query the media capture state.
* @return A constant identifying what media is being captured.
*/
private static int getMediaType(@Nullable WebContents webContents) {
if (webContents == null) {
return MediaType.NO_MEDIA;
}
if (MediaCaptureDevicesDispatcherAndroid.isCapturingScreen(webContents)) {
return MediaType.SCREEN_CAPTURE;
}
boolean audio = MediaCaptureDevicesDispatcherAndroid.isCapturingAudio(webContents);
boolean video = MediaCaptureDevicesDispatcherAndroid.isCapturingVideo(webContents);
if (audio && video) {
return MediaType.AUDIO_AND_VIDEO;
} else if (audio) {
return MediaType.AUDIO_ONLY;
} else if (video) {
return MediaType.VIDEO_ONLY;
} else {
return MediaType.NO_MEDIA;
}
}
private static boolean shouldStartService(
Context context, @MediaType int mediaType, int tabId) {
if (mediaType != MediaType.NO_MEDIA) return true;
SharedPreferencesManager sharedPreferences = ChromeSharedPreferences.getInstance();
Set<String> notificationIds =
sharedPreferences.readStringSet(
ChromePreferenceKeys.MEDIA_WEBRTC_NOTIFICATION_IDS, null);
if (notificationIds != null
&& !notificationIds.isEmpty()
&& notificationIds.contains(String.valueOf(tabId))) {
return true;
}
return false;
}
/**
* Send an intent to MediaCaptureNotificationService to either create, update or destroy the
* notification identified by tabId.
* @param tabId Unique notification id.
* @param webContents The webContents of the tab; used to get the current media type.
* @param url Url of the current webrtc call.
*/
public static void updateMediaNotificationForTab(
Context context, int tabId, @Nullable WebContents webContents, GURL url) {
@MediaType int mediaType = getMediaType(webContents);
if (!shouldStartService(context, mediaType, tabId)) return;
Intent intent = new Intent(context, MediaCaptureNotificationService.class);
intent.setAction(ACTION_MEDIA_CAPTURE_UPDATE);
intent.putExtra(NOTIFICATION_ID_EXTRA, tabId);
intent.putExtra(NOTIFICATION_MEDIA_URL_EXTRA, url.getSpec());
intent.putExtra(NOTIFICATION_MEDIA_TYPE_EXTRA, mediaType);
if (TabWindowManagerSingleton.getInstance().getTabById(tabId) != null) {
intent.putExtra(
NOTIFICATION_MEDIA_IS_INCOGNITO,
TabWindowManagerSingleton.getInstance().getTabById(tabId).isIncognito());
}
context.startService(intent);
}
/** Clear any previous media notifications. */
public static void clearMediaNotifications() {
SharedPreferencesManager sharedPreferences = ChromeSharedPreferences.getInstance();
Set<String> notificationIds =
sharedPreferences.readStringSet(
ChromePreferenceKeys.MEDIA_WEBRTC_NOTIFICATION_IDS, null);
if (notificationIds == null || notificationIds.isEmpty()) return;
Context context = ContextUtils.getApplicationContext();
context.startService(new Intent(context, MediaCaptureNotificationService.class));
}
/** Build PendingIntent for the actions of screen capture notification. */
private PendingIntent buildStopCapturePendingIntent(int notificationId) {
Intent intent = new Intent(getService(), MediaCaptureNotificationService.class);
intent.setAction(ACTION_SCREEN_CAPTURE_STOP);
intent.putExtra(NOTIFICATION_ID_EXTRA, notificationId);
return PendingIntent.getService(
ContextUtils.getApplicationContext(),
notificationId,
intent,
PendingIntent.FLAG_UPDATE_CURRENT);
}
}