Chapter 10 Advanced Chrome APIs

Haiyue
45min

Chapter 10: Advanced Chrome APIs

Learning Objectives

  1. Master advanced usage of common Chrome extension APIs
  2. Learn tab, bookmark, and history management
  3. Implement network requests and cookie operations
  4. Apply notification, menu, and shortcut features

10.1 Tabs API Tab Management

10.1.1 Basic Tab Operations

// tabs-manager.js - Tab Manager
class TabsManager {
  constructor() {
    this.activeTabsCache = new Map();
    this.tabGroups = new Map();
    this.setupEventListeners();
  }

  setupEventListeners() {
    // Listen for tab changes
    chrome.tabs.onCreated.addListener((tab) => {
      this.onTabCreated(tab);
    });

    chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
      this.onTabUpdated(tabId, changeInfo, tab);
    });

    chrome.tabs.onRemoved.addListener((tabId, removeInfo) => {
      this.onTabRemoved(tabId, removeInfo);
    });

    chrome.tabs.onActivated.addListener((activeInfo) => {
      this.onTabActivated(activeInfo);
    });

    chrome.tabs.onMoved.addListener((tabId, moveInfo) => {
      this.onTabMoved(tabId, moveInfo);
    });
  }

  // Get current active tab
  async getCurrentTab() {
    const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
    return tabs[0];
  }

  // Get all tabs
  async getAllTabs() {
    return await chrome.tabs.query({});
  }

  // Find tabs by URL pattern
  async findTabsByUrl(urlPattern) {
    const allTabs = await this.getAllTabs();
    return allTabs.filter(tab => {
      if (typeof urlPattern === 'string') {
        return tab.url.includes(urlPattern);
      } else if (urlPattern instanceof RegExp) {
        return urlPattern.test(tab.url);
      }
      return false;
    });
  }

  // Create new tab
  async createTab(options = {}) {
    const defaultOptions = {
      active: true,
      pinned: false
    };

    const tabOptions = { ...defaultOptions, ...options };

    try {
      const tab = await chrome.tabs.create(tabOptions);
      console.log('Tab created:', tab.id);
      return tab;
    } catch (error) {
      console.error('Failed to create tab:', error);
      throw error;
    }
  }

  // Duplicate tab
  async duplicateTab(tabId) {
    try {
      const tab = await chrome.tabs.duplicate(tabId);
      console.log('Tab duplicated:', tab.id);
      return tab;
    } catch (error) {
      console.error('Failed to duplicate tab:', error);
      throw error;
    }
  }

  // Update tab
  async updateTab(tabId, updateProperties) {
    try {
      const tab = await chrome.tabs.update(tabId, updateProperties);
      console.log('Tab updated:', tabId);
      return tab;
    } catch (error) {
      console.error('Failed to update tab:', error);
      throw error;
    }
  }

  // Close tab
  async closeTab(tabId) {
    try {
      await chrome.tabs.remove(tabId);
      console.log('Tab closed:', tabId);
      return true;
    } catch (error) {
      console.error('Failed to close tab:', error);
      return false;
    }
  }

  // Batch close tabs
  async closeTabs(tabIds) {
    try {
      await chrome.tabs.remove(tabIds);
      console.log('Batch closed tabs:', tabIds.length);
      return true;
    } catch (error) {
      console.error('Batch close failed:', error);
      return false;
    }
  }

  // Move tab
  async moveTab(tabId, moveProperties) {
    try {
      const tabs = await chrome.tabs.move(tabId, moveProperties);
      console.log('Tab moved:', tabId);
      return tabs;
    } catch (error) {
      console.error('Failed to move tab:', error);
      throw error;
    }
  }

  // Toggle pin tab
  async togglePinTab(tabId) {
    try {
      const tab = await chrome.tabs.get(tabId);
      const updatedTab = await chrome.tabs.update(tabId, {
        pinned: !tab.pinned
      });
      console.log(`Tab ${tabId} ${updatedTab.pinned ? 'pinned' : 'unpinned'}`);
      return updatedTab;
    } catch (error) {
      console.error('Failed to toggle pin state:', error);
      throw error;
    }
  }

  // Toggle mute
  async toggleMuteTab(tabId) {
    try {
      const tab = await chrome.tabs.get(tabId);
      const updatedTab = await chrome.tabs.update(tabId, {
        muted: !tab.mutedInfo.muted
      });
      console.log(`Tab ${tabId} ${updatedTab.mutedInfo.muted ? 'muted' : 'unmuted'}`);
      return updatedTab;
    } catch (error) {
      console.error('Failed to toggle mute state:', error);
      throw error;
    }
  }

  // Advanced feature: Tab group management
  async createTabGroup(tabIds, options = {}) {
    try {
      const groupId = await chrome.tabs.group({ tabIds });

      if (options.title) {
        await chrome.tabGroups.update(groupId, {
          title: options.title,
          color: options.color || 'blue'
        });
      }

      this.tabGroups.set(groupId, {
        id: groupId,
        title: options.title || `Group ${groupId}`,
        color: options.color || 'blue',
        tabIds: tabIds,
        createdAt: Date.now()
      });

      console.log('Tab group created:', groupId);
      return groupId;
    } catch (error) {
      console.error('Failed to create tab group:', error);
      throw error;
    }
  }

  // Event handlers
  onTabCreated(tab) {
    console.log('Tab created:', tab.id, tab.url);
    this.activeTabsCache.set(tab.id, tab);

    // Send creation event notification
    chrome.runtime.sendMessage({
      action: 'tabCreated',
      tab: tab
    });
  }

  onTabUpdated(tabId, changeInfo, tab) {
    console.log('Tab updated:', tabId, changeInfo);

    // Update cache
    this.activeTabsCache.set(tabId, tab);

    // Check URL change
    if (changeInfo.url) {
      this.handleUrlChange(tab, changeInfo.url);
    }

    // Check status change
    if (changeInfo.status === 'complete') {
      this.handlePageLoadComplete(tab);
    }

    // Send update event notification
    chrome.runtime.sendMessage({
      action: 'tabUpdated',
      tabId: tabId,
      changeInfo: changeInfo,
      tab: tab
    });
  }

  onTabRemoved(tabId, removeInfo) {
    console.log('Tab removed:', tabId);

    // Clean up cache
    this.activeTabsCache.delete(tabId);

    // Send removal event notification
    chrome.runtime.sendMessage({
      action: 'tabRemoved',
      tabId: tabId,
      removeInfo: removeInfo
    });
  }

  onTabActivated(activeInfo) {
    console.log('Tab activated:', activeInfo.tabId);

    // Send activation event notification
    chrome.runtime.sendMessage({
      action: 'tabActivated',
      activeInfo: activeInfo
    });
  }

  onTabMoved(tabId, moveInfo) {
    console.log('Tab moved:', tabId, moveInfo);
  }

  handleUrlChange(tab, newUrl) {
    // Handle URL change logic
    console.log(`Tab ${tab.id} URL changed: ${newUrl}`);

    // Add URL filtering, redirection features here
    if (this.shouldBlockUrl(newUrl)) {
      this.redirectTab(tab.id, 'chrome://newtab/');
    }
  }

  handlePageLoadComplete(tab) {
    // Handle page load complete
    console.log(`Tab ${tab.id} load complete: ${tab.title}`);

    // Execute content script injection here
    this.injectContentScript(tab.id);
  }

  shouldBlockUrl(url) {
    // URL filtering logic
    const blockedPatterns = [
      /.*\.malicious-site\.com/,
      /.*dangerous-content.*/
    ];

    return blockedPatterns.some(pattern => pattern.test(url));
  }

  async redirectTab(tabId, newUrl) {
    try {
      await chrome.tabs.update(tabId, { url: newUrl });
      console.log(`Tab ${tabId} redirected to: ${newUrl}`);
    } catch (error) {
      console.error('Redirection failed:', error);
    }
  }

  async injectContentScript(tabId) {
    try {
      await chrome.scripting.executeScript({
        target: { tabId: tabId },
        files: ['content-script.js']
      });
      console.log(`Content script injected to tab ${tabId}`);
    } catch (error) {
      console.error('Failed to inject content script:', error);
    }
  }

  // Get tab statistics
  async getTabStats() {
    const allTabs = await this.getAllTabs();

    const stats = {
      total: allTabs.length,
      active: 0,
      pinned: 0,
      muted: 0,
      loading: 0,
      byDomain: new Map(),
      byWindow: new Map()
    };

    allTabs.forEach(tab => {
      if (tab.active) stats.active++;
      if (tab.pinned) stats.pinned++;
      if (tab.mutedInfo && tab.mutedInfo.muted) stats.muted++;
      if (tab.status === 'loading') stats.loading++;

      // Statistics by domain
      try {
        const domain = new URL(tab.url).hostname;
        stats.byDomain.set(domain, (stats.byDomain.get(domain) || 0) + 1);
      } catch (e) {
        // Ignore invalid URLs
      }

      // Statistics by window
      const windowId = tab.windowId;
      stats.byWindow.set(windowId, (stats.byWindow.get(windowId) || 0) + 1);
    });

    return {
      ...stats,
      byDomain: Object.fromEntries(stats.byDomain),
      byWindow: Object.fromEntries(stats.byWindow)
    };
  }

  // Remove duplicate tabs
  async removeDuplicateTabs() {
    const allTabs = await this.getAllTabs();
    const urlMap = new Map();
    const duplicateTabs = [];

    // Find duplicate tabs
    allTabs.forEach(tab => {
      if (urlMap.has(tab.url)) {
        duplicateTabs.push(tab.id);
      } else {
        urlMap.set(tab.url, tab.id);
      }
    });

    if (duplicateTabs.length > 0) {
      await this.closeTabs(duplicateTabs);
      console.log(`Closed ${duplicateTabs.length} duplicate tabs`);
    }

    return duplicateTabs.length;
  }
}

// Global tabs manager instance
const tabsManager = new TabsManager();

// Export for other modules
window.tabsManager = tabsManager;

10.1.2 Tab Object Properties Reference

Tab Object Properties

PropertyTypeDescription
idnumberUnique identifier for the tab
urlstringURL of the tab
titlestringTitle of the tab
activebooleanWhether it is the current active tab
pinnedbooleanWhether it is pinned
mutedbooleanWhether it is muted
statusstringLoading status: “loading”, “complete”, “unloaded”
windowIdnumberID of the window containing the tab
indexnumberPosition index in the window
favIconUrlstringWebsite icon URL
widthnumberTab width (optional)
heightnumberTab height (optional)
incognitobooleanWhether in incognito mode
highlightedbooleanWhether highlighted
selectedbooleanWhether selected (deprecated, kept for compatibility)
audiblebooleanWhether playing audio
discardedbooleanWhether discarded to save memory
autoDiscardablebooleanWhether auto-discard is allowed
groupIdnumberTab group ID (-1 means ungrouped)
openerTabIdnumberID of the tab that opened this tab (optional)
pendingUrlstringURL to be loaded (optional)
sessionIdstringSession ID (optional)

Tab Object Interface Definition

interface TabStatus {
  LOADING: "loading";
  COMPLETE: "complete";
  UNLOADED: "unloaded";

}

interface Tab {
  id: number;
  url: string;
  title?: string;
  active?: boolean;
  pinned?: boolean;
  muted?: boolean;
  status?: TabStatus;
  windowId?: number;
  index?: number;
  favIconUrl?: string;
  width?: number;
  height?: number;
  incognito?: boolean;
  highlighted?: boolean;
  selected?: boolean; // deprecated but kept for compatibility
  audible?: boolean;
  discarded?: boolean;
  autoDiscardable?: boolean;
  groupId?: number;
  openerTabId?: number;
  pendingUrl?: string;
  sessionId?: string;
}

(Continue with remaining sections of CE-010.md…)

10.2 Bookmarks API Bookmark Management

10.2.1 Bookmark Operations Implementation

// bookmarks-manager.js - Bookmarks Manager
class BookmarksManager {
  constructor() {
    this.bookmarksCache = new Map();
    this.bookmarkFolders = new Map();
    this.setupEventListeners();
  }

  setupEventListeners() {
    chrome.bookmarks.onCreated.addListener((id, bookmark) => {
      this.onBookmarkCreated(id, bookmark);
    });

    chrome.bookmarks.onRemoved.addListener((id, removeInfo) => {
      this.onBookmarkRemoved(id, removeInfo);
    });

    chrome.bookmarks.onChanged.addListener((id, changeInfo) => {
      this.onBookmarkChanged(id, changeInfo);
    });

    chrome.bookmarks.onMoved.addListener((id, moveInfo) => {
      this.onBookmarkMoved(id, moveInfo);
    });

    chrome.bookmarks.onChildrenReordered.addListener((id, reorderInfo) => {
      this.onChildrenReordered(id, reorderInfo);
    });
  }

  // Get bookmark tree
  async getBookmarkTree() {
    try {
      const tree = await chrome.bookmarks.getTree();
      return tree[0]; // Return root node
    } catch (error) {
      console.error('Failed to get bookmark tree:', error);
      throw error;
    }
  }

  // Search bookmarks
  async searchBookmarks(query) {
    try {
      const results = await chrome.bookmarks.search(query);
      return results.filter(bookmark => bookmark.url); // Only return actual bookmarks
    } catch (error) {
      console.error('Failed to search bookmarks:', error);
      throw error;
    }
  }

  // Create bookmark
  async createBookmark(bookmarkInfo) {
    const { title, url, parentId } = bookmarkInfo;

    try {
      // Check if bookmark with same URL already exists
      const existing = await this.searchBookmarks(url);
      if (existing.length > 0) {
        console.warn('Bookmark already exists:', url);
        return existing[0];
      }

      const bookmark = await chrome.bookmarks.create({
        title: title || 'Untitled Bookmark',
        url: url,
        parentId: parentId
      });

      console.log('Bookmark created:', bookmark.id);
      return bookmark;
    } catch (error) {
      console.error('Failed to create bookmark:', error);
      throw error;
    }
  }

  // Create bookmark folder
  async createBookmarkFolder(name, parentId) {
    try {
      const folder = await chrome.bookmarks.create({
        title: name,
        parentId: parentId
      });

      console.log('Bookmark folder created:', folder.id);
      return folder;
    } catch (error) {
      console.error('Failed to create bookmark folder:', error);
      throw error;
    }
  }

  // Update bookmark
  async updateBookmark(id, changes) {
    try {
      const updatedBookmark = await chrome.bookmarks.update(id, changes);
      console.log('Bookmark updated:', id);
      return updatedBookmark;
    } catch (error) {
      console.error('Failed to update bookmark:', error);
      throw error;
    }
  }

  // Delete bookmark
  async deleteBookmark(id) {
    try {
      await chrome.bookmarks.remove(id);
      console.log('Bookmark deleted:', id);
      return true;
    } catch (error) {
      console.error('Failed to delete bookmark:', error);
      return false;
    }
  }

  // Move bookmark
  async moveBookmark(id, destination) {
    try {
      const movedBookmark = await chrome.bookmarks.move(id, destination);
      console.log('Bookmark moved:', id);
      return movedBookmark;
    } catch (error) {
      console.error('Failed to move bookmark:', error);
      throw error;
    }
  }

  // Get full path of bookmark
  async getBookmarkPath(bookmarkId) {
    try {
      const path = [];
      let currentId = bookmarkId;

      while (currentId) {
        const bookmarks = await chrome.bookmarks.get(currentId);
        const bookmark = bookmarks[0];

        if (!bookmark) break;

        path.unshift(bookmark);
        currentId = bookmark.parentId;

        // Avoid infinite loop
        if (path.length > 10) break;
      }

      return path;
    } catch (error) {
      console.error('Failed to get bookmark path:', error);
      return [];
    }
  }

  // Get all bookmarks in folder
  async getBookmarksInFolder(folderId, includeSubfolders = false) {
    try {
      const children = await chrome.bookmarks.getChildren(folderId);
      const bookmarks = [];

      for (const child of children) {
        if (child.url) {
          // This is a bookmark
          bookmarks.push(child);
        } else if (includeSubfolders) {
          // This is a folder, recursively get bookmarks
          const subBookmarks = await this.getBookmarksInFolder(child.id, true);
          bookmarks.push(...subBookmarks);
        }
      }

      return bookmarks;
    } catch (error) {
      console.error('Failed to get folder bookmarks:', error);
      return [];
    }
  }

  // Find duplicate bookmarks
  async findDuplicateBookmarks() {
    try {
      const allBookmarks = await this.searchBookmarks('');
      const urlMap = new Map();
      const duplicates = [];

      allBookmarks.forEach(bookmark => {
        if (bookmark.url) {
          if (urlMap.has(bookmark.url)) {
            duplicates.push({
              original: urlMap.get(bookmark.url),
              duplicate: bookmark
            });
          } else {
            urlMap.set(bookmark.url, bookmark);
          }
        }
      });

      return duplicates;
    } catch (error) {
      console.error('Failed to find duplicate bookmarks:', error);
      return [];
    }
  }

  // Clean duplicate bookmarks
  async cleanDuplicateBookmarks() {
    const duplicates = await this.findDuplicateBookmarks();
    const removedCount = duplicates.length;

    for (const { duplicate } of duplicates) {
      await this.deleteBookmark(duplicate.id);
    }

    console.log(`Cleaned ${removedCount} duplicate bookmarks`);
    return removedCount;
  }

  // Export bookmarks
  async exportBookmarks() {
    try {
      const tree = await this.getBookmarkTree();
      const exportData = {
        version: '1.0',
        exportDate: new Date().toISOString(),
        bookmarks: this.flattenBookmarkTree(tree)
      };

      return exportData;
    } catch (error) {
      console.error('Failed to export bookmarks:', error);
      throw error;
    }
  }

  // Flatten bookmark tree
  flattenBookmarkTree(node, path = []) {
    const bookmarks = [];

    if (node.children) {
      for (const child of node.children) {
        if (child.url) {
          bookmarks.push({
            title: child.title,
            url: child.url,
            path: [...path, node.title].filter(p => p),
            id: child.id,
            dateAdded: child.dateAdded,
            dateGroupModified: child.dateGroupModified
          });
        } else {
          bookmarks.push(...this.flattenBookmarkTree(child, [...path, node.title]));
        }
      }
    }

    return bookmarks;
  }

  // Group bookmarks by domain
  async groupBookmarksByDomain() {
    try {
      const allBookmarks = await this.searchBookmarks('');
      const domainGroups = new Map();

      allBookmarks.forEach(bookmark => {
        try {
          const url = new URL(bookmark.url);
          const domain = url.hostname;

          if (!domainGroups.has(domain)) {
            domainGroups.set(domain, []);
          }

          domainGroups.get(domain).push(bookmark);
        } catch (error) {
          // Ignore invalid URLs
        }
      });

      return Object.fromEntries(domainGroups);
    } catch (error) {
      console.error('Failed to group by domain:', error);
      return {};
    }
  }

  // Bookmark statistics
  async getBookmarkStats() {
    try {
      const tree = await this.getBookmarkTree();
      const stats = {
        totalBookmarks: 0,
        totalFolders: 0,
        byDomain: new Map(),
        orphanedBookmarks: 0,
        recentBookmarks: 0
      };

      const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;

      const countBookmarks = (node) => {
        if (node.url) {
          stats.totalBookmarks++;

          // Count recent bookmarks
          if (node.dateAdded && node.dateAdded > oneWeekAgo) {
            stats.recentBookmarks++;
          }

          // Statistics by domain
          try {
            const domain = new URL(node.url).hostname;
            stats.byDomain.set(domain, (stats.byDomain.get(domain) || 0) + 1);
          } catch (error) {
            stats.orphanedBookmarks++;
          }
        } else {
          stats.totalFolders++;
        }

        if (node.children) {
          node.children.forEach(countBookmarks);
        }
      };

      countBookmarks(tree);

      return {
        ...stats,
        byDomain: Object.fromEntries(stats.byDomain)
      };
    } catch (error) {
      console.error('Failed to get bookmark stats:', error);
      return null;
    }
  }

  // Event handlers
  onBookmarkCreated(id, bookmark) {
    console.log('Bookmark created:', id, bookmark.title);
    this.bookmarksCache.set(id, bookmark);

    // Send creation event notification
    chrome.runtime.sendMessage({
      action: 'bookmarkCreated',
      bookmark: bookmark
    });
  }

  onBookmarkRemoved(id, removeInfo) {
    console.log('Bookmark removed:', id);
    this.bookmarksCache.delete(id);

    // Send removal event notification
    chrome.runtime.sendMessage({
      action: 'bookmarkRemoved',
      bookmarkId: id,
      removeInfo: removeInfo
    });
  }

  onBookmarkChanged(id, changeInfo) {
    console.log('Bookmark changed:', id, changeInfo);

    // Update cache
    const cached = this.bookmarksCache.get(id);
    if (cached) {
      Object.assign(cached, changeInfo);
    }

    // Send change event notification
    chrome.runtime.sendMessage({
      action: 'bookmarkChanged',
      bookmarkId: id,
      changeInfo: changeInfo
    });
  }

  onBookmarkMoved(id, moveInfo) {
    console.log('Bookmark moved:', id, moveInfo);

    // Send move event notification
    chrome.runtime.sendMessage({
      action: 'bookmarkMoved',
      bookmarkId: id,
      moveInfo: moveInfo
    });
  }

  onChildrenReordered(id, reorderInfo) {
    console.log('Bookmark children reordered:', id);

    // Send reorder event notification
    chrome.runtime.sendMessage({
      action: 'bookmarkChildrenReordered',
      parentId: id,
      reorderInfo: reorderInfo
    });
  }
}

// Global bookmarks manager instance
const bookmarksManager = new BookmarksManager();

// Export for other modules
window.bookmarksManager = bookmarksManager;

10.3 History API History Records

10.3.1 History Management

// history-manager.js - History Manager
class HistoryManager {
  constructor() {
    this.historyCache = new Map();
    this.setupEventListeners();
  }

  setupEventListeners() {
    chrome.history.onVisited.addListener((historyItem) => {
      this.onHistoryVisited(historyItem);
    });

    chrome.history.onVisitRemoved.addListener((removed) => {
      this.onHistoryRemoved(removed);
    });
  }

  // Search history
  async searchHistory(query, options = {}) {
    const defaultOptions = {
      text: query,
      startTime: 0,
      endTime: Date.now(),
      maxResults: 100
    };

    const searchOptions = { ...defaultOptions, ...options };

    try {
      const results = await chrome.history.search(searchOptions);
      console.log(`History search complete: ${results.length} results`);
      return results;
    } catch (error) {
      console.error('Failed to search history:', error);
      throw error;
    }
  }

  // Get visit records
  async getVisits(url) {
    try {
      const visits = await chrome.history.getVisits({ url });
      return visits;
    } catch (error) {
      console.error('Failed to get visit records:', error);
      throw error;
    }
  }

  // Add history item
  async addHistoryItem(url, title) {
    try {
      await chrome.history.addUrl({
        url: url,
        title: title || ''
      });
      console.log('History item added:', url);
      return true;
    } catch (error) {
      console.error('Failed to add history item:', error);
      return false;
    }
  }

  // Delete history item
  async deleteHistoryItem(url) {
    try {
      await chrome.history.deleteUrl({ url });
      console.log('History item deleted:', url);
      return true;
    } catch (error) {
      console.error('Failed to delete history item:', error);
      return false;
    }
  }

  // Delete history range
  async deleteHistoryRange(startTime, endTime) {
    try {
      await chrome.history.deleteRange({
        startTime,
        endTime
      });
      console.log(`History deleted: ${new Date(startTime)} - ${new Date(endTime)}`);
      return true;
    } catch (error) {
      console.error('Failed to delete history range:', error);
      return false;
    }
  }

  // Clear all history
  async clearAllHistory() {
    try {
      await chrome.history.deleteAll();
      console.log('All history cleared');
      return true;
    } catch (error) {
      console.error('Failed to clear history:', error);
      return false;
    }
  }

  // Get most visited sites
  async getTopSites(limit = 10) {
    try {
      const oneMonthAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;
      const allHistory = await this.searchHistory('', {
        startTime: oneMonthAgo,
        maxResults: 1000
      });

      // Count visits by domain
      const domainCounts = new Map();

      allHistory.forEach(item => {
        try {
          const url = new URL(item.url);
          const domain = url.hostname;
          const count = domainCounts.get(domain) || 0;
          domainCounts.set(domain, count + item.visitCount);
        } catch (error) {
          // Ignore invalid URLs
        }
      });

      // Sort and return top N
      const sortedDomains = Array.from(domainCounts.entries())
        .sort((a, b) => b[1] - a[1])
        .slice(0, limit)
        .map(([domain, count]) => ({ domain, count }));

      return sortedDomains;
    } catch (error) {
      console.error('Failed to get top sites:', error);
      return [];
    }
  }

  // Get history statistics
  async getHistoryStats(days = 30) {
    try {
      const startTime = Date.now() - days * 24 * 60 * 60 * 1000;
      const history = await this.searchHistory('', {
        startTime,
        maxResults: 10000
      });

      const stats = {
        totalVisits: 0,
        uniqueUrls: new Set(),
        uniqueDomains: new Set(),
        visitsByDay: new Map(),
        visitsByHour: new Map(),
        topDomains: new Map()
      };

      history.forEach(item => {
        stats.totalVisits += item.visitCount;
        stats.uniqueUrls.add(item.url);

        try {
          const url = new URL(item.url);
          const domain = url.hostname;
          stats.uniqueDomains.add(domain);

          // Count domain visits
          const domainCount = stats.topDomains.get(domain) || 0;
          stats.topDomains.set(domain, domainCount + item.visitCount);

          // Count by day
          if (item.lastVisitTime) {
            const date = new Date(item.lastVisitTime);
            const day = date.toISOString().split('T')[0];
            const dayCount = stats.visitsByDay.get(day) || 0;
            stats.visitsByDay.set(day, dayCount + 1);

            // Count by hour
            const hour = date.getHours();
            const hourCount = stats.visitsByHour.get(hour) || 0;
            stats.visitsByHour.set(hour, hourCount + 1);
          }
        } catch (error) {
          // Ignore invalid URLs
        }
      });

      return {
        totalVisits: stats.totalVisits,
        uniqueUrls: stats.uniqueUrls.size,
        uniqueDomains: stats.uniqueDomains.size,
        avgVisitsPerDay: stats.totalVisits / days,
        visitsByDay: Object.fromEntries(stats.visitsByDay),
        visitsByHour: Object.fromEntries(stats.visitsByHour),
        topDomains: Object.fromEntries(
          Array.from(stats.topDomains.entries())
            .sort((a, b) => b[1] - a[1])
            .slice(0, 10)
        )
      };
    } catch (error) {
      console.error('Failed to get history stats:', error);
      return null;
    }
  }

  // Find similar visit patterns
  async findSimilarVisitPatterns(targetUrl) {
    try {
      const targetVisits = await this.getVisits(targetUrl);
      const targetTimes = targetVisits.map(v => new Date(v.visitTime).getHours());

      const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
      const recentHistory = await this.searchHistory('', {
        startTime: oneWeekAgo,
        maxResults: 500
      });

      const similarUrls = [];

      for (const item of recentHistory) {
        if (item.url === targetUrl) continue;

        const visits = await this.getVisits(item.url);
        const visitTimes = visits.map(v => new Date(v.visitTime).getHours());

        // Calculate time pattern similarity
        const similarity = this.calculateTimeSimilarity(targetTimes, visitTimes);

        if (similarity > 0.5) {
          similarUrls.push({
            url: item.url,
            title: item.title,
            similarity: similarity,
            visitCount: item.visitCount
          });
        }
      }

      return similarUrls.sort((a, b) => b.similarity - a.similarity);
    } catch (error) {
      console.error('Failed to find similar visit patterns:', error);
      return [];
    }
  }

  calculateTimeSimilarity(times1, times2) {
    if (times1.length === 0 || times2.length === 0) return 0;

    const set1 = new Set(times1);
    const set2 = new Set(times2);
    const intersection = new Set([...set1].filter(x => set2.has(x)));
    const union = new Set([...set1, ...set2]);

    return intersection.size / union.size;
  }

  // Export history
  async exportHistory(days = 30) {
    try {
      const startTime = Date.now() - days * 24 * 60 * 60 * 1000;
      const history = await this.searchHistory('', {
        startTime,
        maxResults: 10000
      });

      const exportData = {
        exportDate: new Date().toISOString(),
        dateRange: {
          start: new Date(startTime).toISOString(),
          end: new Date().toISOString()
        },
        totalItems: history.length,
        history: history.map(item => ({
          url: item.url,
          title: item.title,
          visitCount: item.visitCount,
          lastVisitTime: new Date(item.lastVisitTime).toISOString()
        }))
      };

      return exportData;
    } catch (error) {
      console.error('Failed to export history:', error);
      throw error;
    }
  }

  // Event handlers
  onHistoryVisited(historyItem) {
    console.log('Page visited:', historyItem.url);

    // Update cache
    this.historyCache.set(historyItem.id, historyItem);

    // Send visit event notification
    chrome.runtime.sendMessage({
      action: 'historyVisited',
      historyItem: historyItem
    });
  }

  onHistoryRemoved(removed) {
    console.log('History removed:', removed);

    // Clean cache
    if (removed.urls) {
      removed.urls.forEach(url => {
        // Remove corresponding records from cache
        for (const [id, item] of this.historyCache.entries()) {
          if (item.url === url) {
            this.historyCache.delete(id);
          }
        }
      });
    } else {
      // Clear all
      this.historyCache.clear();
    }

    // Send removal event notification
    chrome.runtime.sendMessage({
      action: 'historyRemoved',
      removed: removed
    });
  }
}

// Global history manager instance
const historyManager = new HistoryManager();

// Export for other modules
window.historyManager = historyManager;

10.4 Notifications API Notifications

10.4.1 Notification System Implementation

// notifications-manager.js - Notifications Manager
class NotificationsManager {
  constructor() {
    this.activeNotifications = new Map();
    this.notificationQueue = [];
    this.isProcessingQueue = false;
    this.setupEventListeners();
  }

  setupEventListeners() {
    chrome.notifications.onClicked.addListener((notificationId) => {
      this.onNotificationClicked(notificationId);
    });

    chrome.notifications.onButtonClicked.addListener((notificationId, buttonIndex) => {
      this.onButtonClicked(notificationId, buttonIndex);
    });

    chrome.notifications.onClosed.addListener((notificationId, byUser) => {
      this.onNotificationClosed(notificationId, byUser);
    });

    chrome.notifications.onPermissionLevelChanged.addListener((level) => {
      this.onPermissionLevelChanged(level);
    });
  }

  // Create basic notification
  async createNotification(options) {
    const notificationId = this.generateNotificationId();
    const notificationOptions = {
      type: 'basic',
      iconUrl: chrome.runtime.getURL('icons/icon48.png'),
      title: 'Notification',
      message: 'This is a notification message',
      ...options
    };

    try {
      await chrome.notifications.create(notificationId, notificationOptions);

      // Save notification info
      this.activeNotifications.set(notificationId, {
        id: notificationId,
        options: notificationOptions,
        createdAt: Date.now(),
        actions: options.actions || []
      });

      console.log('Notification created:', notificationId);
      return notificationId;
    } catch (error) {
      console.error('Failed to create notification:', error);
      throw error;
    }
  }

  // Create rich notification
  async createRichNotification(options) {
    const { title, message, items, progress, imageUrl, buttons } = options;

    const notificationOptions = {
      type: items ? 'list' : (progress !== undefined ? 'progress' : 'image'),
      iconUrl: chrome.runtime.getURL('icons/icon48.png'),
      title: title || 'Rich Notification',
      message: message || '',
      ...options
    };

    // Add list items
    if (items) {
      notificationOptions.items = items.map(item => ({
        title: item.title || '',
        message: item.message || ''
      }));
    }

    // Add progress bar
    if (progress !== undefined) {
      notificationOptions.progress = Math.max(0, Math.min(100, progress));
    }

    // Add image
    if (imageUrl) {
      notificationOptions.imageUrl = imageUrl;
    }

    // Add buttons
    if (buttons) {
      notificationOptions.buttons = buttons.map(button => ({
        title: button.title,
        iconUrl: button.iconUrl || ''
      }));
    }

    return await this.createNotification(notificationOptions);
  }

  // Update notification
  async updateNotification(notificationId, options) {
    try {
      const notification = this.activeNotifications.get(notificationId);
      if (!notification) {
        throw new Error('Notification not found');
      }

      await chrome.notifications.update(notificationId, options);

      // Update cache
      notification.options = { ...notification.options, ...options };
      notification.updatedAt = Date.now();

      console.log('Notification updated:', notificationId);
      return true;
    } catch (error) {
      console.error('Failed to update notification:', error);
      return false;
    }
  }

  // Clear notification
  async clearNotification(notificationId) {
    try {
      await chrome.notifications.clear(notificationId);
      this.activeNotifications.delete(notificationId);
      console.log('Notification cleared:', notificationId);
      return true;
    } catch (error) {
      console.error('Failed to clear notification:', error);
      return false;
    }
  }

  // Clear all notifications
  async clearAllNotifications() {
    const notificationIds = Array.from(this.activeNotifications.keys());

    for (const id of notificationIds) {
      await this.clearNotification(id);
    }

    console.log('All notifications cleared');
    return notificationIds.length;
  }

  // Check permission level
  async getPermissionLevel() {
    return new Promise((resolve) => {
      chrome.notifications.getPermissionLevel((level) => {
        resolve(level);
      });
    });
  }

  // Queue notification system
  async queueNotification(options, priority = 'normal') {
    const notificationItem = {
      id: this.generateNotificationId(),
      options: options,
      priority: priority,
      timestamp: Date.now()
    };

    this.notificationQueue.push(notificationItem);
    this.notificationQueue.sort((a, b) => {
      const priorityOrder = { 'high': 0, 'normal': 1, 'low': 2 };
      return priorityOrder[a.priority] - priorityOrder[b.priority];
    });

    if (!this.isProcessingQueue) {
      this.processNotificationQueue();
    }

    return notificationItem.id;
  }

  async processNotificationQueue() {
    this.isProcessingQueue = true;

    while (this.notificationQueue.length > 0) {
      // Check current active notification count
      if (this.activeNotifications.size >= 5) {
        // Wait for some notifications to be cleared
        await this.delay(2000);
        continue;
      }

      const notificationItem = this.notificationQueue.shift();
      try {
        await this.createNotification(notificationItem.options);
        await this.delay(500); // Prevent notifications from being too frequent
      } catch (error) {
        console.error('Queue notification processing failed:', error);
      }
    }

    this.isProcessingQueue = false;
  }

  // Smart notification feature
  async createSmartNotification(type, data) {
    let notificationOptions;

    switch (type) {
      case 'download_complete':
        notificationOptions = {
          title: 'Download Complete',
          message: `File "${data.filename}" download complete`,
          buttons: [
            { title: 'Open File' },
            { title: 'Open Folder' }
          ],
          actions: ['openFile', 'openFolder'],
          data: data
        };
        break;

      case 'new_email':
        notificationOptions = {
          type: 'list',
          title: `New Email (${data.count})`,
          message: data.count > 1 ? `You have ${data.count} new emails` : 'You have a new email',
          items: data.emails.map(email => ({
            title: email.sender,
            message: email.subject
          })),
          buttons: [
            { title: 'View Email' }
          ],
          actions: ['openEmail'],
          data: data
        };
        break;

      case 'reminder':
        notificationOptions = {
          title: 'Reminder',
          message: data.message,
          buttons: [
            { title: 'Done' },
            { title: 'Snooze' }
          ],
          actions: ['markDone', 'snooze'],
          data: data
        };
        break;

      case 'progress':
        notificationOptions = {
          type: 'progress',
          title: data.title || 'Progress Notification',
          message: data.message,
          progress: data.progress,
          data: data
        };
        break;

      default:
        notificationOptions = {
          title: 'Notification',
          message: data.message || 'Default notification message',
          data: data
        };
    }

    return await this.createNotification(notificationOptions);
  }

  // Batch notifications
  async createBatchNotifications(notifications, options = {}) {
    const { delay = 1000, maxConcurrent = 3 } = options;
    const results = [];

    for (let i = 0; i < notifications.length; i += maxConcurrent) {
      const batch = notifications.slice(i, i + maxConcurrent);
      const batchPromises = batch.map(notif => this.createNotification(notif));

      try {
        const batchResults = await Promise.allSettled(batchPromises);
        results.push(...batchResults);

        // Delay between batches
        if (i + maxConcurrent < notifications.length) {
          await this.delay(delay);
        }
      } catch (error) {
        console.error('Batch notification processing failed:', error);
      }
    }

    const successful = results.filter(r => r.status === 'fulfilled').length;
    console.log(`Batch notifications complete: ${successful}/${notifications.length}`);

    return results;
  }

  // Event handlers
  onNotificationClicked(notificationId) {
    console.log('Notification clicked:', notificationId);

    const notification = this.activeNotifications.get(notificationId);
    if (notification && notification.actions) {
      // Execute default action
      this.executeAction('click', notification);
    }

    // Auto-clear notification
    this.clearNotification(notificationId);
  }

  onButtonClicked(notificationId, buttonIndex) {
    console.log('Notification button clicked:', notificationId, buttonIndex);

    const notification = this.activeNotifications.get(notificationId);
    if (notification && notification.actions && notification.actions[buttonIndex]) {
      const action = notification.actions[buttonIndex];
      this.executeAction(action, notification);
    }

    // Clear notification
    this.clearNotification(notificationId);
  }

  onNotificationClosed(notificationId, byUser) {
    console.log('Notification closed:', notificationId, byUser ? 'User closed' : 'Auto-closed');

    this.activeNotifications.delete(notificationId);

    // Send close event notification
    chrome.runtime.sendMessage({
      action: 'notificationClosed',
      notificationId: notificationId,
      byUser: byUser
    });
  }

  onPermissionLevelChanged(level) {
    console.log('Notification permission level changed:', level);

    // Adjust notification strategy based on permission level
    if (level === 'denied') {
      this.notificationQueue = [];
      this.clearAllNotifications();
    }
  }

  executeAction(action, notification) {
    const data = notification.data || {};

    switch (action) {
      case 'openFile':
        // Open file logic
        console.log('Open file:', data.filepath);
        break;

      case 'openFolder':
        // Open folder logic
        console.log('Open folder:', data.folder);
        break;

      case 'openEmail':
        // Open email logic
        chrome.tabs.create({ url: 'mailto:' });
        break;

      case 'markDone':
        // Mark as done logic
        console.log('Task completed:', data.taskId);
        break;

      case 'snooze':
        // Snooze logic
        setTimeout(() => {
          this.createSmartNotification('reminder', data);
        }, 15 * 60 * 1000); // Remind again after 15 minutes
        break;

      default:
        console.log('Execute action:', action, data);
    }
  }

  generateNotificationId() {
    return `notif_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }

  delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  // Get notification statistics
  getNotificationStats() {
    const active = this.activeNotifications.size;
    const queued = this.notificationQueue.length;

    const typeStats = {};
    for (const notification of this.activeNotifications.values()) {
      const type = notification.options.type || 'basic';
      typeStats[type] = (typeStats[type] || 0) + 1;
    }

    return {
      active: active,
      queued: queued,
      total: active + queued,
      byType: typeStats,
      isProcessingQueue: this.isProcessingQueue
    };
  }
}

// Global notifications manager instance
const notificationsManager = new NotificationsManager();

// Export for other modules
window.notificationsManager = notificationsManager;

warning Permission Requirements Using these Chrome APIs requires declaring appropriate permissions in manifest.json:

  • tabs: Tab operations
  • bookmarks: Bookmark management
  • history: History records
  • notifications: Notification features
  • activeTab: Current tab access

tip Best Practices

  1. Error Handling: Always handle exceptions from API calls
  2. Permission Checks: Check permission status before using APIs
  3. Performance Optimization: Avoid frequent API calls, use caching
  4. User Experience: Provide clear feedback and progress indicators
  5. Privacy Protection: Handle sensitive data like history carefully

note Summary This chapter provided an in-depth introduction to the most commonly used APIs in Chrome extensions, including tab management, bookmark operations, history processing, and notification systems. Mastering these advanced API usages will enable you to develop powerful browser extensions. In the next chapter, we will learn about user interface design and experience optimization to create more user-friendly extension interfaces.

Categories