diff --git a/msmnile.mk b/msmnile.mk
index 2580c56..98c3483 100644
--- a/msmnile.mk
+++ b/msmnile.mk
@@ -142,6 +142,10 @@ PRODUCT_PACKAGES += \
android.hardware.ir@1.0-impl \
android.hardware.ir@1.0-service
+# Device-specific settings
+PRODUCT_PACKAGES += \
+ XiaomiParts
+
# Display
PRODUCT_PACKAGES += \
android.hardware.graphics.composer@2.4-impl \
diff --git a/parts/Android.bp b/parts/Android.bp
new file mode 100644
index 0000000..3967abc
--- /dev/null
+++ b/parts/Android.bp
@@ -0,0 +1,25 @@
+//
+// Copyright (C) 2017-2022 The LineageOS Project
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+android_app {
+ name: "XiaomiParts",
+
+ srcs: ["src/**/*.java"],
+ resource_dirs: ["res"],
+ certificate: "platform",
+ platform_apis: true,
+ system_ext_specific: true,
+ privileged: true,
+
+ static_libs: [
+ "org.lineageos.settings.resources",
+ "//hardware/xiaomi:vendor.xiaomi.hardware.motor-V1.0-java",
+ ],
+
+ optimize: {
+ proguard_flags_files: ["proguard.flags"],
+ },
+}
diff --git a/parts/AndroidManifest.xml b/parts/AndroidManifest.xml
new file mode 100644
index 0000000..4e78b11
--- /dev/null
+++ b/parts/AndroidManifest.xml
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/parts/proguard.flags b/parts/proguard.flags
new file mode 100644
index 0000000..0abe5d3
--- /dev/null
+++ b/parts/proguard.flags
@@ -0,0 +1,7 @@
+-keep class org.lineageos.settings.doze.* {
+ *;
+}
+
+-keep class org.lineageos.settings.popupcamera.* {
+ *;
+}
diff --git a/parts/res/drawable/ic_hand.xml b/parts/res/drawable/ic_hand.xml
new file mode 100644
index 0000000..5f56669
--- /dev/null
+++ b/parts/res/drawable/ic_hand.xml
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/parts/res/drawable/ic_pickup.xml b/parts/res/drawable/ic_pickup.xml
new file mode 100644
index 0000000..a7d5eb7
--- /dev/null
+++ b/parts/res/drawable/ic_pickup.xml
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/parts/res/drawable/ic_pocket.xml b/parts/res/drawable/ic_pocket.xml
new file mode 100644
index 0000000..44ae6e2
--- /dev/null
+++ b/parts/res/drawable/ic_pocket.xml
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/parts/res/drawable/ic_settings_popup.xml b/parts/res/drawable/ic_settings_popup.xml
new file mode 100644
index 0000000..d593eb2
--- /dev/null
+++ b/parts/res/drawable/ic_settings_popup.xml
@@ -0,0 +1,11 @@
+
+
+
+
diff --git a/parts/res/values/arrays.xml b/parts/res/values/arrays.xml
new file mode 100644
index 0000000..9f3ac5e
--- /dev/null
+++ b/parts/res/values/arrays.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+ - popup_muqin_up.ogg
+ - popup_muqin_down.ogg
+ - popup_yingyan_up.ogg
+ - popup_yingyan_down.ogg
+ - popup_mofa_up.ogg
+ - popup_mofa_down.ogg
+ - popup_jijia_up.ogg
+ - popup_jijia_down.ogg
+ - popup_chilun_up.ogg
+ - popup_chilun_down.ogg
+ - popup_cangmen_up.ogg
+ - popup_cangmen_down.ogg
+
+
+
+ - @string/action_none
+ - @string/popup_title_muqin
+ - @string/popup_title_yingyan
+ - @string/popup_title_mofa
+ - @string/popup_title_jijia
+ - @string/popup_title_chilun
+ - @string/popup_title_cangmen
+
+
+
+ - -1
+ - 0
+ - 2
+ - 4
+ - 6
+ - 8
+ - 10
+
+
diff --git a/parts/res/values/strings.xml b/parts/res/values/strings.xml
new file mode 100644
index 0000000..22e2d35
--- /dev/null
+++ b/parts/res/values/strings.xml
@@ -0,0 +1,43 @@
+
+
+
+
+ Use Ambient display
+
+
+ Camera LED
+ Show LED light when the front camera appears and retracts
+ Front camera sound effects
+ Xylophone
+ Condor
+ Magic
+ Mecha
+ Gearwheel
+ Cabin door
+
+
+ Warning
+ Couldn\'t close front camera multiple times. Try calibrating the camera.
+ Couldn\'t open front camera multiple times. Try calibrating the camera.
+ Front camera cannot be used during calibration.
+ Calibrate
+ Couldn\'t calibrate
+ Calibrated successfully. You can open the front camera now.
+ You\'re opening the front camera too frequently.
+ Couldn\'t close front camera. Try again.
+ Couldn\'t open front camera. Try again.
+
diff --git a/parts/res/xml/doze_settings.xml b/parts/res/xml/doze_settings.xml
new file mode 100644
index 0000000..682efe1
--- /dev/null
+++ b/parts/res/xml/doze_settings.xml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/parts/res/xml/popup_settings.xml b/parts/res/xml/popup_settings.xml
new file mode 100644
index 0000000..16c19c8
--- /dev/null
+++ b/parts/res/xml/popup_settings.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
diff --git a/parts/src/org/lineageos/settings/BootCompletedReceiver.java b/parts/src/org/lineageos/settings/BootCompletedReceiver.java
new file mode 100644
index 0000000..8bd6644
--- /dev/null
+++ b/parts/src/org/lineageos/settings/BootCompletedReceiver.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2015 The CyanogenMod Project
+ * 2017-2020 The LineageOS 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.lineageos.settings;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import org.lineageos.settings.doze.DozeUtils;
+import org.lineageos.settings.popupcamera.PopupCameraUtils;
+
+public class BootCompletedReceiver extends BroadcastReceiver {
+
+ private static final boolean DEBUG = false;
+ private static final String TAG = "XiaomiParts";
+
+ @Override
+ public void onReceive(final Context context, Intent intent) {
+ if (DEBUG) Log.d(TAG, "Received boot completed intent");
+ DozeUtils.checkDozeService(context);
+ PopupCameraUtils.checkPopupCameraService(context);
+ }
+}
diff --git a/parts/src/org/lineageos/settings/doze/DozeService.java b/parts/src/org/lineageos/settings/doze/DozeService.java
new file mode 100644
index 0000000..671cc5d
--- /dev/null
+++ b/parts/src/org/lineageos/settings/doze/DozeService.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2015 The CyanogenMod Project
+ * 2017-2020 The LineageOS 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.lineageos.settings.doze;
+
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.IBinder;
+import android.util.Log;
+
+import org.lineageos.settings.sensors.PickupSensor;
+import org.lineageos.settings.sensors.ProximitySensor;
+
+public class DozeService extends Service {
+ private static final String TAG = "DozeService";
+ private static final boolean DEBUG = false;
+
+ private ProximitySensor mProximitySensor;
+ private PickupSensor mPickupSensor;
+ private BroadcastReceiver mScreenStateReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(Intent.ACTION_SCREEN_ON)) {
+ onDisplayOn();
+ } else if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) {
+ onDisplayOff();
+ }
+ }
+ };
+
+ @Override
+ public void onCreate() {
+ if (DEBUG) Log.d(TAG, "Creating service");
+ mProximitySensor = new ProximitySensor(this);
+ mPickupSensor = new PickupSensor(this);
+
+ IntentFilter screenStateFilter = new IntentFilter();
+ screenStateFilter.addAction(Intent.ACTION_SCREEN_ON);
+ screenStateFilter.addAction(Intent.ACTION_SCREEN_OFF);
+ registerReceiver(mScreenStateReceiver, screenStateFilter);
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (DEBUG) Log.d(TAG, "Starting service");
+ return START_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ if (DEBUG) Log.d(TAG, "Destroying service");
+ super.onDestroy();
+ this.unregisterReceiver(mScreenStateReceiver);
+ mProximitySensor.disable();
+ mPickupSensor.disable();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ private void onDisplayOn() {
+ if (DEBUG) Log.d(TAG, "Display on");
+ if (DozeUtils.isPickUpEnabled(this)) {
+ mPickupSensor.disable();
+ }
+ if (DozeUtils.isHandwaveGestureEnabled(this) ||
+ DozeUtils.isPocketGestureEnabled(this)) {
+ mProximitySensor.disable();
+ }
+ }
+
+ private void onDisplayOff() {
+ if (DEBUG) Log.d(TAG, "Display off");
+ if (DozeUtils.isPickUpEnabled(this)) {
+ mPickupSensor.enable();
+ }
+ if (DozeUtils.isHandwaveGestureEnabled(this) ||
+ DozeUtils.isPocketGestureEnabled(this)) {
+ mProximitySensor.enable();
+ }
+ }
+}
diff --git a/parts/src/org/lineageos/settings/doze/DozeSettingsActivity.java b/parts/src/org/lineageos/settings/doze/DozeSettingsActivity.java
new file mode 100644
index 0000000..213f0b4
--- /dev/null
+++ b/parts/src/org/lineageos/settings/doze/DozeSettingsActivity.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2015-2016 The CyanogenMod Project
+ * 2017,2021 The LineageOS 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.lineageos.settings.doze;
+
+import android.os.Bundle;
+
+import com.android.settingslib.collapsingtoolbar.CollapsingToolbarBaseActivity;
+import com.android.settingslib.collapsingtoolbar.R;
+
+public class DozeSettingsActivity extends CollapsingToolbarBaseActivity {
+
+ private static final String TAG_DOZE = "doze";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ getFragmentManager().beginTransaction().replace(R.id.content_frame,
+ new DozeSettingsFragment(), TAG_DOZE).commit();
+ }
+}
diff --git a/parts/src/org/lineageos/settings/doze/DozeSettingsFragment.java b/parts/src/org/lineageos/settings/doze/DozeSettingsFragment.java
new file mode 100644
index 0000000..3da577c
--- /dev/null
+++ b/parts/src/org/lineageos/settings/doze/DozeSettingsFragment.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2015 The CyanogenMod Project
+ * 2017-2019,2021 The LineageOS 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.lineageos.settings.doze;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.os.Handler;
+import android.widget.Switch;
+
+import androidx.preference.Preference;
+import androidx.preference.PreferenceCategory;
+import androidx.preference.PreferenceFragment;
+import androidx.preference.SwitchPreference;
+
+import com.android.settingslib.widget.MainSwitchPreference;
+import com.android.settingslib.widget.OnMainSwitchChangeListener;
+
+import org.lineageos.settings.R;
+
+public class DozeSettingsFragment extends PreferenceFragment implements
+ Preference.OnPreferenceChangeListener, OnMainSwitchChangeListener {
+
+ private MainSwitchPreference mSwitchBar;
+
+ private SwitchPreference mAlwaysOnDisplayPreference;
+
+ private SwitchPreference mPickUpPreference;
+ private SwitchPreference mHandwavePreference;
+ private SwitchPreference mPocketPreference;
+
+ private Handler mHandler = new Handler();
+
+ @Override
+ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+ addPreferencesFromResource(R.xml.doze_settings);
+
+ SharedPreferences prefs = getActivity().getSharedPreferences("doze_settings",
+ Activity.MODE_PRIVATE);
+ if (savedInstanceState == null && !prefs.getBoolean("first_help_shown", false)) {
+ showHelp();
+ }
+
+ boolean dozeEnabled = DozeUtils.isDozeEnabled(getActivity());
+
+ mSwitchBar = (MainSwitchPreference) findPreference(DozeUtils.DOZE_ENABLE);
+ mSwitchBar.addOnSwitchChangeListener(this);
+ mSwitchBar.setChecked(dozeEnabled);
+
+ mAlwaysOnDisplayPreference = (SwitchPreference) findPreference(DozeUtils.ALWAYS_ON_DISPLAY);
+ mAlwaysOnDisplayPreference.setEnabled(dozeEnabled);
+ mAlwaysOnDisplayPreference.setChecked(DozeUtils.isAlwaysOnEnabled(getActivity()));
+ mAlwaysOnDisplayPreference.setOnPreferenceChangeListener(this);
+
+ PreferenceCategory pickupSensorCategory = (PreferenceCategory) getPreferenceScreen().
+ findPreference(DozeUtils.CATEG_PICKUP_SENSOR);
+ PreferenceCategory proximitySensorCategory = (PreferenceCategory) getPreferenceScreen().
+ findPreference(DozeUtils.CATEG_PROX_SENSOR);
+
+ mPickUpPreference = (SwitchPreference) findPreference(DozeUtils.GESTURE_PICK_UP_KEY);
+ mPickUpPreference.setEnabled(dozeEnabled);
+ mPickUpPreference.setOnPreferenceChangeListener(this);
+
+ mHandwavePreference = (SwitchPreference) findPreference(DozeUtils.GESTURE_HAND_WAVE_KEY);
+ mHandwavePreference.setEnabled(dozeEnabled);
+ mHandwavePreference.setOnPreferenceChangeListener(this);
+
+ mPocketPreference = (SwitchPreference) findPreference(DozeUtils.GESTURE_POCKET_KEY);
+ mPocketPreference.setEnabled(dozeEnabled);
+ mPocketPreference.setOnPreferenceChangeListener(this);
+
+ // Hide proximity sensor related features if the device doesn't support them
+ if (!DozeUtils.getProxCheckBeforePulse(getActivity())) {
+ getPreferenceScreen().removePreference(proximitySensorCategory);
+ }
+
+ // Hide AOD if not supported and set all its dependents otherwise
+ if (!DozeUtils.alwaysOnDisplayAvailable(getActivity())) {
+ getPreferenceScreen().removePreference(mAlwaysOnDisplayPreference);
+ } else {
+ pickupSensorCategory.setDependency(DozeUtils.ALWAYS_ON_DISPLAY);
+ proximitySensorCategory.setDependency(DozeUtils.ALWAYS_ON_DISPLAY);
+ }
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if (DozeUtils.ALWAYS_ON_DISPLAY.equals(preference.getKey())) {
+ DozeUtils.enableAlwaysOn(getActivity(), (Boolean) newValue);
+ }
+
+ mHandler.post(() -> DozeUtils.checkDozeService(getActivity()));
+
+ return true;
+ }
+
+ @Override
+ public void onSwitchChanged(Switch switchView, boolean isChecked) {
+ DozeUtils.enableDoze(getActivity(), isChecked);
+ DozeUtils.checkDozeService(getActivity());
+
+ mSwitchBar.setChecked(isChecked);
+
+ if (!isChecked) {
+ DozeUtils.enableAlwaysOn(getActivity(), false);
+ mAlwaysOnDisplayPreference.setChecked(false);
+ }
+ mAlwaysOnDisplayPreference.setEnabled(isChecked);
+
+ mPickUpPreference.setEnabled(isChecked);
+ mHandwavePreference.setEnabled(isChecked);
+ mPocketPreference.setEnabled(isChecked);
+ }
+
+ private void showHelp() {
+ HelpDialogFragment fragment = new HelpDialogFragment();
+ fragment.show(getFragmentManager(), "help_dialog");
+ }
+
+ private static class HelpDialogFragment extends DialogFragment {
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ return new AlertDialog.Builder(getActivity())
+ .setTitle(R.string.doze_settings_help_title)
+ .setMessage(R.string.doze_settings_help_text)
+ .setNegativeButton(R.string.dialog_ok, (dialog, which) -> dialog.cancel())
+ .create();
+ }
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ getActivity().getSharedPreferences("doze_settings", Activity.MODE_PRIVATE)
+ .edit()
+ .putBoolean("first_help_shown", true)
+ .commit();
+ }
+ }
+}
diff --git a/parts/src/org/lineageos/settings/doze/DozeUtils.java b/parts/src/org/lineageos/settings/doze/DozeUtils.java
new file mode 100644
index 0000000..cc7cff1
--- /dev/null
+++ b/parts/src/org/lineageos/settings/doze/DozeUtils.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2015 The CyanogenMod Project
+ * 2017-2019,2021 The LineageOS 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.lineageos.settings.doze;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.hardware.display.AmbientDisplayConfiguration;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.Log;
+
+import androidx.preference.PreferenceManager;
+
+import static android.provider.Settings.Secure.DOZE_ALWAYS_ON;
+import static android.provider.Settings.Secure.DOZE_ENABLED;
+
+public final class DozeUtils {
+
+ protected static final String DOZE_ENABLE = "doze_enable";
+
+ protected static final String ALWAYS_ON_DISPLAY = "always_on_display";
+ protected static final String CATEG_PICKUP_SENSOR = "pickup_sensor";
+ protected static final String CATEG_PROX_SENSOR = "proximity_sensor";
+ protected static final String GESTURE_PICK_UP_KEY = "gesture_pick_up";
+ protected static final String GESTURE_HAND_WAVE_KEY = "gesture_hand_wave";
+ protected static final String GESTURE_POCKET_KEY = "gesture_pocket";
+ private static final String TAG = "DozeUtils";
+ private static final boolean DEBUG = false;
+ private static final String DOZE_INTENT = "com.android.systemui.doze.pulse";
+
+ public static void startService(Context context) {
+ if (DEBUG) Log.d(TAG, "Starting service");
+ context.startServiceAsUser(new Intent(context, DozeService.class),
+ UserHandle.CURRENT);
+ }
+
+ protected static void stopService(Context context) {
+ if (DEBUG) Log.d(TAG, "Stopping service");
+ context.stopServiceAsUser(new Intent(context, DozeService.class),
+ UserHandle.CURRENT);
+ }
+
+ public static void checkDozeService(Context context) {
+ if (isDozeEnabled(context) && !isAlwaysOnEnabled(context) && sensorsEnabled(context)) {
+ startService(context);
+ } else {
+ stopService(context);
+ }
+ }
+
+ protected static boolean getProxCheckBeforePulse(Context context) {
+ try {
+ Context con = context.createPackageContext("com.android.systemui", 0);
+ int id = con.getResources().getIdentifier("doze_proximity_check_before_pulse",
+ "bool", "com.android.systemui");
+ return con.getResources().getBoolean(id);
+ } catch (PackageManager.NameNotFoundException e) {
+ return false;
+ }
+ }
+
+ protected static boolean enableDoze(Context context, boolean enable) {
+ return Settings.Secure.putInt(context.getContentResolver(),
+ DOZE_ENABLED, enable ? 1 : 0);
+ }
+
+ public static boolean isDozeEnabled(Context context) {
+ return Settings.Secure.getInt(context.getContentResolver(),
+ DOZE_ENABLED, 1) != 0;
+ }
+
+ public static void launchDozePulse(Context context) {
+ if (DEBUG) Log.d(TAG, "Launch doze pulse");
+ context.sendBroadcastAsUser(new Intent(DOZE_INTENT),
+ new UserHandle(UserHandle.USER_CURRENT));
+ }
+
+ protected static boolean enableAlwaysOn(Context context, boolean enable) {
+ return Settings.Secure.putIntForUser(context.getContentResolver(),
+ DOZE_ALWAYS_ON, enable ? 1 : 0, UserHandle.USER_CURRENT);
+ }
+
+ protected static boolean isAlwaysOnEnabled(Context context) {
+ final boolean enabledByDefault = context.getResources()
+ .getBoolean(com.android.internal.R.bool.config_dozeAlwaysOnEnabled);
+
+ return Settings.Secure.getIntForUser(context.getContentResolver(),
+ DOZE_ALWAYS_ON, alwaysOnDisplayAvailable(context) && enabledByDefault ? 1 : 0,
+ UserHandle.USER_CURRENT) != 0;
+ }
+
+ protected static boolean alwaysOnDisplayAvailable(Context context) {
+ return new AmbientDisplayConfiguration(context).alwaysOnAvailable();
+ }
+
+ protected static boolean isGestureEnabled(Context context, String gesture) {
+ return PreferenceManager.getDefaultSharedPreferences(context)
+ .getBoolean(gesture, false);
+ }
+
+ public static boolean isPickUpEnabled(Context context) {
+ return isGestureEnabled(context, GESTURE_PICK_UP_KEY);
+ }
+
+ public static boolean isHandwaveGestureEnabled(Context context) {
+ return isGestureEnabled(context, GESTURE_HAND_WAVE_KEY);
+ }
+
+ public static boolean isPocketGestureEnabled(Context context) {
+ return isGestureEnabled(context, GESTURE_POCKET_KEY);
+ }
+
+ public static boolean sensorsEnabled(Context context) {
+ return isPickUpEnabled(context) || isHandwaveGestureEnabled(context)
+ || isPocketGestureEnabled(context);
+ }
+}
diff --git a/parts/src/org/lineageos/settings/popupcamera/Constants.java b/parts/src/org/lineageos/settings/popupcamera/Constants.java
new file mode 100644
index 0000000..1c88c82
--- /dev/null
+++ b/parts/src/org/lineageos/settings/popupcamera/Constants.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2020 The LineageOS 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.lineageos.settings.popupcamera;
+
+public class Constants {
+ public static final int CAMERA_EVENT_DELAY_TIME = 100; // ms
+ public static final int MSG_CAMERA_CLOSED = 1001;
+ public static final int MSG_CAMERA_OPEN = 1002;
+
+ public static final int MOTOR_STATUS_CALIB_ERROR = 18;
+ public static final int MOTOR_STATUS_CALIB_OK = 17;
+ public static final int MOTOR_STATUS_CANCELED = 20;
+ public static final int MOTOR_STATUS_DROPPED = 16;
+ public static final int MOTOR_STATUS_INVALID = 10;
+ public static final int MOTOR_STATUS_MAX = 20;
+ public static final int MOTOR_STATUS_MIN = 10;
+ public static final int MOTOR_STATUS_NOT_INIT = -1;
+ public static final int MOTOR_STATUS_POPUP_JAMMED = 12;
+ public static final int MOTOR_STATUS_POPUP_OK = 11;
+ public static final int MOTOR_STATUS_PRESSED = 15;
+ public static final int MOTOR_STATUS_REQUEST_CALIB = 19;
+ public static final int MOTOR_STATUS_TAKEBACK_JAMMED = 14;
+ public static final int MOTOR_STATUS_TAKEBACK_OK = 13;
+
+ public static final String CLOSE_CAMERA_STATE = "0";
+ public static final String OPEN_CAMERA_STATE = "1";
+
+ public static final String FRONT_CAMERA_ID = "1";
+ public static final String BLUE_LED_PATH = "/sys/class/leds/blue/breath";
+ public static final String GREEN_LED_PATH =
+ "/sys/class/leds/green/breath";
+ public static final String RED_LED_PATH = "/sys/class/leds/red/breath";
+ public static final String BLUE_RIGHT_LED_PATH =
+ "/sys/class/leds/blue-right/breath";
+ public static final String GREEN_RIGHT_LED_PATH =
+ "/sys/class/leds/green-right/breath";
+ public static final String RED_RIGHT_LED_PATH =
+ "/sys/class/leds/red-right/breath";
+ public static final String POPUP_SOUND_PATH = "/system_ext/media/audio/ui/";
+}
diff --git a/parts/src/org/lineageos/settings/popupcamera/PopupCameraPreferences.java b/parts/src/org/lineageos/settings/popupcamera/PopupCameraPreferences.java
new file mode 100644
index 0000000..fe5ae33
--- /dev/null
+++ b/parts/src/org/lineageos/settings/popupcamera/PopupCameraPreferences.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2019 The LineageOS 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.lineageos.settings.popupcamera;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+
+public class PopupCameraPreferences {
+
+ private static final String TAG = "PopupCameraUtils";
+ private static final boolean DEBUG = false;
+ private static final String LED_EFFECT_KEY = "popup_led_effect";
+ private static final boolean LED_EFFECT_DEFAULT_VALUE = true;
+ private static final String SOUND_EFFECT_KEY = "popup_sound_effect";
+ private static final String SOUND_EFFECT_DEFAULT_VALUE = "0";
+ private SharedPreferences mSharedPrefs;
+
+ public PopupCameraPreferences(Context context) {
+ mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(context);
+ }
+
+ public String getSoundEffect() {
+ return mSharedPrefs.getString(SOUND_EFFECT_KEY, SOUND_EFFECT_DEFAULT_VALUE);
+ }
+
+ public boolean isLedAllowed() {
+ return mSharedPrefs.getBoolean(LED_EFFECT_KEY, LED_EFFECT_DEFAULT_VALUE);
+ }
+}
diff --git a/parts/src/org/lineageos/settings/popupcamera/PopupCameraService.java b/parts/src/org/lineageos/settings/popupcamera/PopupCameraService.java
new file mode 100644
index 0000000..8c6d9c2
--- /dev/null
+++ b/parts/src/org/lineageos/settings/popupcamera/PopupCameraService.java
@@ -0,0 +1,413 @@
+/*
+ * Copyright (C) 2019 The LineageOS 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.lineageos.settings.popupcamera;
+
+import android.annotation.NonNull;
+import android.app.AlertDialog;
+import android.app.Service;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.hardware.camera2.CameraManager;
+import android.media.AudioAttributes;
+import android.media.SoundPool;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.util.Log;
+import android.view.WindowManager;
+import android.widget.Toast;
+
+import org.lineageos.settings.R;
+import org.lineageos.settings.sensors.ProximitySensor;
+import org.lineageos.settings.sensors.SensorsUtils;
+import org.lineageos.settings.utils.FileUtils;
+
+import vendor.xiaomi.hardware.motor.V1_0.IMotor;
+import vendor.xiaomi.hardware.motor.V1_0.IMotorCallback;
+import vendor.xiaomi.hardware.motor.V1_0.MotorEvent;
+
+import java.util.NoSuchElementException;
+
+public class PopupCameraService extends Service implements Handler.Callback {
+
+ private static final String TAG = "PopupCameraService";
+ private static final boolean DEBUG = false;
+
+ private IMotor mMotor = null;
+
+ private final Object mLock = new Object();
+ private boolean mMotorBusy = false;
+ private long mClosedEvent;
+ private long mOpenEvent;
+ private Handler mHandler = new Handler(this);
+ private boolean mMotorCalibrating = false;
+ private boolean mErrorDialogShowing;
+
+ private PopupCameraPreferences mPopupCameraPreferences;
+
+ private SensorManager mSensorManager;
+ private Sensor mFreeFallSensor;
+ private ProximitySensor mProximitySensor;
+
+ private int[] mSounds;
+ private SoundPool mSoundPool;
+
+ private IMotorCallback mMotorCallback = new IMotorCallback.Stub() {
+ @Override
+ public void onNotify(MotorEvent event) {
+ int status = event.vaalue;
+ int cookie = event.cookie;
+ if (DEBUG)
+ Log.d(TAG, "onNotify: cookie=" + cookie + ",status=" + status);
+ synchronized (mLock) {
+ if (status == Constants.MOTOR_STATUS_CALIB_OK ||
+ status == Constants.MOTOR_STATUS_CALIB_ERROR) {
+ mMotorCalibrating = false;
+ showCalibrationResult(status);
+ } else if (status == Constants.MOTOR_STATUS_PRESSED) {
+ updateMotor(Constants.CLOSE_CAMERA_STATE);
+ goBackHome();
+ } else if (status == Constants.MOTOR_STATUS_POPUP_JAMMED ||
+ status == Constants.MOTOR_STATUS_TAKEBACK_JAMMED) {
+ showErrorDialog();
+ }
+ }
+ }
+ };
+
+ private CameraManager.AvailabilityCallback mAvailabilityCallback =
+ new CameraManager.AvailabilityCallback() {
+ @Override
+ public void onCameraClosed(@NonNull String cameraId) {
+ super.onCameraClosed(cameraId);
+ if (cameraId.equals(Constants.FRONT_CAMERA_ID)) {
+ mClosedEvent = SystemClock.elapsedRealtime();
+ if (SystemClock.elapsedRealtime() - mOpenEvent <
+ Constants.CAMERA_EVENT_DELAY_TIME &&
+ mHandler.hasMessages(Constants.MSG_CAMERA_OPEN)) {
+ mHandler.removeMessages(Constants.MSG_CAMERA_OPEN);
+ }
+ mHandler.sendEmptyMessageDelayed(Constants.MSG_CAMERA_CLOSED,
+ Constants.CAMERA_EVENT_DELAY_TIME);
+ }
+ }
+
+ @Override
+ public void onCameraOpened(@NonNull String cameraId, @NonNull String packageId) {
+ super.onCameraOpened(cameraId, packageId);
+ if (cameraId.equals(Constants.FRONT_CAMERA_ID)) {
+ mOpenEvent = SystemClock.elapsedRealtime();
+ if (SystemClock.elapsedRealtime() - mClosedEvent <
+ Constants.CAMERA_EVENT_DELAY_TIME &&
+ mHandler.hasMessages(Constants.MSG_CAMERA_CLOSED)) {
+ mHandler.removeMessages(Constants.MSG_CAMERA_CLOSED);
+ }
+ mHandler.sendEmptyMessageDelayed(Constants.MSG_CAMERA_OPEN,
+ Constants.CAMERA_EVENT_DELAY_TIME);
+ }
+ }
+ };
+
+ private SensorEventListener mFreeFallListener = new SensorEventListener() {
+ @Override
+ public void onSensorChanged(SensorEvent event) {
+ if (event.values[0] == 2.0f) {
+ updateMotor(Constants.CLOSE_CAMERA_STATE);
+ goBackHome();
+ }
+ }
+
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {
+ }
+ };
+
+ // Service
+ @Override
+ public void onCreate() {
+ CameraManager cameraManager = getSystemService(CameraManager.class);
+ cameraManager.registerAvailabilityCallback(mAvailabilityCallback, null);
+
+ mSensorManager = getSystemService(SensorManager.class);
+ mFreeFallSensor = SensorsUtils.getSensor(mSensorManager, "xiaomi.sensor.free_fall");
+ mProximitySensor = new ProximitySensor(this);
+
+ mPopupCameraPreferences = new PopupCameraPreferences(this);
+
+ mSoundPool = new SoundPool.Builder()
+ .setMaxStreams(1)
+ .setAudioAttributes(new AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+ .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
+ .build())
+ .build();
+ String[] soundNames = getResources().getStringArray(R.array.popupcamera_effects_names);
+ mSounds = new int[soundNames.length];
+ for (int i = 0; i < soundNames.length; i++) {
+ mSounds[i] = mSoundPool.load(Constants.POPUP_SOUND_PATH + soundNames[i], 1);
+ }
+
+ IMotor motor = getMotor();
+ if (motor == null) {
+ return;
+ }
+
+ try {
+ int status = motor.getMotorStatus();
+ if (status == Constants.MOTOR_STATUS_POPUP_OK ||
+ status == Constants.MOTOR_STATUS_POPUP_JAMMED ||
+ status == Constants.MOTOR_STATUS_TAKEBACK_JAMMED) {
+ mHandler.sendEmptyMessage(Constants.MSG_CAMERA_CLOSED);
+ }
+ } catch (RemoteException e) {
+ // Do nothing
+ }
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (DEBUG)
+ Log.d(TAG, "Starting service");
+ IMotor motor = getMotor();
+ if (motor == null) {
+ return START_STICKY;
+ }
+ mProximitySensor.enable();
+ return START_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ if (DEBUG)
+ Log.d(TAG, "Destroying service");
+ IMotor motor = getMotor();
+ if (motor == null) {
+ return;
+ }
+ mProximitySensor.disable();
+ super.onDestroy();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ // Handler.Callback
+ @Override
+ public boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case Constants.MSG_CAMERA_CLOSED: {
+ updateMotor(Constants.CLOSE_CAMERA_STATE);
+ }
+ break;
+ case Constants.MSG_CAMERA_OPEN: {
+ updateMotor(Constants.OPEN_CAMERA_STATE);
+ }
+ break;
+ }
+ return true;
+ }
+
+ static public IMotor getMotorService() {
+ IMotor motor = null;
+ try {
+ motor = IMotor.getService();
+ } catch (NoSuchElementException | RemoteException e) {
+ // Do nothing
+ }
+ return motor;
+ }
+
+ public IMotor getMotor() {
+ if (mMotor == null) {
+ try {
+ mMotor = getMotorService();
+ if (mMotor != null) {
+ mMotor.setMotorCallback(mMotorCallback);
+ mMotor.asBinder().linkToDeath((cookie) -> {
+ mMotor = null;
+ }, 0);
+ }
+ } catch (RemoteException e) {
+ // Do nothing
+ }
+ }
+ return mMotor;
+ }
+
+ private void updateMotor(String cameraState) {
+ IMotor motor = getMotor();
+ if (motor == null || mProximitySensor.getSawNear()) {
+ return;
+ }
+ final Runnable r = () -> {
+ mMotorBusy = true;
+ try {
+ int status = motor.getMotorStatus();
+ if (DEBUG)
+ Log.d(TAG, "updateMotor: status=" + status);
+ if (cameraState.equals(Constants.OPEN_CAMERA_STATE) &&
+ motor.getMotorStatus() == Constants.MOTOR_STATUS_TAKEBACK_OK) {
+ lightUp();
+ playSoundEffect(Constants.OPEN_CAMERA_STATE);
+ motor.popupMotor(1);
+ mSensorManager.registerListener(mFreeFallListener, mFreeFallSensor,
+ SensorManager.SENSOR_DELAY_NORMAL);
+ } else if (cameraState.equals(Constants.CLOSE_CAMERA_STATE) &&
+ motor.getMotorStatus() == Constants.MOTOR_STATUS_POPUP_OK) {
+ lightUp();
+ playSoundEffect(Constants.CLOSE_CAMERA_STATE);
+ motor.takebackMotor(1);
+ mSensorManager.unregisterListener(mFreeFallListener, mFreeFallSensor);
+ } else {
+ mMotorBusy = false;
+ if (status == Constants.MOTOR_STATUS_REQUEST_CALIB ||
+ status == Constants.MOTOR_STATUS_POPUP_JAMMED ||
+ status == Constants.MOTOR_STATUS_TAKEBACK_JAMMED ||
+ status == Constants.MOTOR_STATUS_CALIB_ERROR) {
+ showErrorDialog();
+ }
+ return;
+ }
+ } catch (RemoteException e) {
+ // Do nothing
+ }
+ mHandler.postDelayed(() -> mMotorBusy = false, 1200);
+ };
+
+ if (mMotorBusy) {
+ mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (mMotorBusy) {
+ mHandler.postDelayed(this, 100);
+ } else {
+ mHandler.post(r);
+ }
+ }
+ }, 100);
+ } else {
+ mHandler.post(r);
+ }
+ }
+
+ private void lightUp() {
+ if (mPopupCameraPreferences.isLedAllowed()) {
+ FileUtils.writeLine(Constants.RED_LED_PATH, "1");
+ FileUtils.writeLine(Constants.GREEN_LED_PATH, "1");
+ FileUtils.writeLine(Constants.BLUE_LED_PATH, "1");
+ FileUtils.writeLine(Constants.RED_RIGHT_LED_PATH, "1");
+ FileUtils.writeLine(Constants.GREEN_RIGHT_LED_PATH, "1");
+ FileUtils.writeLine(Constants.BLUE_RIGHT_LED_PATH, "1");
+
+ mHandler.postDelayed(() -> {
+ FileUtils.writeLine(Constants.RED_LED_PATH, "0");
+ FileUtils.writeLine(Constants.GREEN_LED_PATH, "0");
+ FileUtils.writeLine(Constants.BLUE_LED_PATH, "0");
+ FileUtils.writeLine(Constants.RED_RIGHT_LED_PATH, "0");
+ FileUtils.writeLine(Constants.GREEN_RIGHT_LED_PATH, "0");
+ FileUtils.writeLine(Constants.BLUE_RIGHT_LED_PATH, "0");
+ }, 2300);
+ }
+ }
+
+ private void calibrateMotor() {
+ synchronized (mLock) {
+ IMotor motor = getMotor();
+ if (mMotorCalibrating || motor == null)
+ return;
+ try {
+ mMotorCalibrating = true;
+ motor.calibration();
+ } catch (RemoteException e) {
+ // Do nothing
+ }
+ }
+ }
+
+ private void showCalibrationResult(int status) {
+ mHandler.post(() -> {
+ Toast.makeText(PopupCameraService.this,
+ status == Constants.MOTOR_STATUS_CALIB_OK
+ ? R.string.popup_camera_calibrate_success
+ : R.string.popup_camera_calibrate_failed,
+ Toast.LENGTH_LONG)
+ .show();
+ });
+ }
+
+ private void showErrorDialog() {
+ if (mErrorDialogShowing) {
+ return;
+ }
+ mErrorDialogShowing = true;
+ goBackHome();
+ mHandler.post(() -> {
+ Resources res = getResources();
+ String cameraState = "-1";
+ int dialogMessageResId = cameraState.equals(Constants.CLOSE_CAMERA_STATE)
+ ? R.string.popup_camera_takeback_failed_times_calibrate
+ : R.string.popup_camera_popup_failed_times_calibrate;
+ AlertDialog alertDialog = new AlertDialog.Builder(this, R.style.SystemAlertDialogTheme)
+ .setTitle(res.getString(R.string.popup_camera_tip))
+ .setMessage(res.getString(dialogMessageResId))
+ .setPositiveButton(
+ res.getString(R.string.popup_camera_calibrate_now),
+ (dialog, which) -> {
+ calibrateMotor();
+ })
+ .setNegativeButton(res.getString(android.R.string.cancel), null)
+ .create();
+ alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
+ alertDialog.setCanceledOnTouchOutside(false);
+ alertDialog.show();
+ alertDialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
+ @Override
+ public void onDismiss(DialogInterface dialogInterface) {
+ mErrorDialogShowing = false;
+ }
+ });
+ });
+ }
+
+ private void playSoundEffect(String state) {
+ int soundEffect = Integer.parseInt(mPopupCameraPreferences.getSoundEffect());
+ if (soundEffect != -1) {
+ if (state.equals(Constants.CLOSE_CAMERA_STATE)) {
+ soundEffect++;
+ }
+ mSoundPool.play(mSounds[soundEffect], 1.0f, 1.0f, 0, 0, 1.0f);
+ }
+ }
+
+ public void goBackHome() {
+ Intent homeIntent = new Intent(Intent.ACTION_MAIN);
+ homeIntent.addCategory(Intent.CATEGORY_HOME);
+ homeIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivityAsUser(homeIntent, null, UserHandle.CURRENT);
+ }
+}
diff --git a/parts/src/org/lineageos/settings/popupcamera/PopupCameraSettingsActivity.java b/parts/src/org/lineageos/settings/popupcamera/PopupCameraSettingsActivity.java
new file mode 100644
index 0000000..e7a075b
--- /dev/null
+++ b/parts/src/org/lineageos/settings/popupcamera/PopupCameraSettingsActivity.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2015-2016 The CyanogenMod Project
+ * 2017 The LineageOS 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.lineageos.settings.popupcamera;
+
+import android.os.Bundle;
+import android.preference.PreferenceActivity;
+
+public class PopupCameraSettingsActivity extends PreferenceActivity {
+
+ private static final String TAG_POPUPCAMERA = "popupcamera";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ getFragmentManager()
+ .beginTransaction()
+ .replace(android.R.id.content, new PopupCameraSettingsFragment(),
+ TAG_POPUPCAMERA)
+ .commit();
+ }
+}
\ No newline at end of file
diff --git a/parts/src/org/lineageos/settings/popupcamera/PopupCameraSettingsFragment.java b/parts/src/org/lineageos/settings/popupcamera/PopupCameraSettingsFragment.java
new file mode 100644
index 0000000..d6eb7ec
--- /dev/null
+++ b/parts/src/org/lineageos/settings/popupcamera/PopupCameraSettingsFragment.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2018 The LineageOS 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.lineageos.settings.popupcamera;
+
+import android.os.Bundle;
+import android.view.MenuItem;
+
+import androidx.preference.Preference;
+import androidx.preference.Preference.OnPreferenceChangeListener;
+import androidx.preference.PreferenceFragment;
+
+import org.lineageos.settings.R;
+
+public class PopupCameraSettingsFragment
+ extends PreferenceFragment implements OnPreferenceChangeListener {
+
+ @Override
+ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+ addPreferencesFromResource(R.xml.popup_settings);
+ getActivity().getActionBar().setDisplayHomeAsUpEnabled(true);
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ return false;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ getActivity().onBackPressed();
+ return true;
+ }
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/parts/src/org/lineageos/settings/popupcamera/PopupCameraUtils.java b/parts/src/org/lineageos/settings/popupcamera/PopupCameraUtils.java
new file mode 100644
index 0000000..5a4d859
--- /dev/null
+++ b/parts/src/org/lineageos/settings/popupcamera/PopupCameraUtils.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2019 The LineageOS 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.lineageos.settings.popupcamera;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.UserHandle;
+import android.util.Log;
+
+public class PopupCameraUtils {
+
+ private static final String TAG = "PopupCameraUtils";
+ private static final boolean DEBUG = false;
+
+ private static final boolean isPopUpMotorAvailable() {
+ return PopupCameraService.getMotorService() != null;
+ }
+
+ protected static void startService(Context context) {
+ if (DEBUG) Log.d(TAG, "Starting service");
+ context.startServiceAsUser(new Intent(context, PopupCameraService.class),
+ UserHandle.CURRENT);
+ PackageManager pm = context.getPackageManager();
+ pm.setComponentEnabledSetting(
+ new ComponentName(context, PopupCameraSettingsActivity.class),
+ pm.COMPONENT_ENABLED_STATE_ENABLED, pm.SYNCHRONOUS);
+ }
+
+ protected static void stopService(Context context) {
+ if (DEBUG) Log.d(TAG, "Stopping service");
+ context.stopServiceAsUser(new Intent(context, PopupCameraService.class),
+ UserHandle.CURRENT);
+ PackageManager pm = context.getPackageManager();
+ pm.setComponentEnabledSetting(
+ new ComponentName(context, PopupCameraSettingsActivity.class),
+ pm.COMPONENT_ENABLED_STATE_DEFAULT, pm.SYNCHRONOUS);
+ }
+
+ public static void checkPopupCameraService(Context context) {
+ if (isPopUpMotorAvailable()) {
+ startService(context);
+ } else {
+ stopService(context);
+ }
+ }
+}
diff --git a/parts/src/org/lineageos/settings/sensors/PickupSensor.java b/parts/src/org/lineageos/settings/sensors/PickupSensor.java
new file mode 100644
index 0000000..4d9cd61
--- /dev/null
+++ b/parts/src/org/lineageos/settings/sensors/PickupSensor.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2015 The CyanogenMod Project
+ * 2017-2020 The LineageOS 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.lineageos.settings.sensors;
+
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.os.SystemClock;
+import android.util.Log;
+
+import org.lineageos.settings.doze.DozeUtils;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+public class PickupSensor implements SensorEventListener {
+
+ private static final boolean DEBUG = false;
+ private static final String TAG = "PickupSensor";
+
+ private static final int MIN_PULSE_INTERVAL_MS = 2500;
+
+ private SensorManager mSensorManager;
+ private Sensor mSensor;
+ private Context mContext;
+ private ExecutorService mExecutorService;
+
+ private long mEntryTimestamp;
+
+ public PickupSensor(Context context) {
+ mContext = context;
+ mSensorManager = mContext.getSystemService(SensorManager.class);
+ mSensor = SensorsUtils.getSensor(mSensorManager, "xiaomi.sensor.pickup");
+ mExecutorService = Executors.newSingleThreadExecutor();
+ }
+
+ private Future> submit(Runnable runnable) {
+ return mExecutorService.submit(runnable);
+ }
+
+ @Override
+ public void onSensorChanged(SensorEvent event) {
+ if (DEBUG) Log.d(TAG, "Got sensor event: " + event.values[0]);
+
+ long delta = SystemClock.elapsedRealtime() - mEntryTimestamp;
+ if (delta < MIN_PULSE_INTERVAL_MS) {
+ return;
+ }
+
+ mEntryTimestamp = SystemClock.elapsedRealtime();
+
+ if (event.values[0] == 1) {
+ DozeUtils.launchDozePulse(mContext);
+ }
+ }
+
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {
+ /* Empty */
+ }
+
+ public void enable() {
+ if (DEBUG) Log.d(TAG, "Enabling");
+ submit(() -> {
+ mSensorManager.registerListener(this, mSensor,
+ SensorManager.SENSOR_DELAY_NORMAL);
+ mEntryTimestamp = SystemClock.elapsedRealtime();
+ });
+ }
+
+ public void disable() {
+ if (DEBUG) Log.d(TAG, "Disabling");
+ submit(() -> {
+ mSensorManager.unregisterListener(this, mSensor);
+ });
+ }
+}
diff --git a/parts/src/org/lineageos/settings/sensors/ProximitySensor.java b/parts/src/org/lineageos/settings/sensors/ProximitySensor.java
new file mode 100644
index 0000000..4be81fa
--- /dev/null
+++ b/parts/src/org/lineageos/settings/sensors/ProximitySensor.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2015 The CyanogenMod Project
+ * 2017-2018 The LineageOS 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.lineageos.settings.sensors;
+
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.util.Log;
+
+import org.lineageos.settings.doze.DozeUtils;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+public class ProximitySensor implements SensorEventListener {
+
+ private static final boolean DEBUG = false;
+ private static final String TAG = "ProximitySensor";
+
+ // Maximum time for the hand to cover the sensor: 1s
+ private static final int HANDWAVE_MAX_DELTA_NS = 1000 * 1000 * 1000;
+
+ // Minimum time until the device is considered to have been in the pocket: 2s
+ private static final int POCKET_MIN_DELTA_NS = 2000 * 1000 * 1000;
+ private boolean mSawNear = false;
+ private SensorManager mSensorManager;
+ private Sensor mSensor;
+ private Context mContext;
+ private ExecutorService mExecutorService;
+ private long mInPocketTime = 0;
+
+ public ProximitySensor(Context context) {
+ mContext = context;
+ mSensorManager = mContext.getSystemService(SensorManager.class);
+ mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY, false);
+ mExecutorService = Executors.newSingleThreadExecutor();
+ }
+
+ private Future> submit(Runnable runnable) {
+ return mExecutorService.submit(runnable);
+ }
+
+ @Override
+ public void onSensorChanged(SensorEvent event) {
+ boolean isNear = event.values[0] < mSensor.getMaximumRange();
+ if (mSawNear && !isNear) {
+ if (shouldPulse(event.timestamp)) {
+ DozeUtils.launchDozePulse(mContext);
+ }
+ } else {
+ mInPocketTime = event.timestamp;
+ }
+ mSawNear = isNear;
+ }
+
+ private boolean shouldPulse(long timestamp) {
+ long delta = timestamp - mInPocketTime;
+
+ if (DozeUtils.isHandwaveGestureEnabled(mContext) &&
+ DozeUtils.isPocketGestureEnabled(mContext)) {
+ return true;
+ } else if (DozeUtils.isHandwaveGestureEnabled(mContext)) {
+ return delta < HANDWAVE_MAX_DELTA_NS;
+ } else if (DozeUtils.isPocketGestureEnabled(mContext)) {
+ return delta >= POCKET_MIN_DELTA_NS;
+ }
+ return false;
+ }
+
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {
+ /* Empty */
+ }
+
+ public void enable() {
+ if (DEBUG) Log.d(TAG, "Enabling");
+ submit(() -> {
+ mSensorManager.registerListener(this, mSensor,
+ SensorManager.SENSOR_DELAY_NORMAL);
+ });
+ }
+
+ public void disable() {
+ if (DEBUG) Log.d(TAG, "Disabling");
+ submit(() -> {
+ mSensorManager.unregisterListener(this, mSensor);
+ });
+ }
+
+ public boolean getSawNear() {
+ return mSawNear;
+ }
+}
diff --git a/parts/src/org/lineageos/settings/sensors/SensorsUtils.java b/parts/src/org/lineageos/settings/sensors/SensorsUtils.java
new file mode 100644
index 0000000..eac94bb
--- /dev/null
+++ b/parts/src/org/lineageos/settings/sensors/SensorsUtils.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2015 The CyanogenMod Project
+ * 2020 The LineageOS 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.lineageos.settings.sensors;
+
+import android.hardware.Sensor;
+import android.hardware.SensorManager;
+
+public final class SensorsUtils {
+ public static Sensor getSensor(SensorManager sm, String type) {
+ for (Sensor sensor : sm.getSensorList(Sensor.TYPE_ALL)) {
+ if (type.equals(sensor.getStringType())) {
+ return sensor;
+ }
+ }
+ return null;
+ }
+}
diff --git a/parts/src/org/lineageos/settings/utils/FileUtils.java b/parts/src/org/lineageos/settings/utils/FileUtils.java
new file mode 100644
index 0000000..2228ff8
--- /dev/null
+++ b/parts/src/org/lineageos/settings/utils/FileUtils.java
@@ -0,0 +1,166 @@
+/*
+ * 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.lineageos.settings.utils;
+
+import android.util.Log;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+
+public final class FileUtils {
+ private static final String TAG = "FileUtils";
+
+ private FileUtils() {
+ // This class is not supposed to be instantiated
+ }
+
+ /**
+ * Reads the first line of text from the given file.
+ * Reference {@link BufferedReader#readLine()} for clarification on what a
+ * line is
+ *
+ * @return the read line contents, or null on failure
+ */
+ public static String readOneLine(String fileName) {
+ String line = null;
+ BufferedReader reader = null;
+
+ try {
+ reader = new BufferedReader(new FileReader(fileName), 512);
+ line = reader.readLine();
+ } catch (FileNotFoundException e) {
+ Log.w(TAG, "No such file " + fileName + " for reading", e);
+ } catch (IOException e) {
+ Log.e(TAG, "Could not read from file " + fileName, e);
+ } finally {
+ try {
+ if (reader != null) {
+ reader.close();
+ }
+ } catch (IOException e) {
+ // Ignored, not much we can do anyway
+ }
+ }
+
+ return line;
+ }
+
+ /**
+ * Writes the given value into the given file
+ *
+ * @return true on success, false on failure
+ */
+ public static boolean writeLine(String fileName, String value) {
+ BufferedWriter writer = null;
+
+ try {
+ writer = new BufferedWriter(new FileWriter(fileName));
+ writer.write(value);
+ } catch (FileNotFoundException e) {
+ Log.w(TAG, "No such file " + fileName + " for writing", e);
+ return false;
+ } catch (IOException e) {
+ Log.e(TAG, "Could not write to file " + fileName, e);
+ return false;
+ } finally {
+ try {
+ if (writer != null) {
+ writer.close();
+ }
+ } catch (IOException e) {
+ // Ignored, not much we can do anyway
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Checks whether the given file exists
+ *
+ * @return true if exists, false if not
+ */
+ public static boolean fileExists(String fileName) {
+ final File file = new File(fileName);
+ return file.exists();
+ }
+
+ /**
+ * Checks whether the given file is readable
+ *
+ * @return true if readable, false if not
+ */
+ public static boolean isFileReadable(String fileName) {
+ final File file = new File(fileName);
+ return file.exists() && file.canRead();
+ }
+
+ /**
+ * Checks whether the given file is writable
+ *
+ * @return true if writable, false if not
+ */
+ public static boolean isFileWritable(String fileName) {
+ final File file = new File(fileName);
+ return file.exists() && file.canWrite();
+ }
+
+ /**
+ * Deletes an existing file
+ *
+ * @return true if the delete was successful, false if not
+ */
+ public static boolean delete(String fileName) {
+ final File file = new File(fileName);
+ boolean ok = false;
+ try {
+ ok = file.delete();
+ } catch (SecurityException e) {
+ Log.w(TAG, "SecurityException trying to delete " + fileName, e);
+ }
+ return ok;
+ }
+
+ /**
+ * Renames an existing file
+ *
+ * @return true if the rename was successful, false if not
+ */
+ public static boolean rename(String srcPath, String dstPath) {
+ final File srcFile = new File(srcPath);
+ final File dstFile = new File(dstPath);
+ boolean ok = false;
+ try {
+ ok = srcFile.renameTo(dstFile);
+ } catch (SecurityException e) {
+ Log.w(TAG,
+ "SecurityException trying to rename " + srcPath + " to " + dstPath,
+ e);
+ } catch (NullPointerException e) {
+ Log.e(TAG,
+ "NullPointerException trying to rename " + srcPath + " to " +
+ dstPath,
+ e);
+ }
+ return ok;
+ }
+}