// Copyright 2016 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.incognito;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityManager.AppTask;
import android.app.ActivityManager.RecentTaskInfo;
import android.app.IntentService;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.text.TextUtils;
import android.util.Pair;
import org.chromium.base.ActivityState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.ContextUtils;
import org.chromium.base.ThreadUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.TabState;
import org.chromium.chrome.browser.document.ChromeLauncherActivity;
import org.chromium.chrome.browser.document.DocumentUtils;
import org.chromium.chrome.browser.tabmodel.TabWindowManager;
import org.chromium.chrome.browser.tabmodel.TabbedModeTabPersistencePolicy;
import java.io.File;
import java.lang.ref.WeakReference;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Service that handles the action of clicking on the incognito notification.
*/
public class IncognitoNotificationService extends IntentService {
private static final String TAG = "incognito_notification";
private static final String ACTION_CLOSE_ALL_INCOGNITO =
"com.google.android.apps.chrome.incognito.CLOSE_ALL_INCOGNITO";
@VisibleForTesting
public static PendingIntent getRemoveAllIncognitoTabsIntent(Context context) {
Intent intent = new Intent(context, IncognitoNotificationService.class);
intent.setAction(ACTION_CLOSE_ALL_INCOGNITO);
return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_ONE_SHOT);
}
/** Empty public constructor needed by Android. */
public IncognitoNotificationService() {
super(TAG);
}
@Override
protected void onHandleIntent(Intent intent) {
closeIncognitoTabsInRunningTabbedActivities();
boolean clearedIncognito = deleteIncognitoStateFilesInDirectory(
TabbedModeTabPersistencePolicy.getOrCreateTabbedModeStateDirectory());
// If we failed clearing all of the incognito tabs, then do not dismiss the notification.
if (!clearedIncognito) return;
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
int incognitoCount = TabWindowManager.getInstance().getIncognitoTabCount();
assert incognitoCount == 0;
if (incognitoCount == 0) {
IncognitoNotificationManager.dismissIncognitoNotification();
}
}
});
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
// Now ensure that the snapshots in recents are all cleared for Tabbed activities
// to remove any trace of incognito mode.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
focusChromeIfNecessary();
} else {
removeNonVisibleChromeTabbedRecentEntries();
}
}
});
}
private void focusChromeIfNecessary() {
Set<Integer> visibleTaskIds = getTaskIdsForVisibleActivities();
int tabbedTaskId = -1;
List<WeakReference<Activity>> runningActivities =
ApplicationStatus.getRunningActivities();
for (int i = 0; i < runningActivities.size(); i++) {
Activity activity = runningActivities.get(i).get();
if (activity == null) continue;
if (activity instanceof ChromeTabbedActivity) {
tabbedTaskId = activity.getTaskId();
break;
}
}
// If the task containing the tabbed activity is visible, then do nothing as there is no
// snapshot that would need to be regenerated.
if (visibleTaskIds.contains(tabbedTaskId)) return;
Context context = ContextUtils.getApplicationContext();
Intent startIntent = new Intent(Intent.ACTION_MAIN);
startIntent.setPackage(context.getPackageName());
startIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(startIntent);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void removeNonVisibleChromeTabbedRecentEntries() {
Set<Integer> visibleTaskIds = getTaskIdsForVisibleActivities();
Context context = ContextUtils.getApplicationContext();
ActivityManager manager =
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
PackageManager pm = getPackageManager();
for (AppTask task : manager.getAppTasks()) {
RecentTaskInfo info = DocumentUtils.getTaskInfoFromTask(task);
if (info == null) continue;
String className = DocumentUtils.getTaskClassName(task, pm);
// It is not easily possible to distinguish between tasks sitting on top of
// ChromeLauncherActivity, so we treat them all as likely ChromeTabbedActivities and
// close them to be on the cautious side of things.
if ((TextUtils.equals(className, ChromeTabbedActivity.class.getName())
|| TextUtils.equals(className, ChromeLauncherActivity.class.getName()))
&& !visibleTaskIds.contains(info.id)) {
task.finishAndRemoveTask();
}
}
}
private Set<Integer> getTaskIdsForVisibleActivities() {
List<WeakReference<Activity>> runningActivities =
ApplicationStatus.getRunningActivities();
Set<Integer> visibleTaskIds = new HashSet<>();
for (int i = 0; i < runningActivities.size(); i++) {
Activity activity = runningActivities.get(i).get();
if (activity == null) continue;
int activityState = ApplicationStatus.getStateForActivity(activity);
if (activityState != ActivityState.STOPPED
&& activityState != ActivityState.DESTROYED) {
visibleTaskIds.add(activity.getTaskId());
}
}
return visibleTaskIds;
}
/**
* Iterate across the running activities and for any running tabbed mode activities close their
* incognito tabs.
*
* @see TabWindowManager#getIndexForWindow(Activity)
*/
private void closeIncognitoTabsInRunningTabbedActivities() {
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
List<WeakReference<Activity>> runningActivities =
ApplicationStatus.getRunningActivities();
for (int i = 0; i < runningActivities.size(); i++) {
Activity activity = runningActivities.get(i).get();
if (activity == null) continue;
if (!(activity instanceof ChromeTabbedActivity)) continue;
ChromeTabbedActivity tabbedActivity = (ChromeTabbedActivity) activity;
if (tabbedActivity.isActivityDestroyed()) continue;
tabbedActivity.getTabModelSelector().getModel(true).closeAllTabs(
false, false);
}
}
});
}
/**
* @return Whether deleting all the incognito files was successful.
*/
private boolean deleteIncognitoStateFilesInDirectory(File directory) {
File[] allTabStates = directory.listFiles();
if (allTabStates == null) return true;
boolean deletionSuccessful = true;
for (int i = 0; i < allTabStates.length; i++) {
String fileName = allTabStates[i].getName();
Pair<Integer, Boolean> tabInfo = TabState.parseInfoFromFilename(fileName);
if (tabInfo == null || !tabInfo.second) continue;
deletionSuccessful &= allTabStates[i].delete();
}
return deletionSuccessful;
}
}