/* * Copyright (C) 2016 The CyanogenMod 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 org.cyanogenmod.platform.internal.display; import static cyanogenmod.hardware.LiveDisplayManager.FEATURE_MANAGED_OUTDOOR_MODE; import static cyanogenmod.hardware.LiveDisplayManager.MODE_DAY; import static cyanogenmod.hardware.LiveDisplayManager.MODE_FIRST; import static cyanogenmod.hardware.LiveDisplayManager.MODE_LAST; import static cyanogenmod.hardware.LiveDisplayManager.MODE_OUTDOOR; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Resources; import android.content.res.TypedArray; import android.hardware.display.DisplayManager; import android.net.Uri; import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.PowerManagerInternal; import android.os.Process; import android.os.UserHandle; import android.util.Log; import android.view.Display; import com.android.internal.util.ArrayUtils; import com.android.server.LocalServices; import com.android.server.ServiceThread; import com.android.server.SystemService; import com.android.server.pm.UserContentObserver; import com.android.server.twilight.TwilightListener; import com.android.server.twilight.TwilightManager; import com.android.server.twilight.TwilightState; import org.cyanogenmod.internal.util.QSConstants; import org.cyanogenmod.internal.util.QSUtils; import org.cyanogenmod.platform.internal.R; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.BitSet; import java.util.Iterator; import java.util.List; import cyanogenmod.app.CMContextConstants; import cyanogenmod.app.CMStatusBarManager; import cyanogenmod.app.CustomTile; import cyanogenmod.hardware.ILiveDisplayService; import cyanogenmod.hardware.LiveDisplayConfig; import cyanogenmod.providers.CMSettings; /** * LiveDisplay is an advanced set of features for improving * display quality under various ambient conditions. * * The service is constructed with a set of LiveDisplayFeatures * which provide capabilities such as outdoor mode, night mode, * and calibration. It interacts with CMHardwareService to relay * changes down to the lower layers. */ public class LiveDisplayService extends SystemService { private static final String TAG = "LiveDisplay"; private static final int MSG_MODE_CHANGED = 1; private static final int MSG_DISPLAY_CHANGED = 2; private static final int MSG_LOW_POWER_MODE_CHANGED = 3; private static final int MSG_TWILIGHT_UPDATE = 4; private final Context mContext; private final Handler mHandler; private final ServiceThread mHandlerThread; private DisplayManager mDisplayManager; private ModeObserver mModeObserver; private TwilightManager mTwilightManager; private boolean mInitialized = false; private boolean mAwaitingNudge = true; private boolean mSunset = false; private boolean mLowPowerMode; private int mDisplayState = -1; private final List mFeatures = new ArrayList(); private ColorTemperatureController mCTC; private DisplayHardwareController mDHC; private OutdoorModeController mOMC; private LiveDisplayConfig mConfig; // QS tile private String[] mTileEntries; private String[] mTileDescriptionEntries; private String[] mTileAnnouncementEntries; private String[] mTileValues; private int[] mTileEntryIconRes; private static String ACTION_NEXT_MODE = "cyanogenmod.hardware.NEXT_LIVEDISPLAY_MODE"; private static String EXTRA_NEXT_MODE = "next_mode"; public LiveDisplayService(Context context) { super(context); mContext = context; // We want a slightly higher priority thread to handle these requests mHandlerThread = new ServiceThread(TAG, Process.THREAD_PRIORITY_DISPLAY + 1, false /*allowIo*/); mHandlerThread.start(); mHandler = new LiveDisplayHandler(mHandlerThread.getLooper()); updateCustomTileEntries(); } @Override public void onStart() { if (mContext.getPackageManager().hasSystemFeature( CMContextConstants.Features.LIVEDISPLAY)) { publishBinderService(CMContextConstants.CM_LIVEDISPLAY_SERVICE, mBinder); } else { Log.wtf(TAG, "CM LiveDisplay service started by system server but feature xml not" + " declared. Not publishing binder service!"); } } @Override public void onBootPhase(int phase) { if (phase == PHASE_BOOT_COMPLETED) { mAwaitingNudge = getSunsetCounter() < 1; mDHC = new DisplayHardwareController(mContext, mHandler); mFeatures.add(mDHC); mCTC = new ColorTemperatureController(mContext, mHandler, mDHC); mFeatures.add(mCTC); mOMC = new OutdoorModeController(mContext, mHandler); mFeatures.add(mOMC); // Call onStart of each feature and get it's capabilities final BitSet capabilities = new BitSet(); for (Iterator it = mFeatures.iterator(); it.hasNext();) { final LiveDisplayFeature feature = it.next(); if (feature.onStart()) { feature.getCapabilities(capabilities); } else { it.remove(); } } int defaultMode = mContext.getResources().getInteger( org.cyanogenmod.platform.internal.R.integer.config_defaultLiveDisplayMode); mConfig = new LiveDisplayConfig(capabilities, defaultMode, mCTC.getDefaultDayTemperature(), mCTC.getDefaultNightTemperature(), mOMC.getDefaultAutoOutdoorMode(), mDHC.getDefaultAutoContrast(), mDHC.getDefaultCABC(), mDHC.getDefaultColorEnhancement()); mDisplayManager = (DisplayManager) getContext().getSystemService( Context.DISPLAY_SERVICE); mDisplayManager.registerDisplayListener(mDisplayListener, null); PowerManagerInternal pmi = LocalServices.getService(PowerManagerInternal.class); pmi.registerLowPowerModeObserver(mLowPowerModeListener); mTwilightManager = LocalServices.getService(TwilightManager.class); mTwilightManager.registerListener(mTwilightListener, mHandler); updateTwilight(); updateDisplayState(mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY).getState()); if (mConfig.hasModeSupport()) { mModeObserver = new ModeObserver(mHandler); mModeObserver.update(); mContext.registerReceiver(mNextModeReceiver, new IntentFilter(ACTION_NEXT_MODE)); publishCustomTile(); } mInitialized = true; } } private void updateCustomTileEntries() { Resources res = mContext.getResources(); mTileEntries = res.getStringArray(R.array.live_display_entries); mTileDescriptionEntries = res.getStringArray(R.array.live_display_description); mTileAnnouncementEntries = res.getStringArray(R.array.live_display_announcement); mTileValues = res.getStringArray(R.array.live_display_values); TypedArray typedArray = res.obtainTypedArray(R.array.live_display_drawables); mTileEntryIconRes = new int[typedArray.length()]; for (int i = 0; i < mTileEntryIconRes.length; i++) { mTileEntryIconRes[i] = typedArray.getResourceId(i, 0); } typedArray.recycle(); } private int getCurrentModeIndex() { return ArrayUtils.indexOf(mTileValues, String.valueOf(mModeObserver.getMode())); } private int getNextModeIndex() { int next = getCurrentModeIndex() + 1; if (next >= mTileValues.length) { next = 0; } int nextMode = 0; while (true) { nextMode = Integer.valueOf(mTileValues[next]); // Skip outdoor mode if it's unsupported, and skip the day setting // if it's the same as the off setting if (((!mConfig.hasFeature(MODE_OUTDOOR) || mConfig.hasFeature(FEATURE_MANAGED_OUTDOOR_MODE) && nextMode == MODE_OUTDOOR)) || (mCTC.getDayColorTemperature() == mConfig.getDefaultDayTemperature() && nextMode == MODE_DAY)) { next++; if (next >= mTileValues.length) { next = 0; } } else { break; } } return nextMode; } private void publishCustomTile() { // This action should be performed as system final int userId = UserHandle.myUserId(); long token = Binder.clearCallingIdentity(); try { int idx = getCurrentModeIndex(); final UserHandle user = new UserHandle(userId); final Context resourceContext = QSUtils.getQSTileContext(mContext, userId); CMStatusBarManager statusBarManager = CMStatusBarManager.getInstance(mContext); CustomTile tile = new CustomTile.Builder(resourceContext) .setLabel(mTileEntries[idx]) .setContentDescription(mTileDescriptionEntries[idx]) .setIcon(mTileEntryIconRes[idx]) .setOnLongClickIntent(getCustomTileLongClickPendingIntent()) .setOnClickIntent(getCustomTileNextModePendingIntent()) .shouldCollapsePanel(false) .build(); statusBarManager.publishTileAsUser(QSConstants.TILE_LIVE_DISPLAY, LiveDisplayService.class.hashCode(), tile, user); } finally { Binder.restoreCallingIdentity(token); } } private void unpublishCustomTile() { // This action should be performed as system final int userId = UserHandle.myUserId(); long token = Binder.clearCallingIdentity(); try { CMStatusBarManager statusBarManager = CMStatusBarManager.getInstance(mContext); statusBarManager.removeTileAsUser(QSConstants.TILE_LIVE_DISPLAY, LiveDisplayService.class.hashCode(), new UserHandle(userId)); } finally { Binder.restoreCallingIdentity(token); } } private PendingIntent getCustomTileNextModePendingIntent() { Intent i = new Intent(ACTION_NEXT_MODE); i.putExtra(EXTRA_NEXT_MODE, getNextModeIndex()); return PendingIntent.getBroadcastAsUser(mContext, 0, i, PendingIntent.FLAG_UPDATE_CURRENT, UserHandle.CURRENT); } private PendingIntent getCustomTileLongClickPendingIntent() { Intent i = new Intent(CMSettings.ACTION_LIVEDISPLAY_SETTINGS); i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); return PendingIntent.getActivityAsUser(mContext, 0, i, PendingIntent.FLAG_UPDATE_CURRENT, null, UserHandle.CURRENT); } private final BroadcastReceiver mNextModeReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { int mode = intent.getIntExtra(EXTRA_NEXT_MODE, mConfig.getDefaultMode()); if (mConfig.hasFeature(mode) && mode >= MODE_FIRST && mode <= MODE_LAST) { putInt(CMSettings.System.DISPLAY_TEMPERATURE_MODE, mode); } } }; private final IBinder mBinder = new ILiveDisplayService.Stub() { @Override public LiveDisplayConfig getConfig() { return mConfig; } @Override public int getMode() { return mModeObserver.getMode(); } @Override public boolean setMode(int mode) { mContext.enforceCallingOrSelfPermission( cyanogenmod.platform.Manifest.permission.MANAGE_LIVEDISPLAY, null); if (mConfig.hasFeature(mode) && mode >= MODE_FIRST && mode <= MODE_LAST) { putInt(CMSettings.System.DISPLAY_TEMPERATURE_MODE, mode); return true; } return false; } @Override public float[] getColorAdjustment() { return mDHC.getColorAdjustment(); } @Override public boolean setColorAdjustment(float[] adj) { mContext.enforceCallingOrSelfPermission( cyanogenmod.platform.Manifest.permission.MANAGE_LIVEDISPLAY, null); return mDHC.setColorAdjustment(adj); } @Override public boolean isAutoContrastEnabled() { return mDHC.isAutoContrastEnabled(); } @Override public boolean setAutoContrastEnabled(boolean enabled) { mContext.enforceCallingOrSelfPermission( cyanogenmod.platform.Manifest.permission.MANAGE_LIVEDISPLAY, null); return mDHC.setAutoContrastEnabled(enabled); } @Override public boolean isCABCEnabled() { return mDHC.isCABCEnabled(); } @Override public boolean setCABCEnabled(boolean enabled) { mContext.enforceCallingOrSelfPermission( cyanogenmod.platform.Manifest.permission.MANAGE_LIVEDISPLAY, null); return mDHC.setCABCEnabled(enabled); } @Override public boolean isColorEnhancementEnabled() { return mDHC.isColorEnhancementEnabled(); } @Override public boolean setColorEnhancementEnabled(boolean enabled) { mContext.enforceCallingOrSelfPermission( cyanogenmod.platform.Manifest.permission.MANAGE_LIVEDISPLAY, null); return mDHC.setColorEnhancementEnabled(enabled); } @Override public boolean isAutomaticOutdoorModeEnabled() { return mOMC.isAutomaticOutdoorModeEnabled(); } @Override public boolean setAutomaticOutdoorModeEnabled(boolean enabled) { mContext.enforceCallingOrSelfPermission( cyanogenmod.platform.Manifest.permission.MANAGE_LIVEDISPLAY, null); return mOMC.setAutomaticOutdoorModeEnabled(enabled); } @Override public int getDayColorTemperature() { return mCTC.getDayColorTemperature(); } @Override public boolean setDayColorTemperature(int temperature) { mContext.enforceCallingOrSelfPermission( cyanogenmod.platform.Manifest.permission.MANAGE_LIVEDISPLAY, null); mCTC.setDayColorTemperature(temperature); return true; } @Override public int getNightColorTemperature() { return mCTC.getNightColorTemperature(); } @Override public boolean setNightColorTemperature(int temperature) { mContext.enforceCallingOrSelfPermission( cyanogenmod.platform.Manifest.permission.MANAGE_LIVEDISPLAY, null); mCTC.setNightColorTemperature(temperature); return true; } @Override public int getColorTemperature() { return mCTC.getColorTemperature(); } @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println(); pw.println("LiveDisplay Service State:"); pw.println(" mMode=" + mModeObserver.getMode()); pw.println(" mDisplayState=" + mDisplayState); pw.println(" mAwaitingNudge=" + mAwaitingNudge); pw.println(" mConfig=" + mConfig.toString()); for (int i = 0; i < mFeatures.size(); i++) { mFeatures.get(i).dump(pw); } } }; // Listener for screen on/off events private final DisplayManager.DisplayListener mDisplayListener = new DisplayManager.DisplayListener() { @Override public void onDisplayAdded(int displayId) { } @Override public void onDisplayRemoved(int displayId) { } @Override public void onDisplayChanged(int displayId) { if (displayId == Display.DEFAULT_DISPLAY) { mHandler.obtainMessage(MSG_DISPLAY_CHANGED, mDisplayManager.getDisplay(displayId).getState(), 0).sendToTarget(); } } }; // Display postprocessing can have power impact. private PowerManagerInternal.LowPowerModeListener mLowPowerModeListener = new PowerManagerInternal.LowPowerModeListener() { @Override public void onLowPowerModeChanged(boolean lowPowerMode) { if (lowPowerMode != mLowPowerMode) { mLowPowerMode = lowPowerMode; mHandler.obtainMessage(MSG_LOW_POWER_MODE_CHANGED, (lowPowerMode ? 1 : 0), 0).sendToTarget(); } } }; // Watch for mode changes private final class ModeObserver extends UserContentObserver { private final Uri MODE_SETTING = CMSettings.System.getUriFor(CMSettings.System.DISPLAY_TEMPERATURE_MODE); ModeObserver(Handler handler) { super(handler); final ContentResolver cr = mContext.getContentResolver(); cr.registerContentObserver(MODE_SETTING, false, this, UserHandle.USER_ALL); observe(); } @Override protected void update() { mHandler.obtainMessage(MSG_MODE_CHANGED, getMode(), 0).sendToTarget(); publishCustomTile(); } int getMode() { return getInt(CMSettings.System.DISPLAY_TEMPERATURE_MODE, mConfig.getDefaultMode()); } } // Night watchman private final TwilightListener mTwilightListener = new TwilightListener() { @Override public void onTwilightStateChanged() { mHandler.obtainMessage(MSG_TWILIGHT_UPDATE, mTwilightManager.getCurrentState()).sendToTarget(); } }; private int getSunsetCounter() { // Counter used to determine when we should tell the user about this feature. // If it's not used after 3 sunsets, we'll show the hint once. return CMSettings.System.getIntForUser(mContext.getContentResolver(), CMSettings.System.LIVE_DISPLAY_HINTED, -3, UserHandle.USER_CURRENT); } private void updateSunsetCounter(int count) { CMSettings.System.putIntForUser(mContext.getContentResolver(), CMSettings.System.LIVE_DISPLAY_HINTED, count, UserHandle.USER_CURRENT); mAwaitingNudge = count > 0; } private void stopNudgingMe() { if (mAwaitingNudge) { updateSunsetCounter(1); } } /** * Show a friendly notification to the user about the potential benefits of decreasing * blue light at night. Do this only once if the feature has not been used after * three sunsets. It would be great to enable this by default, but we don't want * the change of screen color to be considered a "bug" by a user who doesn't * understand what's happening. * * @param state */ private void nudge() { final TwilightState twilight = mTwilightManager.getCurrentState(); if (!mAwaitingNudge || twilight == null) { return; } int counter = getSunsetCounter(); // check if we should send the hint only once after sunset boolean transition = twilight.isNight() && !mSunset; mSunset = twilight.isNight(); if (!transition) { return; } if (counter <= 0) { counter++; updateSunsetCounter(counter); } if (counter == 0) { //show the notification and don't come back here final Intent intent = new Intent(CMSettings.ACTION_LIVEDISPLAY_SETTINGS); PendingIntent result = PendingIntent.getActivity( mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); Notification.Builder builder = new Notification.Builder(mContext) .setContentTitle(mContext.getResources().getString( org.cyanogenmod.platform.internal.R.string.live_display_title)) .setContentText(mContext.getResources().getString( org.cyanogenmod.platform.internal.R.string.live_display_hint)) .setSmallIcon(org.cyanogenmod.platform.internal.R.drawable.ic_livedisplay_notif) .setStyle(new Notification.BigTextStyle().bigText(mContext.getResources() .getString( org.cyanogenmod.platform.internal.R.string.live_display_hint))) .setContentIntent(result) .setAutoCancel(true); NotificationManager nm = (NotificationManager)mContext.getSystemService(Context.NOTIFICATION_SERVICE); nm.notifyAsUser(null, 1, builder.build(), UserHandle.CURRENT); updateSunsetCounter(1); } } private int getInt(String setting, int defValue) { return CMSettings.System.getIntForUser(mContext.getContentResolver(), setting, defValue, UserHandle.USER_CURRENT); } private void putInt(String setting, int value) { CMSettings.System.putIntForUser(mContext.getContentResolver(), setting, value, UserHandle.USER_CURRENT); } private synchronized void updateTwilight() { final TwilightState twilight = mTwilightManager.getCurrentState(); for (int i = 0; i < mFeatures.size(); i++) { mFeatures.get(i).onTwilightUpdated(twilight); } } private synchronized void updateDisplayState(int displayState) { if (mDisplayState != displayState) { mDisplayState = displayState; for (int i = 0; i < mFeatures.size(); i++) { mFeatures.get(i).onDisplayStateChanged(displayState == Display.STATE_ON); } } } private synchronized void updateMode(int mode) { for (int i = 0; i < mFeatures.size(); i++) { mFeatures.get(i).onModeChanged(mode); } } private synchronized void updateLowPowerMode(boolean lowPowerMode) { if (mLowPowerMode != lowPowerMode) { mLowPowerMode = lowPowerMode; for (int i = 0; i < mFeatures.size(); i++) { mFeatures.get(i).onLowPowerModeChanged(mLowPowerMode); } } } private final class LiveDisplayHandler extends Handler { public LiveDisplayHandler(Looper looper) { super(looper, null, true /*async*/); } @Override public void handleMessage(Message msg) { if (!mInitialized) { return; } switch (msg.what) { case MSG_DISPLAY_CHANGED: updateDisplayState(msg.arg1); break; case MSG_LOW_POWER_MODE_CHANGED: updateLowPowerMode(msg.arg1 == 1); break; case MSG_TWILIGHT_UPDATE: updateTwilight(); nudge(); break; case MSG_MODE_CHANGED: stopNudgingMe(); updateMode(msg.arg1); break; } } } }