// Copyright 2015 The Chromium Authors. All rights reserved.
// 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.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.IBinder;
import android.support.v4.app.NotificationCompat;
import android.util.SparseIntArray;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabWebContentsDelegateAndroid;
import java.net.MalformedURLException;
import java.net.URL;
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 MediaCaptureNotificationService extends Service {
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_TYPE_EXTRA = "NotificationMediaType";
private static final String NOTIFICATION_MEDIA_URL_EXTRA = "NotificationMediaUrl";
private static final String WEBRTC_NOTIFICATION_IDS = "WebRTCNotificationIds";
private static final String TAG = "MediaCapture";
private static final int MEDIATYPE_NO_MEDIA = 0;
private static final int MEDIATYPE_AUDIO_AND_VIDEO = 1;
private static final int MEDIATYPE_VIDEO_ONLY = 2;
private static final int MEDIATYPE_AUDIO_ONLY = 3;
private static final int MEDIATYPE_SCREEN_CAPTURE = 4;
private NotificationManager mNotificationManager;
private Context mContext;
private SharedPreferences mSharedPreferences;
private final SparseIntArray mNotifications = new SparseIntArray();
@Override
public void onCreate() {
mContext = getApplicationContext();
mNotificationManager = (NotificationManager) mContext.getSystemService(
Context.NOTIFICATION_SERVICE);
mSharedPreferences = ContextUtils.getAppSharedPreferences();
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, 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();
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);
if (ACTION_MEDIA_CAPTURE_UPDATE.equals(action)) {
updateNotification(notificationId, mediaType, url);
} else if (ACTION_SCREEN_CAPTURE_STOP.equals(action)) {
// Notify native to stop screen capture when the STOP button in notification
// is clicked.
TabWebContentsDelegateAndroid.notifyStopped(notificationId);
}
}
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.getStringSet(WEBRTC_NOTIFICATION_IDS, null);
if (notificationIds == null) return;
Iterator<String> iterator = notificationIds.iterator();
while (iterator.hasNext()) {
mNotificationManager.cancel(NOTIFICATION_NAMESPACE, Integer.parseInt(iterator.next()));
}
SharedPreferences.Editor sharedPreferenceEditor = mSharedPreferences.edit();
sharedPreferenceEditor.remove(MediaCaptureNotificationService.WEBRTC_NOTIFICATION_IDS);
sharedPreferenceEditor.apply();
}
/**
* 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.
*/
private void updateNotification(int notificationId, int mediaType, String url) {
if (doesNotificationExist(notificationId)
&& !doesNotificationNeedUpdate(notificationId, mediaType)) {
return;
}
destroyNotification(notificationId);
if (mediaType != MEDIATYPE_NO_MEDIA) {
createNotification(notificationId, mediaType, url);
}
if (mNotifications.size() == 0) stopSelf();
}
/**
* Destroys the notification for the id notificationId.
* @param notificationId Unique id of the notification.
*/
private void destroyNotification(int notificationId) {
if (doesNotificationExist(notificationId)) {
mNotificationManager.cancel(NOTIFICATION_NAMESPACE, notificationId);
mNotifications.delete(notificationId);
updateSharedPreferencesEntry(notificationId, true);
}
}
/**
* Creates a notification 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.
*/
private void createNotification(int notificationId, int mediaType, String url) {
NotificationCompat.Builder builder =
new NotificationCompat.Builder(mContext)
.setAutoCancel(false)
.setOngoing(true)
.setContentTitle(mContext.getString(R.string.app_name))
.setSmallIcon(getNotificationIconId(mediaType))
.setLocalOnly(true);
StringBuilder contentText =
new StringBuilder(getNotificationContentText(mediaType, url)).append('.');
Intent tabIntent = Tab.createBringTabToFrontIntent(notificationId);
if (tabIntent != null) {
PendingIntent contentIntent = PendingIntent.getActivity(
mContext, notificationId, tabIntent, 0);
builder.setContentIntent(contentIntent);
if (mediaType == MEDIATYPE_SCREEN_CAPTURE) {
// Add a "Stop" button to the screen capture notification and turn the notification
// into a high priority one.
builder.setPriority(Notification.PRIORITY_HIGH);
builder.setVibrate(new long[0]);
builder.addAction(R.drawable.ic_vidcontrol_stop,
mContext.getResources().getString(R.string.accessibility_stop),
buildStopCapturePendingIntent(notificationId));
} else {
contentText.append(mContext.getResources().getString(
R.string.media_notification_link_text, url));
}
} else {
contentText.append(" ").append(url);
}
builder.setContentText(contentText);
Notification notification = new NotificationCompat.BigTextStyle(builder)
.bigText(contentText).build();
mNotificationManager.notify(NOTIFICATION_NAMESPACE, notificationId, notification);
mNotifications.put(notificationId, mediaType);
updateSharedPreferencesEntry(notificationId, false);
}
/**
* Builds notification content text for the provided mediaType and url.
* @param mediaType Media type of the notification.
* @param url Url of the current webrtc call.
* @return A string builder initialized to the contents of the specified string.
*/
private String getNotificationContentText(int mediaType, String url) {
if (mediaType == MEDIATYPE_SCREEN_CAPTURE) {
return mContext.getResources().getString(
R.string.screen_capture_notification_text, url);
}
int notificationContentTextId = 0;
if (mediaType == MEDIATYPE_AUDIO_AND_VIDEO) {
notificationContentTextId = R.string.video_audio_call_notification_text_2;
} else if (mediaType == MEDIATYPE_VIDEO_ONLY) {
notificationContentTextId = R.string.video_call_notification_text_2;
} else if (mediaType == MEDIATYPE_AUDIO_ONLY) {
notificationContentTextId = R.string.audio_call_notification_text_2;
}
return mContext.getResources().getString(notificationContentTextId);
}
/**
* @param mediaType Media type of the notification.
* @return An icon id of the provided mediaType.
*/
private int getNotificationIconId(int mediaType) {
int notificationIconId = 0;
if (mediaType == MEDIATYPE_AUDIO_AND_VIDEO) {
notificationIconId = R.drawable.webrtc_video;
} else if (mediaType == MEDIATYPE_VIDEO_ONLY) {
notificationIconId = R.drawable.webrtc_video;
} else if (mediaType == MEDIATYPE_AUDIO_ONLY) {
notificationIconId = R.drawable.webrtc_audio;
} else if (mediaType == MEDIATYPE_SCREEN_CAPTURE) {
notificationIconId = R.drawable.webrtc_video;
}
return notificationIconId;
}
/**
* 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<String>(mSharedPreferences.getStringSet(WEBRTC_NOTIFICATION_IDS,
new HashSet<String>()));
if (remove && !notificationIds.isEmpty()
&& notificationIds.contains(String.valueOf(notificationId))) {
notificationIds.remove(String.valueOf(notificationId));
} else if (!remove) {
notificationIds.add(String.valueOf(notificationId));
}
SharedPreferences.Editor sharedPreferenceEditor = mSharedPreferences.edit();
sharedPreferenceEditor.putStringSet(WEBRTC_NOTIFICATION_IDS, notificationIds);
sharedPreferenceEditor.apply();
}
@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 audio If audio is being captured.
* @param video If video is being captured.
* @param screen If screen is being captured.
* @return A constant identify what media is being captured.
*/
public static int getMediaType(boolean audio, boolean video, boolean screen) {
if (screen) {
return MEDIATYPE_SCREEN_CAPTURE;
} else 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, int mediaType, int tabId) {
if (mediaType != MEDIATYPE_NO_MEDIA) return true;
SharedPreferences sharedPreferences =
ContextUtils.getAppSharedPreferences();
Set<String> notificationIds =
sharedPreferences.getStringSet(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 mediaType The media type that is being captured.
* @param fullUrl Url of the current webrtc call.
*/
public static void updateMediaNotificationForTab(
Context context, int tabId, int mediaType, String fullUrl) {
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);
String baseUrl = fullUrl;
try {
URL url = new URL(fullUrl);
baseUrl = url.getProtocol() + "://" + url.getHost();
} catch (MalformedURLException e) {
Log.w(TAG, "Error parsing the webrtc url, %s ", fullUrl);
}
intent.putExtra(NOTIFICATION_MEDIA_URL_EXTRA, baseUrl);
intent.putExtra(NOTIFICATION_MEDIA_TYPE_EXTRA, mediaType);
context.startService(intent);
}
/**
* Clear any previous media notifications.
*/
public static void clearMediaNotifications(Context context) {
SharedPreferences sharedPreferences =
ContextUtils.getAppSharedPreferences();
Set<String> notificationIds =
sharedPreferences.getStringSet(WEBRTC_NOTIFICATION_IDS, null);
if (notificationIds == null || notificationIds.isEmpty()) return;
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(this, MediaCaptureNotificationService.class);
intent.setAction(ACTION_SCREEN_CAPTURE_STOP);
intent.putExtra(NOTIFICATION_ID_EXTRA, notificationId);
return PendingIntent.getService(
mContext, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
}