/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ //! # TaskbarManager Pin WinRT API //! //! This implements functionality to pin an app to the taskbar using the //! TaskbarManager WinRT API. This was originally exposed to UWP/MSIX apps, and //! later extended to unpackaged Win32 apps while locking down the undocumented //! [IPinnedList3 COM API][crate::taskbar::com]. //! //! ## Secondary Pinning //! //! For unpackaged Win32 applications secondary applications can be pinned to //! the taskbar by temporarily changing the process AUMID while requesting the //! application be pinned. //! //! This does not work for MSIX applications as the process AUMID is //! virtualized, and modifying it within the application is not visible to the //! system. Instead MSIX relies on [Secondary Tiles][crate::secondary_tiles]. //! //! ## Limited Access Feature //! //! This feature initially required the "com.microsoft.windows.taskbar.pin" //! [Limited Access Feature][crate::limited_access_features] unlocked to work //! for unpackaged Win32 applications. //! //! As of Windows 11 26200 Build 7840 this graduated out of limited access and //! became generally available. //! //! ## Requirements //! //! This API requires a shortcut present in the virtual shell:appsfolder //! directory, i.e. a shortcut with unique AUMID in either the User or Common //! Start Menu folders. Note that there is a delay between files being created //! in the Start Menu folders and becoming accessible in shell:appsfolder. //! //! Additionally the app must be focused when pinning is requested. use nserror::{NS_ERROR_NOT_AVAILABLE, NS_ERROR_UNEXPECTED, nsresult}; use nsstring::nsAString; use std::sync::LazyLock; use windows::{ApplicationModel::Package, UI::Shell::TaskbarManager, core::Error as WinError}; use super::PinResult; use crate::{ limited_access_features::LimitedAccessFeatureService, util::{async_timer, thread::MainThreadGuard}, }; static LAF_LOCK: LazyLock> = LazyLock::new(|| { let svc = LimitedAccessFeatureService::new(); let feature_id = svc.get_taskbar_pin_feature_id()?; let feature = svc.generate_limited_access_feature(&feature_id)?; feature.unlock()?.then_some(()).ok_or(NS_ERROR_UNEXPECTED) }); pub(super) fn is_pinning_allowed() -> bool { if let Err(_e) = *LAF_LOCK { // Limited Access Feature no longer necessary for Windows 11 26200 Build // 7840, and possibly other channels. log::info!( "Failed to unlock Limited Access Feature, attempting to use Taskbar Pinning API assuming LAF is no longer necessary." ); } TaskbarManager::GetDefault() .and_then(|m| m.IsPinningAllowed()) .unwrap_or(false) } /// Pins the provided app to the taskbar using the WinRT TaskbarManager API, /// optionally returning before the pin request has resolved. pub(super) async fn pin_to_taskbar( aumid: &nsAString, fire_and_forget: bool, // We need to be on a UI thread for taskbar pinning prompt to show. _main_guard: MainThreadGuard, ) -> Result { if let Err(_e) = *LAF_LOCK { // Limited Access Feature no longer necessary for Windows 11 26200 Build // 7840, and possibly other channels. log::info!( "Failed to unlock Limited Access Feature, attempting to use Taskbar Pinning API assuming LAF is no longer necessary." ); } log::info!("Pinning with AUMID {aumid}"); // Ideally we would refactor this such that AUMID is only provided in // non-MSIX contexts and error when that is violated, and generalize our // pinning abstraction to automatically use secondary tile pinning when in // an MSIX context. For now we match the behavior adopted from prior // implementations. let aumid_holder = match Package::Current() { Ok(_) => None, Err(_) => Some(aumid::Holder::set_aumid(aumid).await?), }; let manager = TaskbarManager::GetDefault().map_err(|e| { log::debug!("TaskbarManager not available: {e:?}"); NS_ERROR_NOT_AVAILABLE })?; async { if xpcom::is_in_automation() { // Return early in tests to avoid actually pinning the app. Also // forces the AUMID to reset immediately by dropping the AUMID // holder instead of moving it into an async to resolve later. return Ok(PinResult::Unknown); } let user_confirmed = manager.RequestPinCurrentAppAsync()?; if let Some(aumid_holder) = aumid_holder { // Schedule restoring the process AUMID to remove a race between it // and processing the pin request using the temporarily set AUMID. // This is believed to work due to it hypothetically allowing the // main thread's STA Message loop to run first. // // Note: we don't want to await the pin request before resetting the // AUMID. The pin request requires user interaction and is therefore // not guaranteed to resolve. We want to restore the AUMID as soon // as possible to prevent potential adverse interactions with // Windows APIs reliant on the process AUMID. Additionally this // livelocks following attempts to pin to the taskbar because we // synchronize attempts to change the process AUMID, as might occur // when setting up several web apps in succession. // // If in the future we want to ensure AUMID is safe to reset, we // could try inspecting the "App" application's notification history // to observe when the pin prompt is shown. moz_task::spawn_local("WinRT Pin Defer AUMID Restore", async { use std::time::Duration; if let Err(e) = async_timer::sleep(Duration::from_millis(100)).await { log::error!("Error delaying before restoring the default AUMID, incorrect app might prompt to pin: {e:?}"); } aumid_holder.restore_aumid(); }) .detach(); } if fire_and_forget { log::info!("Pin via WinRT with fire and forget ran to end."); Ok(PinResult::Unknown) } else if user_confirmed.await? { log::info!("Pin via WinRT affirmed by user."); Ok(PinResult::Pinned) } else { log::info!("Pin via WinRT rejected by user or system."); Ok(PinResult::Rejected) } }.await.map_err(|e: WinError| { log::error!("Error using TaskbarManager API: {e:?}"); nserror::NS_ERROR_UNEXPECTED }) } mod aumid { //! This module provides a scoped override of the current process's //! AppUserModelID (AUMID) that orders concurrent overrides and restores the //! AUMID on drop. use futures::lock::{Mutex, MutexGuard}; use nserror::{NS_ERROR_UNEXPECTED, nsresult}; use nsstring::{nsAString, nsString}; use std::sync::LazyLock; use windows::{ Win32::{ System::Com::CoTaskMemFree, UI::Shell::{ GetCurrentProcessExplicitAppUserModelID, SetCurrentProcessExplicitAppUserModelID, }, }, core::{HSTRING, PCWSTR}, }; static DEFAULT_AUMID: LazyLock, nsresult>> = LazyLock::new(|| { // SAFETY: GetCurrentProcessExplicitAppUserModelID handles pointer // safety directly, and should probably be marked safe. // // PWSTR is only used when GetCurrentProcessExplicitAppUserModelID // succeeds therefore is a non-null and valid. unsafe { GetCurrentProcessExplicitAppUserModelID().map(|aumid| { let hstr = aumid.to_hstring(); CoTaskMemFree(Some(aumid.0 as _)); Mutex::new(hstr) }) } .map_err(|e| { log::error!("Failed to retrieve the current process AUMID: {e:?}"); NS_ERROR_UNEXPECTED }) }); /// Holder for the AUMID Mutex lock to ensure only one task sets the process /// AUMID at a time. pub(super) struct Holder<'a> { default_aumid_lock: MutexGuard<'a, HSTRING>, } impl Holder<'_> { /// Attempts to acquire a lock to set the current process AUMID, then /// set it to the provided AUMID. #[must_use] pub(super) async fn set_aumid(temp_aumid: &nsAString) -> Result { // Block while AUMID is temporarily modified. let default_aumid_lock = DEFAULT_AUMID.as_ref().map_err(|e| *e)?.lock().await; let original_aumid = &*default_aumid_lock; log::info!("Original process AUMID was {original_aumid}, setting it to {temp_aumid}"); let temp_aumid = nsString::from(temp_aumid); // SAFETY: nsString is inherently non-null and null-terminated // therefore valid to construct a PCWSTR from to pass to // SetCurrentProcessExplicitAppUserModelID. unsafe { SetCurrentProcessExplicitAppUserModelID(PCWSTR::from_raw(temp_aumid.as_ptr())) } .map_err(|e| { log::error!("Error setting the process AUMID: {e:?}"); NS_ERROR_UNEXPECTED })?; Ok(Self { default_aumid_lock }) } /// Drops self to trigger the AUMID to revert to the default and release /// the lock to set the default AUMID. pub(super) fn restore_aumid(self) {} } impl Drop for Holder<'_> { /// Restore the default AUMID. fn drop(&mut self) { log::info!("Restoring process AUMID to {:?}", self.default_aumid_lock); // SAFETY: HSTRING deref ensures it returns a null-terminated string // even when empty, which is a valid parameter for // SetCurrentProcessExplicitAppUserModelID. if let Err(e) = unsafe { SetCurrentProcessExplicitAppUserModelID(&*self.default_aumid_lock) } { log::error!("Error restoring AUMID: {e:?}"); } } } }