/* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.car.user; import static android.car.hardware.power.CarPowerManager.CarPowerStateListener; import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO; import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.AppOpsManager; import android.car.CarNotConnectedException; import android.car.hardware.power.CarPowerManager; import android.car.settings.CarSettings; import android.car.user.CarUserManager; import android.car.user.CarUserManager.UserLifecycleListener; import android.car.user.IUserNotice; import android.car.user.IUserNoticeUI; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.pm.PackageManager; import android.content.res.Resources; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.PowerManager; import android.os.RemoteException; import android.os.UserHandle; import android.provider.Settings; import android.util.IndentingPrintWriter; import android.util.Slog; import android.view.IWindowManager; import android.view.WindowManagerGlobal; import com.android.car.CarLocalServices; import com.android.car.CarLog; import com.android.car.CarServiceBase; import com.android.car.R; import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; /** * Service to show initial notice UI to user. It only launches it when setting is enabled and * it is up to notice UI (=Service) to dismiss itself upon user's request. * *

Conditions to show notice UI are: *

    *
  1. Cold boot *
  2. Car power state change to ON (happens in wakeup from suspend to RAM) *
*/ public final class CarUserNoticeService implements CarServiceBase { private static final boolean DBG = false; private static final String TAG = CarLog.tagFor(CarUserNoticeService.class); // Keyguard unlocking can be only polled as we cannot dismiss keyboard. // Polling will stop when keyguard is unlocked. private static final long KEYGUARD_POLLING_INTERVAL_MS = 100; private final Context mContext; // null means feature disabled. @Nullable private final Intent mServiceIntent; private final Handler mMainHandler; private final Object mLock = new Object(); // This one records if there is a service bound. This will be cleared as soon as service is // unbound (=UI dismissed) @GuardedBy("mLock") private boolean mServiceBound = false; // This one represents if UI is shown for the current session. This should be kept until // next event to show UI comes up. @GuardedBy("mLock") private boolean mUiShown = false; @GuardedBy("mLock") @UserIdInt private int mUserId = UserHandle.USER_NULL; @GuardedBy("mLock") private CarPowerManager mCarPowerManager; @GuardedBy("mLock") private IUserNoticeUI mUiService; @GuardedBy("mLock") @UserIdInt private int mIgnoreUserId = UserHandle.USER_NULL; private final UserLifecycleListener mUserLifecycleListener = event -> { if (DBG) Slog.d(TAG, "onEvent(" + event + ")"); if (CarUserManager.USER_LIFECYCLE_EVENT_TYPE_SWITCHING == event.getEventType()) { int userId = event.getUserId(); if (DBG) Slog.d(TAG, "User switch event received. Target User:" + userId); CarUserNoticeService.this.mMainHandler.post(() -> { stopUi(/* clearUiShown= */ true); synchronized (mLock) { // This should be the only place to change user mUserId = userId; } startNoticeUiIfNecessary(); }); } }; private final CarPowerStateListener mPowerStateListener = new CarPowerStateListener() { @Override public void onStateChanged(int state) { if (state == CarPowerManager.CarPowerStateListener.SHUTDOWN_PREPARE) { mMainHandler.post(() -> stopUi(/* clearUiShown= */ true)); } else if (state == CarPowerManager.CarPowerStateListener.ON) { // Only ON can be relied on as car can restart while in garage mode. mMainHandler.post(() -> startNoticeUiIfNecessary()); } } }; private final BroadcastReceiver mDisplayBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { // Runs in main thread, so do not use Handler. if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) { if (isDisplayOn()) { Slog.i(TAG, "SCREEN_OFF while display is already on"); return; } Slog.i(TAG, "Display off, stopping UI"); stopUi(/* clearUiShown= */ true); } else if (Intent.ACTION_SCREEN_ON.equals(intent.getAction())) { if (!isDisplayOn()) { Slog.i(TAG, "SCREEN_ON while display is already off"); return; } Slog.i(TAG, "Display on, starting UI"); startNoticeUiIfNecessary(); } } }; private final IUserNotice.Stub mIUserNotice = new IUserNotice.Stub() { @Override public void onDialogDismissed() { mMainHandler.post(() -> stopUi(/* clearUiShown= */ false)); } }; private final ServiceConnection mUiServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { synchronized (mLock) { if (!mServiceBound) { // already unbound but passed due to timing. This should be just ignored. return; } } IUserNoticeUI binder = IUserNoticeUI.Stub.asInterface(service); try { binder.setCallbackBinder(mIUserNotice); } catch (RemoteException e) { Slog.w(TAG, "UserNoticeUI Service died", e); // Wait for reconnect binder = null; } synchronized (mLock) { mUiService = binder; } } @Override public void onServiceDisconnected(ComponentName name) { // UI crashed. Stop it so that it does not come again. stopUi(/* clearUiShown= */ true); } }; // added for debugging purpose @GuardedBy("mLock") private int mKeyguardPollingCounter; private final Runnable mKeyguardPollingRunnable = () -> { synchronized (mLock) { mKeyguardPollingCounter++; } startNoticeUiIfNecessary(); }; public CarUserNoticeService(Context context) { this(context, new Handler(Looper.getMainLooper())); } @VisibleForTesting CarUserNoticeService(Context context, Handler handler) { mMainHandler = handler; Resources res = context.getResources(); String componentName = res.getString(R.string.config_userNoticeUiService); if (componentName.isEmpty()) { // feature disabled mContext = null; mServiceIntent = null; return; } mContext = context; mServiceIntent = new Intent(); mServiceIntent.setComponent(ComponentName.unflattenFromString(componentName)); } public void ignoreUserNotice(int userId) { synchronized (mLock) { mIgnoreUserId = userId; } } private boolean checkKeyguardLockedWithPolling() { mMainHandler.removeCallbacks(mKeyguardPollingRunnable); IWindowManager wm = WindowManagerGlobal.getWindowManagerService(); boolean locked = true; if (wm != null) { try { locked = wm.isKeyguardLocked(); } catch (RemoteException e) { Slog.w(TAG, "system server crashed", e); } } if (locked) { mMainHandler.postDelayed(mKeyguardPollingRunnable, KEYGUARD_POLLING_INTERVAL_MS); } return locked; } private boolean isNoticeScreenEnabledInSetting(@UserIdInt int userId) { return Settings.Secure.getIntForUser(mContext.getContentResolver(), CarSettings.Secure.KEY_ENABLE_INITIAL_NOTICE_SCREEN_TO_USER, 1 /*enable by default*/, userId) == 1; } private boolean isDisplayOn() { PowerManager pm = mContext.getSystemService(PowerManager.class); if (pm == null) { return false; } return pm.isInteractive(); } private boolean grantSystemAlertWindowPermission(@UserIdInt int userId) { AppOpsManager appOpsManager = mContext.getSystemService(AppOpsManager.class); if (appOpsManager == null) { Slog.w(TAG, "AppOpsManager not ready yet"); return false; } String packageName = mServiceIntent.getComponent().getPackageName(); int packageUid; try { packageUid = mContext.getPackageManager().getPackageUidAsUser(packageName, userId); } catch (PackageManager.NameNotFoundException e) { Slog.wtf(TAG, "Target package for config_userNoticeUiService not found:" + packageName + " userId:" + userId); return false; } appOpsManager.setMode(AppOpsManager.OP_SYSTEM_ALERT_WINDOW, packageUid, packageName, AppOpsManager.MODE_ALLOWED); Slog.i(TAG, "Granted SYSTEM_ALERT_WINDOW permission to package:" + packageName + " package uid:" + packageUid); return true; } private void startNoticeUiIfNecessary() { int userId; synchronized (mLock) { if (mUiShown || mServiceBound) { if (DBG) { Slog.d(TAG, "Notice UI not necessary: mUiShown " + mUiShown + " mServiceBound " + mServiceBound); } return; } userId = mUserId; if (mIgnoreUserId == userId) { if (DBG) { Slog.d(TAG, "Notice UI not necessary: mIgnoreUserId " + mIgnoreUserId + " userId " + userId); } return; } else { mIgnoreUserId = UserHandle.USER_NULL; } } if (userId == UserHandle.USER_NULL) { if (DBG) Slog.d(TAG, "Notice UI not necessary: userId " + userId); return; } // headless user 0 is ignored. if (userId == UserHandle.USER_SYSTEM) { if (DBG) Slog.d(TAG, "Notice UI not necessary: userId " + userId); return; } if (!isNoticeScreenEnabledInSetting(userId)) { if (DBG) { Slog.d(TAG, "Notice UI not necessary as notice screen not enabled in settings."); } return; } if (userId != ActivityManager.getCurrentUser()) { if (DBG) { Slog.d(TAG, "Notice UI not necessary as user has switched. will be handled by user" + " switch callback."); } return; } // Dialog can be not shown if display is off. // DISPLAY_ON broadcast will handle this later. if (!isDisplayOn()) { if (DBG) Slog.d(TAG, "Notice UI not necessary as display is off."); return; } // Do not show it until keyguard is dismissed. if (checkKeyguardLockedWithPolling()) { if (DBG) Slog.d(TAG, "Notice UI not necessary as keyguard is not dismissed."); return; } if (!grantSystemAlertWindowPermission(userId)) { if (DBG) { Slog.d(TAG, "Notice UI not necessary as System Alert Window Permission not" + " granted."); } return; } boolean bound = mContext.bindServiceAsUser(mServiceIntent, mUiServiceConnection, Context.BIND_AUTO_CREATE, UserHandle.of(userId)); if (bound) { Slog.i(TAG, "Bound UserNoticeUI Service: " + mServiceIntent); synchronized (mLock) { mServiceBound = true; mUiShown = true; } } else { Slog.w(TAG, "Cannot bind to UserNoticeUI Service Service" + mServiceIntent); } } private void stopUi(boolean clearUiShown) { mMainHandler.removeCallbacks(mKeyguardPollingRunnable); boolean serviceBound; synchronized (mLock) { mUiService = null; serviceBound = mServiceBound; mServiceBound = false; if (clearUiShown) { mUiShown = false; } } if (serviceBound) { Slog.i(TAG, "Unbound UserNoticeUI Service"); mContext.unbindService(mUiServiceConnection); } } @Override public void init() { if (mServiceIntent == null) { // feature disabled return; } CarPowerManager carPowerManager; synchronized (mLock) { mCarPowerManager = CarLocalServices.createCarPowerManager(mContext); carPowerManager = mCarPowerManager; } try { carPowerManager.setListener(mPowerStateListener); } catch (CarNotConnectedException e) { // should not happen throw new RuntimeException("CarNotConnectedException from CarPowerManager", e); } CarUserService userService = CarLocalServices.getService(CarUserService.class); userService.addUserLifecycleListener(mUserLifecycleListener); IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(Intent.ACTION_SCREEN_OFF); intentFilter.addAction(Intent.ACTION_SCREEN_ON); mContext.registerReceiver(mDisplayBroadcastReceiver, intentFilter); } @Override public void release() { if (mServiceIntent == null) { // feature disabled return; } mContext.unregisterReceiver(mDisplayBroadcastReceiver); CarUserService userService = CarLocalServices.getService(CarUserService.class); userService.removeUserLifecycleListener(mUserLifecycleListener); CarPowerManager carPowerManager; synchronized (mLock) { carPowerManager = mCarPowerManager; mUserId = UserHandle.USER_NULL; } carPowerManager.clearListener(); stopUi(/* clearUiShown= */ true); } @Override @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) public void dump(IndentingPrintWriter writer) { synchronized (mLock) { if (mServiceIntent == null) { writer.println("*CarUserNoticeService* disabled"); return; } if (mUserId == UserHandle.USER_NULL) { writer.println("*CarUserNoticeService* User not started yet."); return; } writer.println("*CarUserNoticeService* mServiceIntent:" + mServiceIntent + ", mUserId:" + mUserId + ", mUiShown:" + mUiShown + ", mServiceBound:" + mServiceBound + ", mKeyguardPollingCounter:" + mKeyguardPollingCounter + ", Setting enabled:" + isNoticeScreenEnabledInSetting(mUserId) + ", Ignore User: " + mIgnoreUserId); } } }