1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
2  * This Source Code Form is subject to the terms of the Mozilla Public
3  * License, v. 2.0. If a copy of the MPL was not distributed with this
4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 
6 package org.mozilla.gecko;
7 
8 import android.content.ContentResolver;
9 import android.content.Context;
10 import android.content.res.Configuration;
11 import android.database.ContentObserver;
12 import android.hardware.input.InputManager;
13 import android.net.Uri;
14 import android.os.Build;
15 import android.os.Handler;
16 import android.os.Looper;
17 import android.provider.Settings;
18 import androidx.annotation.RequiresApi;
19 import android.util.Log;
20 import android.view.InputDevice;
21 import org.mozilla.gecko.annotation.WrapForJNI;
22 import org.mozilla.gecko.util.InputDeviceUtils;
23 import org.mozilla.gecko.util.ThreadUtils;
24 
25 public class GeckoSystemStateListener
26         implements InputManager.InputDeviceListener {
27     private static final String LOGTAG = "SystemStateListener";
28 
29     private static final GeckoSystemStateListener listenerInstance = new GeckoSystemStateListener();
30 
31     private boolean mInitialized;
32     private ContentObserver mContentObserver;
33     private static Context sApplicationContext;
34     private InputManager mInputManager;
35     private boolean mIsNightMode;
36 
getInstance()37     public static GeckoSystemStateListener getInstance() {
38         return listenerInstance;
39     }
40 
GeckoSystemStateListener()41     private GeckoSystemStateListener() {
42     }
43 
initialize(final Context context)44     public synchronized void initialize(final Context context) {
45         if (mInitialized) {
46             Log.w(LOGTAG, "Already initialized!");
47             return;
48         }
49         mInputManager = (InputManager)
50             context.getSystemService(Context.INPUT_SERVICE);
51         mInputManager.registerInputDeviceListener(listenerInstance, ThreadUtils.getUiHandler());
52 
53         sApplicationContext = context;
54         final ContentResolver contentResolver = sApplicationContext.getContentResolver();
55         final Uri animationSetting = Settings.System.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE);
56         mContentObserver = new ContentObserver(new Handler(Looper.getMainLooper())) {
57             @Override
58             public void onChange(final boolean selfChange) {
59                 onDeviceChanged();
60             }
61         };
62         contentResolver.registerContentObserver(animationSetting, false, mContentObserver);
63 
64         mIsNightMode = (sApplicationContext.getResources().getConfiguration().uiMode &
65             Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
66 
67         mInitialized = true;
68     }
69 
shutdown()70     public synchronized void shutdown() {
71         if (!mInitialized) {
72             Log.w(LOGTAG, "Already shut down!");
73             return;
74         }
75 
76         if (mInputManager != null) {
77             Log.e(LOGTAG, "mInputManager should be valid!");
78             return;
79         }
80 
81         mInputManager.unregisterInputDeviceListener(listenerInstance);
82 
83         final ContentResolver contentResolver = sApplicationContext.getContentResolver();
84         contentResolver.unregisterContentObserver(mContentObserver);
85 
86         mInitialized = false;
87         mInputManager = null;
88         mContentObserver = null;
89     }
90 
91     @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
92     @WrapForJNI(calledFrom = "gecko")
93     /**
94      * For prefers-reduced-motion media queries feature.
95      *
96      * Uses `Settings.Global` which was introduced in API version 17.
97      */
prefersReducedMotion()98     private static boolean prefersReducedMotion() {
99         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
100             return false;
101         }
102 
103         final ContentResolver contentResolver = sApplicationContext.getContentResolver();
104 
105         return Settings.Global.getFloat(contentResolver,
106                                         Settings.Global.ANIMATOR_DURATION_SCALE,
107                                         1) == 0.0f;
108     }
109 
110     /**
111      * For prefers-color-scheme media queries feature.
112      */
isNightMode()113     public boolean isNightMode() {
114         return mIsNightMode;
115     }
116 
updateNightMode(final int newUIMode)117     public void updateNightMode(final int newUIMode) {
118         final boolean isNightMode = (newUIMode & Configuration.UI_MODE_NIGHT_MASK)
119             == Configuration.UI_MODE_NIGHT_YES;
120         if (isNightMode == mIsNightMode) {
121             return;
122         }
123         mIsNightMode = isNightMode;
124         onDeviceChanged();
125     }
126 
127     @WrapForJNI(stubName = "OnDeviceChanged", calledFrom = "any", dispatchTo = "gecko")
nativeOnDeviceChanged()128     private static native void nativeOnDeviceChanged();
129 
onDeviceChanged()130     public static void onDeviceChanged() {
131         if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
132             nativeOnDeviceChanged();
133         } else {
134             GeckoThread.queueNativeCallUntil(
135                     GeckoThread.State.PROFILE_READY, GeckoSystemStateListener.class,
136                     "nativeOnDeviceChanged");
137         }
138     }
139 
notifyDeviceChanged(final int deviceId)140     private void notifyDeviceChanged(final int deviceId) {
141         final InputDevice device = InputDevice.getDevice(deviceId);
142         if (device == null ||
143             !InputDeviceUtils.isPointerTypeDevice(device)) {
144             return;
145         }
146         onDeviceChanged();
147     }
148 
149     @Override
onInputDeviceAdded(final int deviceId)150     public void onInputDeviceAdded(final int deviceId) {
151         notifyDeviceChanged(deviceId);
152     }
153 
154     @Override
onInputDeviceRemoved(final int deviceId)155     public void onInputDeviceRemoved(final int deviceId) {
156         // Call onDeviceChanged directly without checking device source types
157         // since we can no longer get a valid `InputDevice` in the case of
158         // device removal.
159         onDeviceChanged();
160     }
161 
162     @Override
onInputDeviceChanged(final int deviceId)163     public void onInputDeviceChanged(final int deviceId) {
164         notifyDeviceChanged(deviceId);
165     }
166 }
167