/* * Copyright (C) 2015 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.server.telecom; import android.annotation.NonNull; import android.app.UiModeManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.net.Uri; import android.telecom.Log; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; /** * Provides various system states to the rest of the telecom codebase. */ public class SystemStateHelper implements UiModeManager.OnProjectionStateChangedListener { public interface SystemStateListener { /** * Listener method to inform interested parties when a package name requests to enter or * exit car mode. * @param priority the priority of the enter/exit request. * @param packageName the package name of the requester. * @param isCarMode {@code true} if the package is entering car mode, {@code false} * otherwise. */ void onCarModeChanged(int priority, String packageName, boolean isCarMode); /** * Listener method to inform interested parties when a package has set automotive projection * state. * @param automotiveProjectionPackage the package that set automotive projection. */ void onAutomotiveProjectionStateSet(String automotiveProjectionPackage); /** * Listener method to inform interested parties when automotive projection state has been * cleared. */ void onAutomotiveProjectionStateReleased(); /** * Notifies when a package has been uninstalled. * @param packageName the package name of the uninstalled package */ void onPackageUninstalled(String packageName); } private final Context mContext; private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { Log.startSession("SSH.oR"); try { synchronized (mLock) { String action = intent.getAction(); if (UiModeManager.ACTION_ENTER_CAR_MODE_PRIORITIZED.equals(action)) { int priority = intent.getIntExtra(UiModeManager.EXTRA_PRIORITY, UiModeManager.DEFAULT_PRIORITY); String callingPackage = intent.getStringExtra( UiModeManager.EXTRA_CALLING_PACKAGE); Log.i(SystemStateHelper.this, "ENTER_CAR_MODE_PRIORITIZED; priority=%d, pkg=%s", priority, callingPackage); onEnterCarMode(priority, callingPackage); } else if (UiModeManager.ACTION_EXIT_CAR_MODE_PRIORITIZED.equals(action)) { int priority = intent.getIntExtra(UiModeManager.EXTRA_PRIORITY, UiModeManager.DEFAULT_PRIORITY); String callingPackage = intent.getStringExtra( UiModeManager.EXTRA_CALLING_PACKAGE); Log.i(SystemStateHelper.this, "EXIT_CAR_MODE_PRIORITIZED; priority=%d, pkg=%s", priority, callingPackage); onExitCarMode(priority, callingPackage); } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) { Uri data = intent.getData(); if (data == null) { Log.w(SystemStateHelper.this, "Got null data for package removed, ignoring"); return; } mListeners.forEach( l -> l.onPackageUninstalled(data.getEncodedSchemeSpecificPart())); } else { Log.w(SystemStateHelper.this, "Unexpected intent received: %s", intent.getAction()); } } } finally { Log.endSession(); } } }; @Override public void onProjectionStateChanged(int activeProjectionTypes, @NonNull Set projectingPackages) { Log.startSession("SSH.oPSC"); try { synchronized (mLock) { if (projectingPackages.isEmpty()) { onReleaseAutomotiveProjection(); } else { onSetAutomotiveProjection(projectingPackages.iterator().next()); } } } finally { Log.endSession(); } } private Set mListeners = new CopyOnWriteArraySet<>(); private boolean mIsCarModeOrProjectionActive; private final TelecomSystem.SyncRoot mLock; public SystemStateHelper(Context context, TelecomSystem.SyncRoot lock) { mContext = context; mLock = lock; IntentFilter intentFilter1 = new IntentFilter( UiModeManager.ACTION_ENTER_CAR_MODE_PRIORITIZED); intentFilter1.addAction(UiModeManager.ACTION_EXIT_CAR_MODE_PRIORITIZED); IntentFilter intentFilter2 = new IntentFilter(Intent.ACTION_PACKAGE_REMOVED); intentFilter2.addDataScheme("package"); mContext.registerReceiver(mBroadcastReceiver, intentFilter1); mContext.registerReceiver(mBroadcastReceiver, intentFilter2); Log.i(this, "Registering broadcast receiver: %s", intentFilter1); Log.i(this, "Registering broadcast receiver: %s", intentFilter2); mContext.getSystemService(UiModeManager.class).addOnProjectionStateChangedListener( UiModeManager.PROJECTION_TYPE_AUTOMOTIVE, mContext.getMainExecutor(), this); mIsCarModeOrProjectionActive = getSystemCarModeOrProjectionState(); } public void addListener(SystemStateListener listener) { if (listener != null) { mListeners.add(listener); } } public boolean removeListener(SystemStateListener listener) { return mListeners.remove(listener); } public boolean isCarModeOrProjectionActive() { return mIsCarModeOrProjectionActive; } public boolean isDeviceAtEar() { return isDeviceAtEar(mContext); } /** * Returns a guess whether the phone is up to the user's ear. Use the proximity sensor and * the gravity sensor to make a guess * @return true if the proximity sensor is activated, the magnitude of gravity in directions * parallel to the screen is greater than some configurable threshold, and the * y-component of gravity isn't less than some other configurable threshold. */ public static boolean isDeviceAtEar(Context context) { SensorManager sm = context.getSystemService(SensorManager.class); if (sm == null) { return false; } Sensor grav = sm.getDefaultSensor(Sensor.TYPE_GRAVITY); Sensor proximity = sm.getDefaultSensor(Sensor.TYPE_PROXIMITY); if (grav == null || proximity == null) { return false; } AtomicBoolean result = new AtomicBoolean(true); CountDownLatch gravLatch = new CountDownLatch(1); CountDownLatch proxLatch = new CountDownLatch(1); final double xyGravityThreshold = context.getResources().getFloat( R.dimen.device_on_ear_xy_gravity_threshold); final double yGravityNegativeThreshold = context.getResources().getFloat( R.dimen.device_on_ear_y_gravity_negative_threshold); SensorEventListener listener = new SensorEventListener() { @Override public void onSensorChanged(SensorEvent event) { if (event.sensor.getType() == Sensor.TYPE_GRAVITY) { if (gravLatch.getCount() == 0) { return; } double xyMag = Math.sqrt(event.values[0] * event.values[0] + event.values[1] * event.values[1]); if (xyMag < xyGravityThreshold || event.values[1] < yGravityNegativeThreshold) { result.set(false); } gravLatch.countDown(); } else if (event.sensor.getType() == Sensor.TYPE_PROXIMITY) { if (proxLatch.getCount() == 0) { return; } if (event.values[0] >= proximity.getMaximumRange()) { result.set(false); } proxLatch.countDown(); } } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { } }; try { sm.registerListener(listener, grav, SensorManager.SENSOR_DELAY_FASTEST); sm.registerListener(listener, proximity, SensorManager.SENSOR_DELAY_FASTEST); boolean accelValid = gravLatch.await(100, TimeUnit.MILLISECONDS); boolean proxValid = proxLatch.await(100, TimeUnit.MILLISECONDS); if (accelValid && proxValid) { return result.get(); } else { Log.w(SystemStateHelper.class.getSimpleName(), "Timed out waiting for sensors: %b %b", accelValid, proxValid); return false; } } catch (InterruptedException e) { return false; } finally { sm.unregisterListener(listener); } } private void onEnterCarMode(int priority, String packageName) { Log.i(this, "Entering carmode"); mIsCarModeOrProjectionActive = getSystemCarModeOrProjectionState(); for (SystemStateListener listener : mListeners) { listener.onCarModeChanged(priority, packageName, true /* isCarMode */); } } private void onExitCarMode(int priority, String packageName) { Log.i(this, "Exiting carmode"); mIsCarModeOrProjectionActive = getSystemCarModeOrProjectionState(); for (SystemStateListener listener : mListeners) { listener.onCarModeChanged(priority, packageName, false /* isCarMode */); } } private void onSetAutomotiveProjection(String packageName) { Log.i(this, "Automotive projection set."); mIsCarModeOrProjectionActive = getSystemCarModeOrProjectionState(); for (SystemStateListener listener : mListeners) { listener.onAutomotiveProjectionStateSet(packageName); } } private void onReleaseAutomotiveProjection() { Log.i(this, "Automotive projection released."); mIsCarModeOrProjectionActive = getSystemCarModeOrProjectionState(); for (SystemStateListener listener : mListeners) { listener.onAutomotiveProjectionStateReleased(); } } /** * Checks the system for the current car projection state. * * @return True if projection is active, false otherwise. */ private boolean getSystemCarModeOrProjectionState() { UiModeManager uiModeManager = mContext.getSystemService(UiModeManager.class); if (uiModeManager != null) { return uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR || (uiModeManager.getActiveProjectionTypes() & UiModeManager.PROJECTION_TYPE_AUTOMOTIVE) != 0; } Log.w(this, "Got null UiModeManager, returning false."); return false; } }